有没有一种简单的方法来交换Git中的已暂存和未暂存内容?

3
有时在处理某个问题时,可能会发现其他问题需要先解决。此时,我可能已经部分提交了该文件和其他已跟踪的文件(包括新文件),想要用未提交的内容替换当前的提交内容。有什么方法可以做到这一点吗?
示例会话:
$ cd "$(mktemp --directory)"
$ git init
Initialized empty Git repository in /tmp/tmp.7HssIqQ2qT/.git/
$ echo foo > foo
$ echo bar > bar
$ git add .
$ git commit --message="Initial commit"
[master (root-commit) c6db082] Initial commit
 2 files changed, 2 insertions(+)
 create mode 100644 bar
 create mode 100644 foo
$ echo foo2 > foo
$ echo baz > baz
$ git add foo baz
$ echo foo3 >> foo
$ echo bar2 > bar
$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   baz
    modified:   foo

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

    modified:   bar
    modified:   foo

基本上,如何提交上述更改以及尚未提交的更改,注意只有“foo”的部分更改已经被暂存?迄今为止我能想到的最简单的方法是创建两个存储,并以相反的顺序弹出它们,但它并不是很有效:

$ git stash --include-untracked --keep-index
Saved working directory and index state WIP on master: 3dba6aa Initial commit
$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   baz
    modified:   foo
$ git stash --include-untracked
$ git status
On branch master
nothing to commit, working tree clean
$ git stash pop stash@{1}
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   baz

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

    modified:   bar
    modified:   foo

Dropped stash@{1} (6fb8f81bdb2dc9fc1f218e7c25e74ad32c01fc0e)

不知何故,baz作为最老的stash的一部分被弹出,因此状态已经混乱。

3个回答

2
注意:在我回答下面的问题之前,我有两个注释:
  • 你可能最好使用git worktree add并在两个分支中工作,或者只是现在提交,稍后重新排列。如果您现在提交,稍后可以使用git rebase -i交换提交的顺序。我认为这样可以更优雅地实现下一个符号点中的内容。 (但您必须记住要rebase。)

  • 下面的技巧使用完整的快照,您可能正在考虑差异,如果是这种情况,则两个git read-tree可能无法获得您想要的结果。尝试一下,看看是否符合您的要求。如果不是您想要的,请考虑通过git diff --cachedgit diff制作两个补丁,并保存这两个补丁,然后进行硬重置,然后按顺序应用这两个补丁,在它们之间使用git add; git commit。请注意,这很可能会导致合并冲突!

无论如何,我认为没有什么特别优雅的方法,但即使没有--keep-indexgit stash也可以让您获得所需的提交。 (注意:我建议不使用--include-untracked。)

请记住,提交是(或更准确地说,包含)每个文件的完整快照,或者更具体地说,包含在提交时在索引中的每个文件。同时,索引中的内容是每个文件的完整副本。您的工作树中的内容取决于您; Git只会从您的工作树复制文件到索引(git add)或从索引复制到您的工作树(2.23及更高版本中的git restore)。1

与此同时,简单的git commit从当前索引中制作一个新的提交。如果匹配了HEAD中的内容,则需要添加一个标记2,但这就是它的作用。与之相比,git stash会创建两个或三个提交。第一个提交通常像往常一样从索引中进行,只是它不在任何分支上。第二个提交是通过将当前跟踪的工作树文件(即在索引中的文件)复制到索引中来进行的,因此第二个提交包含了你跟踪的工作树文件。第三个提交(如果存在)则包含未包含在第二个提交中的工作树文件(并且随后已被删除)。我们可以将前两个提交称为IW,而《git-stash》文档正是这么做的——跟随链接并阅读DISCUSSION部分。
因此,运行git stash会创建两个提交,每个提交都保存了您现在想要在工作树(I提交)或索引(W提交)中拥有的文件。因此,现在我们可以使用新的git restore命令来更新工作树,然后使用git read-tree来填充索引,或者使用两个单独的git read-tree命令(在任何版本的Git中都可以)。
git stash                  # make commits I and W which are stash^2 and stash
git read-tree -mu stash^2  # copy I commit to index and work-tree
git read-tree stash        # copy W commit to index, leaving work-tree untouched

之后我们可以使用git stash drop来丢弃存储的内容(但我这里不会这样做)。

这里稍微有点棘手的是,新加入的bazW提交中,并出现在changes staged for commit部分。因此:

On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
    modified:   bar
    new file:   baz
    modified:   foo

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
    modified:   bar
    modified:   foo

如果您不想让这些新文件出现在索引中,只需在它们上运行git rm --cached命令:

$ git rm --cached baz
rm 'baz'
$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
    modified:   bar
    modified:   foo

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
    modified:   bar
    modified:   foo

Untracked files:
  (use "git add <file>..." to include in what will be committed)
    baz

请注意,从索引中删除新文件会将其定义为未跟踪文件。(使用git diff --cached或git diff --staged(如果您更喜欢这种拼写)查看现在已暂存的内容。)
我故意略过/忽略了git checkout或git switch通常所做的事情,这涉及将提交复制到索引和工作树中,因此我们通常会执行以下操作之一:
commit -> index                # e.g., git reset --mixed
          index -> work-tree   # e.g., git checkout-index / git restore
          index <- work-tree   # e.g., git add
git restore 命令能够执行 commit -> work-tree 操作,完全绕过(不更改)索引,这是以前的任何命令都无法做到的。我们可以使用 git show HEAD:path/to/file > path/to/file 进行近似操作,它更加灵活,我们不必两次使用 相同的 路径,但取决于重定向,这就是为什么我们必须两次给出路径的原因。 2 这个标志被拼写为 --allow-empty,但是索引可能实际上并不是空的。它可能充满了文件。只是文件与 HEAD 提交中的文件匹配,因此 差异 是空的。 3 第三个提交存在的前提是您使用了 -u-a 或它们的长拼写形式,即 --include-untracked--all

如果baz的部分内容在索引中,这是否有效?我基本上正在寻找一个通用的“交换索引和工作树”命令。此外,感谢您如此深入地研究这个问题! - l0b0
如果baz的一部分只在索引中,那么索引包含文件的副本。无论副本中有什么,都会出现在索引中。这不是差异。这就是提交(包括存储)和补丁之间的关键区别:提交是快照,补丁是差异。 - torek
“'索引保存的是在你运行git add命令时文件的副本'和'这两个baz是完全相同的副本'不矛盾吗?如果有两个完全相同的 baz副本,那么第三个东西是什么保留了即将提交的内容与工作目录中的内容之间的差异?由于可能只提交新文件的一部分,所以必须有某种方式保存添加文件时的状态。” - l0b0
两个baz的副本是相同的,因为在创建和git-add之间,baz没有发生变化,以及对工作树的后续修改。而两个未提交的foo的副本则不相同。 - torek
1
是的,没错。差异 不在索引中:索引包含一个一行文件,读取 foo,而工作树包含一个两行文件,先读取 foo 然后是 bar差异 是根据这两个文件副本的请求动态计算的。 - torek
显示剩余3条评论

0

嗯...也许有一些更加优雅的方法来实现它,但你可以这样做:

# save content from index into a temp file
git show :0:path-to-file > file-from-index
# if content is in file-from-index, continue with the recipe
# save file from working tree into index
git add path-to-file
# move file as you saved from index into the right working tree path
mv file-from-index path-to-file

就是这样。


0

好的,来回答一下问题,

第一步是准备工作,并且让你只留下工作树中已经被暂存的修改:

git stash -k

接下来,要切换到仅包含未暂存更改的内容,

git read-tree -um @; git cherry-pick -nm2 stash

并切换到仅暂存更改的内容,

git read-tree -um stash^2

并且可以回到存储状态,无论您对当前索引和工作树进行了什么修改。

git read-tree -um stash
git stash drop

如果更新可能存在冲突,您可能需要使用--reset -u而不是-um


我并不想涉及另一个分支或创建任何提交(尚未),我只想交换暂存区和非暂存区内容。 - l0b0
啊,抱歉假设了额外的步骤,现在好了吗? - jthill

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接