git reset --soft 的实际用途是什么?

199

我已经使用Git一个月多了。事实上,我昨天才第一次使用reset,但是软重置对我来说仍然不太清楚。

我知道可以使用软重置来编辑提交记录而不改变索引或工作目录,就像我使用 git commit --amend 命令一样。

这两个命令真的一样吗(reset --soft vs commit --amend)?在实际应用中使用它们有什么理由?更重要的是,除了修改提交记录之外,reset --soft 还有其他用途吗?

13个回答

143

git reset主要是关于移动HEAD通常也会更新分支ref
问题:那工作树和索引呢?
当使用--soft时,它会移动HEAD,通常也会更新分支ref,但只会影响HEAD
这与commit --amend不同的地方在于:

  • 它不会创建新的提交。
  • 它实际上可以将HEAD移动到任何提交(而commit --amend只是关于移动HEAD,同时允许重新做当前提交)

刚刚找到这个合并的例子:

  • 经典合并
  • 子树合并

全部合并成一个(章鱼式,因为合并了两个以上的分支)提交合并。

Tomas "wereHamster" Carnecky 在他的"Subtree Octopus merge" 文章中解释:

  • 如果你想将一个项目合并到另一个项目的子目录中,并且随后保持子项目与主项目同步,可以使用子树合并策略。它是 git 子模块的替代方案。
  • 章鱼合并策略可用于合并三个或更多的分支。普通合并策略只能合并两个分支,如果尝试合并超过两个分支,git 会自动回退到章鱼合并策略。
问题是你只能选择一种策略。但是我想要将两者结合起来,以便获得一个干净的历史记录,其中整个存储库都会被原子方式更新到新版本。
我有一个超级项目,让我们称其为projectA,还有一个子项目projectB,我将其合并到了projectA的一个子目录中。
(这就是子树合并部分)

I'm also maintaining a few local commits.
ProjectA is regularly updated, projectB has a new version every couple days or weeks and usually depends on a particular version of projectA.

When I decide to update both projects, I don't simply pull from projectA and projectB as that would create two commits for what should be an atomic update of the whole project.
Instead, I create a single merge commit which combines projectA, projectB and my local commits.
The tricky part here is that this is an octopus merge (three heads), but projectB needs to be merged with the subtree strategy. So this is what I do:

# Merge projectA with the default strategy:
git merge projectA/master

# Merge projectB with the subtree strategy:
git merge -s subtree projectB/master

在这里作者使用了 reset --hard,然后使用 read-tree 来恢复前两次合并对工作树和索引所做的更改,但是这就是 reset --soft 可以帮助的地方:
我如何重新执行那两次已经完成的合并,即我的工作树和索引正常,但不必记录那两个提交?

# Move the HEAD, and just the HEAD, two commits back!
git reset --soft HEAD@{2}

现在,我们可以恢复Tomas的解决方案:

# Pretend that we just did an octopus merge with three heads:
echo $(git rev-parse projectA/master) > .git/MERGE_HEAD
echo $(git rev-parse projectB/master) >> .git/MERGE_HEAD

# And finally do the commit:
git commit

所以,每次:
- 你对最终的工作树和索引感到满意 - 你对达到这个状态的所有提交都不满意
`git reset --soft` 就是答案。
请注意,--no-soft 没有意义,Git 2.42(2023年第三季度)现在会告诉你。
请参见commit 3821eb6(2023年7月19日),由Junio C Hamano(gitster提交。
(由Junio C Hamano -- gitster --commit e672bc4中合并,2023年7月27日) e672bc4f76:合并分支'jc/parse-options-reset'
命令行解析器修复。* jc/parse-options-reset:reset: 拒绝--no-(mixed|soft|hard|merge|keep)选项
你会得到一个"未知选项"错误。

8
备忘录:使用git reset --soft进行压缩的简单示例:https://dev59.com/cWw15IYBdhLWcg3wCnQn - VonC
4
如果你切换到了错误的分支,这段话的意思是在提交之前将所有更改返回到暂存区,然后在切换到正确的分支时将其随着你一起移动。 - smoebody

75

应用场景 - 合并一系列本地提交

"哎呀,这三个提交可以合并成一个。"

因此,撤销最近的3个(或更多)提交(不影响索引和工作目录),然后将所有更改作为一个提交。

例如:

> git add -A; git commit -m "Start here."
> git add -A; git commit -m "One"
> git add -A; git commit -m "Two"
> git add -A' git commit -m "Three"
> git log --oneline --graph -4 --decorate

> * da883dc (HEAD, master) Three
> * 92d3eb7 Two
> * c6e82d3 One
> * e1e8042 Start here.

> git reset --soft HEAD~3
> git log --oneline --graph -1 --decorate

> * e1e8042 Start here.

现在,所有您所做的更改都已被保留,可以作为一个提交。

问题的简短回答

这两个命令真的是一样的吗(reset --soft vs commit --amend)?

  • 不是。

在实际应用中使用其中之一有任何原因吗?

  • commit --amend 用于在最后一个提交中添加/移除文件或更改其消息。
  • reset --soft <commit> 用于将几个连续的提交合并成一个新提交。

更重要的是,reset --soft 还有其他用途吗?

  • 请参见其他答案 :)


2
除了修改提交之外,是否有其他用途可以使用 reset --soft?- 没有。 - Antony Hatchkins
1
对于最后一条评论的回应 - 部分同意,但我认为修改单个提交太具体了。例如,一旦编写了一个功能,您可能希望压缩所有内容并切出新的提交,这些提交对于审阅者更有用。我认为软重置(包括普通的git reset)在以下情况下很好:(a)想要重写历史记录,(b)不关心旧提交(因此可以避免交互式变基的麻烦),以及(c)有多个提交要更改(否则,commit --amend更简单)。 - johncip
哦,哇。这真的很方便,同时也非常直观。不错! - Eliezer Berlin

25

我使用它来修改的不仅是最后一次提交。

假设我在提交A时犯了一个错误,然后又提交了B。现在我只能修改B。 所以我执行 git reset --soft HEAD^^,我更正并重新提交A,然后再次提交B。

当然,对于大型提交来说这不是很方便...但通常你也不应该做太大的提交。


4
git commit --fixup HEAD^^ 的意思是“创建一个修正上上个提交的修正提交”。 git rebase --autosquash HEAD~X 也能够很好地工作。它的作用是将当前分支与 HEAD 向前 X 次提交进行交互式重写,同时自动压缩以前使用 git commit --fixup 创建的修正提交。 - Niklas
5
当你选择编辑提交记录 A 和 B 时,使用 git rebase --interactive HEAD^^ 命令。这种方式保留了提交记录 A 和 B 的提交信息,如果需要的话,您仍然可以修改它们。 - Paul Pladijs
2
我在将代码重置为A后,如何重新提交B? - northben
1
另一种可能更省事的方法:检查您的git日志,获取B的提交哈希,然后git reset A,进行更改并添加,git commit --amendgit cherry-pick <B-commit-hash> - naught101

23

另一个潜在的用途是作为代替git stash(有些人不喜欢,例如参见https://codingkilledthecat.wordpress.com/2012/04/27/git-stash-pop-considered-harmful/)的选择。

例如,如果我正在某个分支上工作,需要紧急修复主分支上的问题,我只需执行:

git commit -am "In progress."

然后检查主分支并进行修复。当我完成后,返回到我的分支并执行

git reset --soft HEAD~1

继续在我离开的地方工作。


3
更新(现在我更了解git):--soft 在这里实际上是不必要的,除非你真的关心改变立即被暂存。现在我只用 git reset HEAD~ 就可以了。如果我需要切换分支并希望保持一些更改已经被暂存,那么我会执行 git commit -m "staged changes",然后 git commit -am "unstaged changes",之后再执行 git reset HEAD~ 并跟着执行 git reset --soft HEAD~ 来完全恢复工作状态。虽然,老实说,现在我知道了 git-worktree,这些事情我做得少多了 :) - deltacrux

13

一个实用的用途是,如果你已经提交到你的本地仓库了(例如,git commit -m),那么你可以通过执行git reset --soft HEAD~1来撤销最后一次提交。

此外,需要提醒您的是,如果您已经暂存了更改内容(例如,使用git add .),那么您可以通过执行git reset --mixed HEAD来撤销暂存,或者通常也可以使用git reset

最后,git reset --hard会清除所有东西,包括您的本地更改。在head后面的“~”告诉您要从顶部退回多少次提交。


1
git reset --soft HEAD ~1 gives me fatal: Cannot do soft reset with paths. I think we need to remove the space after HEAD so it would be git reset --soft HEAD~1 - Andrew Lohr

7

一种可能的用途是在您想要在另一台机器上继续工作时使用。它将按照以下方式工作:

  1. Checkout a new branch with a stash-like name,

    git checkout -b <branchname>_stash
    
  2. Push your stash branch up,

    git push -u origin <branchname>_stash
    
  3. Switch to your other machine.

  4. Pull down both your stash and existing branches,

    git checkout <branchname>_stash; git checkout <branchname>
    
  5. You should be on your existing branch now. Merge in the changes from the stash branch,

    git merge <branchname>_stash
    
  6. Soft reset your existing branch to 1 before your merge,

    git reset --soft HEAD^
    
  7. Remove your stash branch,

    git branch -d <branchname>_stash
    
  8. Also remove your stash branch from origin,

    git push origin :<branchname>_stash
    
  9. Continue working with your changes as if you had stashed them normally.

我认为,未来GitHub等平台应该在更少的步骤中提供这种“远程存储”功能。

2
我想指出,在你的第一台机器上进行的第一个stash和pop是完全不必要的,你可以直接从脏工作副本创建一个新分支,提交,然后将更改推送到远程。 - user456814

7
使用“git reset --soft <sha1>”的一个很好的理由是将HEAD移动到裸仓库中。如果尝试使用--mixed--hard选项,将会出现错误,因为您正在尝试修改不存在的工作树和/或索引。
注意:您需要直接从裸仓库中执行此操作。
再次注意:您需要确保在裸仓库中要重置的分支是活动分支。如果不是,请按照VonC在答案中提供的方法更新裸仓库中的活动分支,当您可以直接访问该仓库时。

1
正如你在之前的回答中提到的(https://dev59.com/SG445IYBdhLWcg3w7unD#4625151),只有在直接访问裸仓库的情况下才成立(你在这里提到了这一点)。不过还是赞一个。 - VonC
@VonC 是的,完全正确,谢谢添加注释!我总是忘记添加,因为我假设重置是直接从仓库进行的。此外,我假设在裸仓库中重置的分支是他们当前的活动分支。如果该分支不是他们的活动分支,则需要根据您的答案(https://dev59.com/NHA75IYBdhLWcg3wbofM#3302018)更新操作。我也会更新答案并提供关于活动分支的信息。再次感谢!!! - Hazok

7
您可以使用git reset --soft来改变您想要作为父级的版本,用于对索引和工作树中的更改进行更改。这种情况很少发生。有时候您可能会决定将工作树中的更改放到另一个分支上。或者您可以使用它来将几个提交折叠成一个(类似于挤压/折叠)。
请参见VonC的答案,以了解实际示例:如何在Git中挤压前两个提交?

这是一个很好的问题,我以前没有遇到过。但我不确定如何使用reset soft将更改放到另一个分支上。所以我先checkout Branch,然后使用git reset --soft anotherBranch,然后在那里commit?但你并没有真正改变checkout branch,所以你会commit到_Branch_还是_anotherBranch_? - AJJ
在执行此操作时,重要的是不要使用git checkout,因为这会更改您的树。将git reset --soft视为仅操纵修订版本HEAD指向的一种方式。 - Johannes Rudolph

4
虽然我非常喜欢这个线程中的答案,但我对git reset --soft的使用略有不同,但是在实际场景中仍然非常实用。
我使用一个集成开发环境(IDE)进行开发,它具有良好的差异工具,可以显示自上次提交以来的更改(暂存和未暂存)。现在,我的大多数任务涉及多个提交。例如,假设我进行了5次提交来完成特定的任务。在每个增量提交(从1到5)期间,我都使用IDE中的差异工具查看自上一次提交以来的更改。我认为这是一种非常有帮助的查看自己更改的方法,然后再进行提交。
但是,在我完成任务时,当我想要查看所有的更改(包括第1次提交之前)以便在发出拉取请求之前进行自我代码审查时,我只能看到我先前的提交(第4次提交后)而不是当前任务的所有提交的更改。
因此,我使用git reset --soft HEAD~4回到4次提交之前。这样我就可以看到所有的更改。当我确信我的更改后,我就可以使用git reset HEAD@{1}并自信地将其推送到远程仓库。

1
然后重复执行 git add --patchgit commit 命令,以构建提交系列,就像您一直知道自己在做什么一样。您的第一个提交就像桌子上的笔记或备忘录的第一稿,它们是为了组织您的思路,而不是用于发布。 - jthill
嘿,我不太明白你在建议什么。 - Swanky Coder
1
只需使用 git add -pgit commit 构建一个发布系列,而不是执行 git reset @{1} 来恢复您的初稿系列。 - jthill
1
是的,正确的。另一种方法是(我通常采用的方法)在upstream/master上进行rebase,并将提交压缩为一个单独的提交。 - Swanky Coder

3
SourceTree是一个git GUI,它具有非常方便的界面,可以方便地对需要的部分进行暂存。但它并没有类似于修改正确版本的功能。
因此,在这种情况下,“git reset --soft HEAD~1”比“commit --amend”更有用。我可以撤消提交操作,将所有更改返回到暂存区域,并继续使用SourceTree调整已暂存的位。
实际上,对我来说,“commit --amend”命令似乎更多余,但是Git就是Git,不会回避做略微不同的相似命令。

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