如何将我的最后N个提交压缩在一起?

5289

我该如何将最近的 N 次提交压缩成一个提交记录?


8
相关:Git - 推送前合并多个提交记录 - user456814
3
要将多个 Git 提交压缩成一个,请参考此链接 - https://dev59.com/CnI-5IYBdhLWcg3w0cKG#9254257。 - goelakash
3
挤压操作后需要进行强制推送(force push)。该网址(https://dev59.com/NWkv5IYBdhLWcg3w3Ug0)提示需要合并才能推送至Github。 - vikramvi
3
除了已发布的答案外,GUI客户端可以轻松实现这一点。我可以在GitKraken中只用四个点击压缩数十个提交。 - Aaron Franke
我尝试的所有答案都搞砸了子模块。这个答案没有。 - ridilculous
为什么没有像 git fold --from abc --to efg 这样的命令? - user27182
46个回答

5475
你可以很容易地做到这一点,而不需要使用git rebasegit merge --squash。在这个例子中,我们将合并最后3个提交。
如果你想从头开始编写新的提交信息,只需这样做即可:
git reset --soft HEAD~3 &&
git commit

如果你想要开始编辑新的提交信息,并将现有的提交信息连接起来(即类似于使用 pick/squash/squash/.../squash 的 git rebase -i 指令列表所开始的方式),那么你需要提取这些信息并传递给 git commit。
git reset --soft HEAD~3 && 
git commit --edit -m"$(git log --format=%B --reverse HEAD..HEAD@{1})"

这两种方法都将最后三个提交压缩成一个新的提交。软重置只是将HEAD重新指向您不想压缩的最后一个提交。软重置不会影响索引或工作树,使得索引处于所需状态以进行新的提交(即它已经包含了您即将“丢弃”的提交中的所有更改)。
根据评论进行编辑
您已经重写了历史记录,因此必须使用--force标志将此分支推送回远程。这就是--force标志的用途,但您可以更加小心,始终完全定义您的目标。
git push --force origin myBranch

297
哈!我喜欢这种方法。它最接近问题的本质。可惜需要很多巫术操作。应该将这种方法添加到基本命令中,比如可能是 git rebase --squash-recent 或者甚至是 git commit --amend-many - Adrian Ratnapala
17
如果你的分支有“上游”设置,那么你可以使用branch@{upstream}(或者对于当前分支只需使用@{upstream};在这两种情况下,最后一部分可以缩写为@{u};参见 gitrevisions)。这可能与你的“上次推送的提交”不同(例如,如果其他人推送了建立在你最近推送的提交之上的内容,然后你获取了该内容),但似乎接近你想要的。 - Chris Johnsen
157
这有点需要我使用 push -f,但除此之外一切都很好,谢谢。 - 2rs2ts
59
注意:@2rs2ts git push -f听起来很危险。请确保只压缩本地提交记录,不要触及已推送的提交记录! - Matthias M
39
我需要使用 git push --force 命令来提交并更新该次提交。 - Zach Saucier
显示剩余33条评论

2935

使用git rebase -i <after-this-commit>命令,在第二个及后续的提交记录中将"pick"替换成"squash"或"fixup",具体步骤参考手册

在本例中,<after-this-commit>可以是SHA1哈希值,也可以是相对于当前分支HEAD位置的相对路径,用于分析rebase命令的提交记录。例如,如果用户想要查看当前HEAD以前5个提交记录,命令为git rebase -i HEAD~5


165
“<after-this-commit>” 是什么意思? - 2540625
75
<after-this-commit> 表示提交X+1,即你想压缩的最早提交的父提交。 - joozek
27
如果您已经推送了提交记录,您需要使用“push -f”的方式强制将远程分支移动到您的新提交记录。这会影响那些在旧提交记录上工作的人们。 - interfect
87
这种“rebase -i”方法和“reset --soft”的区别在于,“rebase -i”允许我保留提交者的信息,而“reset --soft”允许我重新提交。有时我需要压缩合并拉取请求的提交,同时保留作者信息。有时我也需要对自己的提交执行软重置。无论如何,这两个很好的答案都得到了赞同。 - zionyx
56
请使用git rebase -i <after-this-commit> 命令,并按照手册中描述的方法将第二个及以后的提交中的“pick”替换为“squash”或“fixup”,以此来合并提交历史记录。 - Cheeso
显示剩余16条评论

980

你可以使用 git merge --squash 来实现这一点,这比 git rebase -i 稍微更加优雅。假设你在主分支上且想要将最近的12个提交压缩成一个。

警告:首先确保你已提交你的工作——检查 git status 是否干净(因为 git reset --hard 会丢弃已暂存和未暂存的更改)。

然后执行以下操作:

# Reset the current branch to the commit just before the last 12:
git reset --hard HEAD~12

# HEAD@{1} is where the branch was just before the previous command.
# This command sets the state of the index to be as it would just
# after a merge from that commit:
git merge --squash HEAD@{1}

# Commit those squashed changes.  The commit message will be helpfully
# prepopulated with the commit messages of all the squashed commits:
git commit

Git合并文档详细描述了--squash选项。


更新:相较于Chris Johnsen在他的回答中建议的简单方式git reset --soft HEAD~12 && git commit,这种方法唯一的优点是您可以获得合并每个提交消息的预填信息。


108
@Mark Amery: 我认为这种方法更加优雅的原因有很多。比如说,它不需要无谓地生成一个编辑器,然后在“待办事项”文件中搜索和替换字符串。而且,在脚本中使用git merge --squash更容易。本质上,这种做法的理由是你根本不需要git rebase -i的“交互性”。 - Mark Longair
2
尽管我欣赏在进行大型更改时拥有冗长的提交消息的优点,但这种方法与Chris的方法相比也存在真正的缺点:执行**硬重置(git reset --hard)会触及更多的文件。例如,如果您正在使用Unity3D,则会感激较少的文件被触及。 - cregox
15
另一个优点是,与 rebase 相比,git merge --squash 在面对移动、删除和重命名时更不容易产生合并冲突,特别是当你从本地分支合并时。(声明:基于仅有的一次经验,如果这在一般情况下不正确,请纠正我!) - Cheezmeister
4
每当硬重置时,我总是非常不情愿 - 我会使用临时标记而不是 HEAD@ {1},只是为了保险起见,例如当您的工作流由于停电等原因被中断一个小时。 - Tobias Kienzler
12
@B T:你的提交被删除了吗?:( 我不确定你的意思。你提交过的任何东西都可以从git的reflog轻松恢复。如果你有未提交的工作,但文件已经被暂存,你仍然可以恢复它们的内容,尽管这需要更多的工作。但是,如果你的工作甚至没有被暂存,恐怕没什么办法;这就是为什么答案最前面说:“首先检查git status是否干净(因为git reset --hard将放弃已暂存和未暂存的更改)”。 - Mark Longair
显示剩余11条评论

356

2020年简单的解决方案,无需变基:

git reset --soft HEAD~2 
git commit -m "new commit message"
git push -f

2表示最后两个提交将被合并。您可以用任何数字替换它。

31
这个做法不应该被鼓励。在push命令中使用“-f(强制)”选项是一种危险的做法,特别是当你要推送到一个共享版本库(即公共历史记录)时,这会给贡献者带来很多麻烦。 - FXQuantTrader
5
如果目标是将此新提交作为正在进行的PR的一部分添加到主分支中,则可以使用git reset --soft $(git merge-base feature master),然后使用git commit - Karl Pokus
3
如果每个人都强制推送,情况正好相反。几乎总是需要使用 --force 强制推送。这样做可以让你与团队的其他成员以及不同的远程位置保持一致,因为你知道正在发生什么。而且你还知道历史的哪部分已经稳定了。没有必要把这些隐藏起来。 - hakre
18
我认为,应该积极鼓励使用force推送,并清楚地传达这种方法的影响。强制推送与变基和工作同步紧密相关,因此,在我看来,更多人了解此操作的效果比将其作为外部命令让人们害怕使用要好得多。 - Matheus Felipe
11
同意Matheus的观点,强制推送到特性分支是可以接受的,并且应该鼓励使用变基(rebase)。直接推送到主分支(main)始终应该有限制。 - Rush
显示剩余11条评论

316

我建议尽可能避免使用git reset,特别是对于Git新手。除非你真的需要基于提交数量自动化一个过程,否则有一种不太复杂的方法...

  1. 将要被压缩的提交放在一个工作分支上(如果它们还没有),使用gitk来执行此操作。
  2. 检出目标分支(例如“master”)。
  3. git merge --squash (工作分支名称)
  4. git commit

提交消息将根据压缩预填充。


11
这是最安全的方法:不使用软件/硬件重置(!!),也不使用reflog! - TeChn4K
37
如果您能详细说明(1),那就太好了。 - Adam
4
基本上,这意味着使用 gitk 的图形用户界面来标记你要压缩的代码行,并标记要压缩到哪个基础上。在正常情况下,这两个标签已经存在,因此可以跳过第一步。 - Brent Bradburn
6
请注意,此方法不会将工作分支标记为已完全合并,因此删除它需要强制删除。:( - Kyrstellaine
3
对于(1),我发现 git branch your-feature && git reset --hard HEAD~N 是最方便的方法。但是,它确实涉及到再次使用 git reset,而这个答案试图避免这种情况。 - eis
显示剩余9条评论

312

感谢 这篇实用的博客文章,我发现你可以使用以下命令将最近的3次提交压缩成一个:

git rebase -i HEAD~3

即使您在本地分支上没有任何跟踪信息或远程库,也很方便使用以下命令:

git rebase -i HEAD~3将打开交互式变基编辑器,然后可以像正常情况下一样重新排序、压缩、重新命名等操作。


使用交互式变基编辑器:

交互式变基编辑器显示了最近的三个提交。 运行命令git rebase -i HEAD~3时,此约束由HEAD~3确定。

最近的提交HEAD显示在第一行。 以#开头的行是注释/文档。

显示的文档非常清晰。 在任何给定的行上,您可以从pick命令更改为其他命令。

我喜欢使用fixup命令,因为它会将提交的更改“压缩”到上一行的提交中并且丢弃提交的消息。

由于第一行上的提交是HEAD,在大多数情况下,您会将其保留为pick。 您不能使用squashfixup,因为没有其他提交可以将其压缩到。

您还可以更改提交的顺序。 这使您可以压缩或修复不按时间顺序相邻的提交。

interactive rebase editor


一个实用的例子

最近我添加了一个新功能。 然后,我修复了两个错误。 但是现在我发现了一个bug(或者只是一个拼写错误)在我添加的新功能中。 多么烦人啊! 我不想让我的提交历史记录被污染!

我首先纠正错误并创建一个新的提交,并注释为squash this into my new feature!.

然后,我运行git loggitk 并获取新功能的提交SHA(在这种情况下是1ff9460)。

接下来,我使用git rebase -i 1ff9460~打开交互式变基编辑器。 在提交SHA之后加上~告诉编辑器将该提交包含在编辑器中。

接下来,我将包含修复的提交(fe7f1e0)移动到新功能提交的下面,并将pick更改为fixup

在关闭编辑器时,修复将被压缩到新功能提交中,我的提交历史记录会变得干净整洁!

这在所有提交都是本地时非常有效,但如果您尝试更改已推送到远程的任何提交,则可能会给其他开发人员造成问题!

enter image description here


8
你必须选择最佳选项并压制其他选项吗?你应该编辑你的答案,详细说明如何使用交互式变基编辑器。 - Kellen Stuart
4
是的,在第一行保留 "pick"。如果你在第一行选择了 "squash" 或者 "fixup",git 会显示一个消息:"error: cannot 'fixup' without a previous commit"。然后它会给出两个选项: "You can fix this with 'git rebase --edit-todo' and then run 'git rebase --continue'." 或者你可以中止操作并重新开始:"Or you can abort the rebase with 'git rebase --abort'."。 - br3nt
更多关于 git rebase -interactive 的信息请参见此处 - Timo
4
@Timo,正确。最老的在最上面,最新的在最下面。这就是为什么您需要“挑选”第一行。当您选择一行上的“压缩”或“修复”时,它将把更改放入上一行的提交中。 - br3nt
2
当你想要合并一定数量的提交或者至少查看可以合并的提交时,以下代码片段可能是最佳答案。通常我会使用它。 - Staghouse
显示剩余3条评论

161

根据Chris Johnsen的答案,

在bash(或Windows上的Git Bash)中添加全局的“squash”别名。

git config --global alias.squash '!f(){ git reset --soft HEAD~${1} && git commit --edit -m"$(git log --format=%B --reverse HEAD..HEAD@{1})"; };f'

...或者使用Windows的命令提示符:
git config --global alias.squash "!f(){ git reset --soft HEAD~${1} && git commit --edit -m\"$(git log --format=%B --reverse HEAD..HEAD@{1})\"; };f"

你的 `~/.gitconfig` 现在应该包含这个别名:
[alias]
    squash = "!f(){ git reset --soft HEAD~${1} && git commit --edit -m\"$(git log --format=%B --reverse HEAD..HEAD@{1})\"; };f"

用法:
git squash N

... 这会自动将最后的 N 个提交合并在一起。

注意:生成的提交消息是所有合并的提交按顺序组合而成的。如果您对此不满意,您可以随时使用 git commit --amend 手动修改它。(或者,编辑别名以符合您的喜好。)


7
有趣,但我更愿意自己打压缩提交信息,用描述性的方式概括我的多个提交,而不是让它自动输入。因此,我宁愿指定 git squash -m "New summary." 并让 N 自动确定为未推送的提交数量。 - Asclepius
2
@A-B-B,这听起来像是一个单独的问题。(我认为这不完全是OP所问的;在我的git squash工作流程中,我从未感到需要它。) - EthanB
4
这很不错。就个人而言,我希望有一个版本可以使用合并在一起的提交中第一个提交的提交信息。这对于像空格微调之类的事情很有用。 - funroll
@SteveClay 在这附近:git commit --edit -m\"$(git log --format=%B --reverse HEAD..HEAD@{1})\" :) - EthanB
3
你可以使用 git commit --amend 进一步更改提交信息,但这个别名使你能够开始撰写应该包含在提交信息中的良好内容。 - dashesy
显示剩余7条评论

94

在您希望合并提交的分支上运行:

git rebase -i HEAD~(n number of commits back to review)

例子:

git rebase -i HEAD~2

如果您想要将这些提交合并在一起,您需要打开文本编辑器,并将每个提交前面的 'pick' 切换为 'squash'。来自文档:

p, pick = 使用提交

s, squash = 使用提交,但与前一个提交合并

例如,如果您希望将所有提交合并为一个,则 'pick' 是您进行的第一个提交,所有未来的提交(放置在第一个提交下方)都应设置为 'squash'。如果使用vim,请在插入模式下使用:x保存退出编辑器。

然后,继续变基:

git add .

git rebase --continue

查看更多关于重写提交历史的方法和其他信息,请参见这篇有用的文章


4
请同时解释一下 --continue 和 vim 的 :x 是什么意思。 - not2qubit
2
重新定位将按块进行,因为它通过您分支上的提交进行操作。在添加正确的文件配置后,使用git rebase --continue来移动到下一个提交并开始合并。:x是一个命令,当使用vim时保存文件更改,请参见 - aabiro

89

要做到这一点,您可以使用以下git命令。

 git rebase -i HEAD~n

n(这里等于4)是最后一个提交的编号。然后您将获得以下选项,

pick 01d1124 Message....
pick 6340aaa Message....
pick ebfd367 Message....
pick 30e0ccb Message....

请像下面这样更新:pick 一个提交,将其他提交 squash 到最新的提交中。

p 01d1124 Message....
s 6340aaa Message....
s ebfd367 Message....
s 30e0ccb Message....

有关详细信息,请单击链接


有关 git rebase -interactive 的更多详细信息,请参见此处的 SO(https://dev59.com/ZFsV5IYBdhLWcg3w2h_R#35704829)。 - Timo

81

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