git - 有没有一种简单的方法来交换分阶段和非分阶段的 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 worktree add
在两个分支中使用和工作,或者只是现在提交然后稍后重新安排。如果您现在提交,您可以git rebase -i
稍后使用来交换提交的顺序。我认为,这可以让您更优雅地实现下一个要点中的补丁将做什么。(但你必须记住变基。)下面的技巧使用完整快照,您可能会考虑差异,在这种情况下,两个
git read-tree
s 可能无法得到您想要的。试试看,看看是不是你想要的。如果这不是您想要的,请考虑通过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但这就是它的作用。将此与 进行比较,它会进行两次或三次提交。这些提交中的第一个像往常一样从索引中进行,除了它不在任何分支上。这些提交中的第二个是通过将您当前跟踪的工作树文件(索引中的那些文件)复制到索引中来进行的,以便第二个提交包含您跟踪的工作树文件。第三次提交,如果存在,3HEAD
git stash
包含不在第二次提交中的工作树文件(随后已被删除)。我们可以调用前两个提交I
和W
,文档就是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当且仅当您使用-u
or-a
或其更长的拼写,--include-untracked
or时,第三次提交才存在--all
。