在Git中,交换已暂存和未暂存更改的最短方法是什么?

38
如果索引中添加了一些更改,而还有一些更改未添加到索引中,我应该如何交换这两组更改?

1
这是我的最终解决方案(最初发布在此处https://dev59.com/jmUq5IYBdhLWcg3wSOfR#20458127) - Eric Woodruff
7个回答

18

我认为这是最容易通过使用临时提交来完成的。当您有已暂存和未暂存的提交时,尝试重新排序更改时可能会发生冲突。

使用已暂存的更改进行提交,并创建一个分支以备将来使用:

git commit -m "Saved staged"
git branch save-staged

对于未暂存的更改,进行提交(如果包括新文件,则需要先显式地使用git add添加):

git commit -a -m "Unstaged changes"

将未暂存的更改变基于原始 HEAD (可能需要解决冲突):

git rebase --onto HEAD^^ HEAD^

将暂存的更改合并到未暂存的更改中(可能涉及冲突解决):

git reset --hard save-staged
git rebase --onto HEAD@{1} HEAD^

最后,将索引重置为(最初的)未暂存更改:

git reset HEAD^

并将分支指针移回原始HEAD位置:

git reset --soft HEAD^

删除临时分支:

git branch -D save-staged

1
@whitered:公开提问:你有什么难以理解的地方吗?reset、rebase和commit的手册应该详细描述我使用的所有选项,我使用的步骤应该是逐步的逻辑操作。 - CB Bailey

8

如果想要一个更底层的解决方案,你可以通过一些基础操作直接与索引进行通信:

INDEXTREE=`git write-tree`
git add -A
WORKTREE=`git write-tree`
git checkout $INDEXTREE -- .
git clean -f
git read-tree $WORKTREE

那个操作会在Git存储库中建立一对临时树对象,一个用于索引,另一个用于工作副本。然后,它将恢复旧的索引并将其检出到工作树中。最后,它将索引重置为表示旧工作树版本的版本。 我没有测试过这个操作,所以不确定它在索引或工作树中添加文件方面的表现如何。

谢谢你的回答!这激发了我制作一个更好的、可编写的版本,也让我制作了另一个脚本,可以仅存储已暂存的更改。 - JesusFreke

6

通过补丁的方式(对于二进制更改无效):

保存暂存状态和未暂存状态的补丁

git diff >> unstaged.patch
git diff --cached >> staged.patch

应用未曾提交的更改。
git reset --hard
git apply unstaged.patch

除了补丁文件外,将这些更改放入暂存区

git add -A
git reset -- staged.patch unstaged.patch

应用最初的分阶段更改。
git apply staged.patch

删除补丁文件

rm staged.patch unstaged.patch

有趣的解决方案。+1。只是出于好奇,您是否重新尝试了我更新后的 git stash 答案? - VonC
1
是的,我已经重新尝试过了,除非有一些新的(之前未跟踪的)文件被暂存,否则它都可以正常工作。我的补丁变体在这种情况下也会失败。 - whitered

5

这是基于 Walter Mundt 的回答 改进的,适用于新文件已暂存的情况下。它旨在作为脚本使用,例如 git-invert-index

#!/bin/sh

# first, go to the root of the git repo
pushd `git rev-parse --show-toplevel`

# write out a tree with only the stuff in staging
INDEXTREE=`git write-tree`

# now write out a tree with everything
git add -A
ALL=`git write-tree`

# get back to a clean state with no changes, staged or otherwise
git reset -q --hard
git clean -fd

# apply the changes that were originally staged, that we want to
# be unstaged
git checkout $INDEXTREE -- .
git reset

# apply the originally unstaged changes to the index
git diff-tree -p $INDEXTREE $ALL | git apply --index --reject

# return to the original folder
popd

(请注意,如此编写可能会在 git clean 过程中从工作副本中删除一些空子目录。鉴于 Git 对空目录的普遍忽略,这可能很难避免。) - Walter Mundt
你为什么要使用“diff-tree | apply; add -A”而不是“checkout $INDEXTREE -- .”?请注意,使用树形结构的checkout不会更改您的分支,因此两者应该具有相同的净效果,但是checkout应该更快,因为它不必构建和解析补丁文件,然后重新扫描工作树以填充索引。 - Walter Mundt
对于第一个 diff-tree,您希望查看已保存的 INDEXTREE 和所有树之间的更改。仅检出其中一个并不是您想要的。对于第二种情况,使用 git checkout 将会把 INDEXTREE 中的更改添加到索引中,但它们应该处于未暂存状态。 - JesusFreke
@WalterMundt 谢谢您的查看。当时我没有考虑到 git checkout,我总是乐于接受建议 :) - JesusFreke
看起来结账可以处理未暂存的更改,如果您在暂存更改之前这样做。已更新脚本。谢谢! - JesusFreke
显示剩余3条评论

4

Charles Bailey提供了一个更完整的解决方案,涉及提交和管理潜在的冲突解决。

我最初尝试只使用git stash,但我一开始忽略了git stash save会保存索引(已暂存的更改)未暂存的更改(当您想交换索引内容和未暂存的更改时不方便)。

因此,我改为以下方法:

  • git commit -m "临时提交"(为当前索引创建一个提交)
  • git stash(将未添加到索引的内容存储起来)
  • git reset --soft HEAD^(保留先前提交的文件)
  • git stash再次执行
  • git stash pop stash@{1}(应用之前存储的内容,即尚未添加到索引的初始更改)
  • git add -A
  • git stash drop stash@{1}以清除我们之前应用的存储(stash@ {0} 仍包含最初在索引中的内容)

最终结果:

  • 未添加到索引的内容现在已添加。
  • 最初在索引中的内容被存储了起来。

git stash 意味着 git reset --hard,因此 git reset --mixed 是无操作的,第二个 git stash 没有任何作用? - CB Bailey
1
第一步应该是 git stash save --keep-index,我猜在第四步放弃之前必须先应用第一个存储。但无论如何,这种方式对我来说不起作用,因为如果我有两个存储并尝试应用其中一个,两个存储都会被应用,原因无法解释。 - whitered
我就是无法让存储(stash)方法生效。问题在于第二次存储会保存所有的更改(工作树和 HEAD 在两次存储之间不会发生变化),即使记录了这些更改是否被暂存。应用第二个存储总是应用了所有更改。 - CB Bailey
@Charles: "当您想记录工作目录和索引的当前状态时,请使用git stash"。我完全忽略了这个小事实;) 为了好玩,我发布了另一个“git stash”版本。 - VonC
1
@VonC 不用谢。 :-) 我真的觉得这种方法是所有提供答案中最直观的,所以我想帮助其他人也能够轻松成功地应用它。这也将有助于我拥有一个好的参考,我肯定会在以后再次谷歌搜索(6个月后当我需要它时并且已经忘记了确切的步骤)! - Magne
显示剩余4条评论

2
可以分为三个步骤完成:
git stash push -S
git add .
git stash pop

你还可以像这样将所有内容包裹在一个命令中:
alias swap='git stash push -S && git add . && git stash pop'

现在你可以直接在命令行上执行swap

这样行不通。Git 以不能储藏既有暂存又有未暂存更改的文件而闻名。这正是我想要进行交换的主要原因,这样我就可以不再使用破碎的 1. 储藏 2. 提交,而是可以 1. 交换,2. 提交,3. 储藏我最初暂存的内容。 - undefined
@MarkJeronimus我不同意你的说法,Git无法储藏既有暂存又有未暂存更改的文件。无论如何,我认为你应该先尝试这个命令,以验证其是否有效,这样你的回应才能更现实而非理想化。 - undefined
我看到这是在Git 2.35中添加的,这个版本发布不到两年。我遇到这个问题的时间要长得多,那时还没有解决方案。此外,我使用的Linux Mint附带的Git版本仍然是2.25,因为它们通常不太进步(除非你使用Arch或其他发行版)。是时候看看是否有更新的PPA了。 - undefined

0

使用 `git rebase -i` 是最好的解决方案。

替代方案(太长):

Note the name of your current branch as:
<name-of-your-branch>

git checkout -b temp-unstaged
git commit -m "staged"
git branch temp-staged
git add .
git commit -m "unstaged"
git rebase --onto HEAD~2 HEAD~1
git checkout temp-staged
git rebase temp-unstaged
git reset HEAD~1
git reset --soft HEAD~1
git checkout <name-of-your-branch>
git branch -D temp-staged
git branch -D temp-unstaged

最后,我们可以验证已经暂存和未暂存的更改已经交换。

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