在Git中如何压缩前两个提交?

679

使用git rebase --interactive <commit>命令,您可以将任意数量的提交合并成一个单独的提交。

很好,但是如果想将提交压缩到初始提交中,则似乎不可能进行。

有什么方法可以实现吗?


相关问题:

在一个相关的问题中,我设法提出了另一种针对需要对第一个提交进行压缩的方法,那就是将其变为第二个提交。

如果您感兴趣:git:如何将提交插入到第一个位置,同时将所有其他提交向后移动?


相关:在Git中编辑根提交? - user456814
使用别名 squash =!“f(){NL = $ 1; GIT_EDITOR = \”sed -i'2,$ NL s / pick / squash /; /#这是第二个提交消息:/,$ {d}'\“; git rebase -i HEAD〜$ NL;}; f” 进行一行命令 git squash 2。请参阅 http://stackoverflow.com/a/28789349/670229。 - brauliobo
1
关于“squash”脚本:由于有两个提交,因此此脚本创建的HEAD~2不存在,因此在这种特定情况下无法使用。 - ijw
9个回答

947

2012年7月更新 (git 1.7.12+)

现在,您可以将所有提交的内容变基到根,并选择第二次提交Y与第一次提交X合并。

git rebase -i --root master

pick sha1 X
squash sha1 Y
pick sha1 Z
git rebase [-i] --root $tip

This command can now be used to rewrite all the history leading from "$tip" down to the root commit.

查看来自Chris Webb (arachsys)commit df5df20c13 (rebase -i:支持不带--onto的--root, 2012-06-26)在GitHub上。

正如评论中所述,在任何rebase操作后,如果您需要在远程存储库中发布该重制版,则需要使用git push --force-with-lease(比--force更安全,因为Mikko Mantalainen 提醒我们)。


原始答案(2009年2月)

我相信你会在SO问题“如何将Git存储库的前两个提交组合在一起?”中找到不同的解决方案。

Charles Bailey在那里提供了最详细的答案,提醒我们提交是完整的树(不仅是与先前状态的差异)。
在这里,旧的提交("初始提交")和新的提交(压缩的结果)将没有共同的祖先。
这意味着你不能将初始提交"commit --amend"成为新的提交,然后将以前初始提交的历史记录(大量冲突)重新基于新的初始提交。

(使用git rebase -i --root <aBranch>就不再是事实了)

相反(使用A原始的"初始提交",B是需要压缩到初始提交的后续提交):

  1. Go back to the last commit that we want to form the initial commit (detach HEAD):

    git checkout <sha1_for_B>
    
  2. Reset the branch pointer to the initial commit, but leaving the index and working tree intact:

    git reset --soft <sha1_for_A>
    
  3. Amend the initial tree using the tree from 'B':

    git commit --amend
    
  4. Temporarily tag this new initial commit (or you could remember the new commit sha1 manually):

    git tag tmp
    
  5. Go back to the original branch (assume master for this example):

    git checkout master
    
  6. Replay all the commits after B onto the new initial commit:

    git rebase --onto tmp <sha1_for_B>
    
  7. Remove the temporary tag:

    git tag -d tmp
    
那么,"rebase --onto"在合并过程中不会引入冲突,因为它将历史记录(B之后的记录)变基到tmp(表示压缩后的新初始提交),以便与最初的提交(A)进行平凡快进合并。这适用于"A-B",也适用于"A-...-...-...-B"(任意数量的提交可以通过这种方式压缩成一个初始提交)。

2
很好的提示。我会记住的。唉,我在一个“git svn”仓库上尝试了它,但是它确实断开了与svn的连接。没关系,我有备份... - towi
1
@MattHuggins 但是如果你进行变基,那么你必须使用--force选项推送,而不能只是简单地推送。历史记录已经被更改(SHA1不同),因此推送不再是快进的。我确认如果你先拉取,然后再推送,你最终会得到重复的提交。请参见https://dev59.com/JGs05IYBdhLWcg3wFN8h - VonC
1
请注意,如果您的上游存储库(即您要推送到的存储库)是本地可访问的,则其他人将不知道您是否执行了 push --force ;) 请参阅 https://dev59.com/jmUp5IYBdhLWcg3wz58H。@MattHuggins - VonC
1
@MikkoRantalainen 谢谢,好的建议。我已经将您的评论包含在答案中以增加可见性,并附上了--force--force-with-lease之间差异的链接。 - VonC
1
这在2022年仍然很有用。也许我们都应该将第一次提交留空,这样就不需要发生这种情况,每个人都可以使用HEAD~1来使他们的生活更轻松。 - Little Jack
显示剩余14条评论

35

如果您只想将所有提交合并为一个初始提交,只需重置存储库并修正第一个提交:

git reset hash-of-first-commit
git add -A
git commit --amend

Git reset命令会保留工作树,因此所有文件都还在。只需使用git add命令添加文件,并使用这些更改修改第一个提交。与交互式变基(rebase -i)相比,您将失去合并git注释的能力。


33

我重新修改了VonC的脚本,使其可以自动完成所有操作而不需要我进行任何操作。你只需提供两个提交SHA1值,它将会把这两个提交之间的所有内容压缩成一个名为“压缩历史”的提交:

#!/bin/sh
# Go back to the last commit that we want
# to form the initial commit (detach HEAD)
git checkout $2

# reset the branch pointer to the initial commit (= $1),
# but leaving the index and working tree intact.
git reset --soft $1

# amend the initial tree using the tree from $2
git commit --amend -m "squashed history"

# remember the new commit sha1
TARGET=`git rev-list HEAD --max-count=1`

# go back to the original branch (assume master for this example)
git checkout master

# Replay all the commits after $2 onto the new initial commit
git rebase --onto $TARGET $2

请注意,此方法仅适用于最近的提交记录,无法在历史记录中重新定位提交。 - Krystian
请参考此答案 - user456814

24

4
如果Git变得不那么疯狂,它应该可以自动完成这项任务。有一种不错的方法可以将这样一个初始提交插入到现有的代码库中... https://dev59.com/mnRB5IYBdhLWcg3wXmNI#14729121 - Sam Watkins
1
git commit --allow-empty -m empty 经常是我的第一个提交。这甚至避免了在提交中添加 .gitignore 文件的“污染”。请注意,一些旧工具可能无法查看这样的空树。 - Tino

5
这将把第二次提交合并到第一次提交中:
A-B-C-... -> AB-C-...
git filter-branch --commit-filter '
    if [ "$GIT_COMMIT" = <sha1ofA> ];
    then
        skip_commit "$@";
    else
        git commit-tree "$@";
    fi
' HEAD

AB的提交信息将从B中获取(尽管我更喜欢从A中获取)。

与Uwe Kleine-König的答案具有相同的效果,但对于非初始A也有效。


@Anothony - 你好,作为一个初学者的git用户,我不确定这是否适合我的需求,但它看起来很有前途。你能否再解释一下?我正在尝试将所有的git提交压缩成一个,以便在现有项目中进行挑选(保留初始提交是可以的)。然而,由于有许多项目,git rebase -i并不适用于脚本化。这个命令对我有用吗?我需要指定第一个提交(A)的哈希值,C是HEAD吗?如果你能提供更多的解释,那就太好了!非常感谢! - marked
将所有提交压缩为一个通常不需要合并两个项目。解释为什么需要在单独的问题中提出。 "如何为第一次提交(A)指定哈希,其中C是HEAD?"也是一个独立的问题。 git rev-list --reverse HEAD | head -n1可能是答案。 - Antony Hatchkins

3
压缩第一个和第二个提交会导致第一个提交被重写。如果有多个基于第一个提交的分支,您将切断该分支。
考虑以下示例:
a---b---HEAD
 \
  \
   '---d

将a和b压缩成一个名为“ab”的新提交将导致两个不同的树,这在大多数情况下是不可取的,因为git-merge和git-rebase将无法在这两个分支之间工作。
ab---HEAD

a---d

如果你真的想要这样做,是可以实现的。看一下git-filter-branch,它是一个强大(但危险)的历史重写工具。

好的观点。+1。我想你需要从AB分支,并将A---D变基到该分支中,以便从新的共同点AB重放A-D。然后删除AD分支,在那一点上没有用了。 - VonC

3
你可以使用git filter-branch来实现。例如:
git filter-branch --parent-filter \
'if test $GIT_COMMIT != <sha1ofB>; then cat; fi'

这导致AB-C放弃了A的提交记录。

这对我没有起作用。git filter-branch 说分支没有改变。 - Leo
@Leo:你有没有用实际的哈希ID替换<sha1ofB>? - Uwe Kleine-König
1
这个需要更多的点赞!因为我的Git历史比较复杂,所以变基对我没用,但是这个方法可行。 - hansmosh

-1

在提交到远程之前,您可以使用交互式变基来修改最后两个提交

git rebase HEAD^^ -i

3
没错,但是kch问的是要压缩前两个提交(first two commits),而不是最近的两个提交(last two commits)。 - joshdoe
就像这样,非常简单,您只需使用要合并的提交哈希而不是HEAD^^即可。 - Sebastian Blask
2
@SebastianBlask,我不相信这是那么简单的。如果您使用第一个提交的SHA1,则只会从第二个提交开始。不幸的是,无法压缩/修复该提交。 - Drew Noakes

-6

有一个更简单的方法来实现这个。假设你在master分支上:

创建一个新的孤立分支,它会删除所有提交历史记录:

$ git checkout --orphan new_branch

添加您的初始提交信息:

$ git commit -a

摆脱旧的未合并主分支:

$ git branch -D master

将您当前的分支new_branch重命名为master

$ git branch -m master

6
你意思是,然后你会失去整个提交历史记录吗? - kch

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