首页 > 解决方案 > 有没有一种简单的方法来交换分阶段和非分阶段的 Git 内容?

问题描述

有时在做某事时,我会发现其他需要先修复的东西。此时我可能已经暂存了该文件的一部分和其他一些文件(新的和跟踪的),我想将当前暂存内容换成非暂存内容。有没有办法做到这一点?

示例会话:

$ 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 作为最古老的藏匿处的一部分被弹出,所以状态已经一团糟。

标签: git

解决方案


注意:在我深入了解下面的答案之前,我有两个注意事项:

  • 您可能最好git worktree add在两个分支中使用和工作,或者只是现在提交然后稍后重新安排。如果您现在提交,您可以git rebase -i稍后使用来交换提交的顺序。我认为,这可以让您更优雅地实现下一个要点中的补丁将做什么。(但你必须记住变基。)

  • 下面的技巧使用完整快照,您可能会考虑差异,在这种情况下,两个git read-trees 可能无法得到您想要的。试试看,看看是不是你想要的。如果这不是您想要的,请考虑通过git diff --cached和 然后制作两个补丁git diff,保存这两个补丁,进行硬重置,然后按顺序应用两个补丁中的每一个,并git add; git commit在它们之间添加 a。请注意,这很可能会出现合并冲突!

无论如何,我不认为有什么特别优雅的东西,但是git stash——即使没有——--keep-index也许能得到你想要的提交。(注意:我建议不要使用--include-untracked。)

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

同时,一个简单的从你现在git commit索引中的任何内容进行新的提交。 如果这与您需要添加的标志相匹配,则2但这就是它的作用。将此与 进行比较,它会进行两次或三次提交。这些提交中的第一个像往常一样从索引中进行,除了它不在任何分支上。这些提交中的第二个是通过将您当前跟踪的工作树文件(索引中的那些文件)复制到索引中来进行的,以便第二个提交包含您跟踪的工作树文件。第三次提交,如果存在,3HEADgit stash包含不在第二次提交中的工作树文件(随后已被删除)。我们可以调用前两个提交IW文档就是git-stash这样做的——点击链接并阅读讨论部分。

因此,运行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(尽管我不会在这里)。

这里有点棘手的一个部分是baz,这是新的,位于提交W中,因此出现在该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当且仅当您使用-uor-a或其更长的拼写,--include-untrackedor时,第三次提交才存在--all


推荐阅读