在推送之前将多个提交合并为一个

178

这个问题不仅涉及如何完成此任务,还涉及使用Git是好还是坏的最佳实践。

考虑到本地上我在 main 分支上进行大部分工作,但我创建了一个主题分支,我将其称为 feature_branch。 在处理 feature_branch 并在两个分支之间切换进行其他工作的过程中,结果发现我在 feature_branch 上做了多个提交,但在每个提交之间,我都没有 push。我的问题是:

  1. 您认为这是一种不好的惯例吗?在每个推送中每个分支保持一个提交是否更明智?什么情况下有多个提交在一个分支上再执行推送是有益的?

  2. 我该如何将 feature_branch 上的多个提交合并到 main 分支以进行推送?是否可以不必担心它,只需进行包含多个提交的推送,或者将提交合并成一个再进行推送会更好?如果需要进行合并,应该如何操作?


https://dev59.com/Imw05IYBdhLWcg3w72Q0#6934882 - Eric
7个回答

171
对于你的第一个问题,没有任何问题同时推送多个提交。很多时候,您可能希望将工作分解为几个小的、逻辑上的提交,但只有在整个系列准备好后才将它们推上去。或者当您处于离线状态时,您可能会进行多个本地提交,并在重新连接后一次性将它们全部推送。没有理由限制您每次推送一个提交。
通常我认为最好保持每个提交是一个单一的、逻辑上的、连贯的更改,其中包含使其工作所需的所有内容(因此,它不会使您的代码处于破损状态)。如果您有两个提交,但如果您只应用第一个提交,则它们将导致代码破损,那么将第二个提交合并到第一个提交中可能是一个好主意。但如果您有两个提交,每个提交都做出了合理的更改,将它们分别推送是可以的。
如果您确实想将几个提交合并在一起,您可以使用git rebase -i。如果您在分支topical_xFeature上,则可以运行git rebase -i master。这将打开一个编辑器窗口,其中列出了一堆提交,前缀为pick。您可以将除第一个以外的所有提交更改为squash,这将告诉Git保留所有这些更改,但将它们压缩到第一个提交中。完成后,检出master并合并您的功能分支:
git checkout topical_xFeature
git rebase -i master
git checkout master
git merge topical_xFeature

或者,如果您只想将topical_xFeature中的所有内容压缩到master中,可以执行以下操作:

git checkout master
git merge --squash topical_xFeature
git commit

哪一个选择由你决定。通常情况下,我不会担心有多个较小的提交,但有时候你不想麻烦地进行额外的小提交,所以你只需将它们压缩成一个即可。


2
在我使用--squash合并后,我无法通过git branch -d topic删除主题分支。为什么Git无法识别所有更改已经合并? - balki
12
因为 Git 是根据提交记录出现在给定分支的历史记录中来检测补丁是否已合并,所以将提交压缩成一个新的提交会改变它们的状态。虽然这个新的提交做了和其他提交一样的事情,但 Git 并不能识别它们是相同的,除非它们具有相同的提交 ID(SHA-1)。因此,一旦你压缩提交后,就需要使用 git branch -D topic 命令强制删除旧的分支。 - Brian Campbell

85

以下是我通常在推送代码之前将多个提交合并成单个提交的方法。

为了实现这一点,我建议您使用GIT提供的'squash'概念。

按照以下步骤进行。

1) git rebase -i master (您也可以使用特定的提交代替master)

打开交互式编辑器,在其中显示所有提交。基本上,您需要识别要合并为单个提交的提交。

假设这些是您的提交,并在编辑器中显示如下。

pick f7f3f6d changed my name a bit    
pick 310154e updated README formatting and added blame   
pick a5f4a0d added cat-file  

需要注意的是,这些提交记录按照与使用log命令通常看到的顺序相反的顺序列出。也就是说,较旧的提交将首先显示。

2)将最后提交的更改中的“pick”更改为“squash”,类似于下面所示。这样做的话,您最近的两个提交记录将合并为第一个提交记录。

pick f7f3f6d changed my name a bit         
squash 310154e updated README formatting and added blame   
squash a5f4a0d added cat-file

如果您有很多提交需要合并,也可以使用简短的形式:

p f7f3f6d changed my name a bit         
s 310154e updated README formatting and added blame   
s a5f4a0d added cat-file

为进行编辑,请使用“i”键,它将启用插入编辑器。请记住,最顶部(最旧的)提交无法压缩,因为没有先前的提交可以合并。因此必须选择或使用“p”来挑选。使用“Esc”退出插入模式。

3)现在使用以下命令:wq保存编辑器。

当您保存时,您将获得一个单一提交,该提交引入了所有三个先前提交的更改。

希望这会对您有所帮助。


12
或许这对其他人来说很显而易见,但是当你说“git rebase -i”时,你还需要指定你从哪个提交开始。当我尝试按照这个例子操作的时候,我并没有意识到这一点。所以,在这个例子中,应该这样写:“git rebase -i xxxxx”,其中xxxxx是在时间上紧接在f7f3f6d之前的提交。一旦我弄清楚了这一点,一切都和上面描述的一样顺利。 - nukeguy
很有趣,@nukeguy,我没有遇到任何问题,不需要指定特定的提交。它只是默认使用已经存在的内容。 - JCrooks
1
也许像@nukeguy一样,git rebase -i HEAD~2对我来说是一个有用的起点。然后这个答案很有帮助。接着,我的git status显示“您的分支和'origin/feature/xyz'已经分叉,各自有1个和1个不同的提交。”所以我需要使用git push origin feature/xyz --force-with-lease。请参见https://dev59.com/DVIH5IYBdhLWcg3wgvGx#59309553和https://www.freecodecamp.org/forum/t/how-to-squash-multiple-commits-into-one-with-git-squash/13231。 - Ryan
如果有大量的 pick 需要替换为 squash,该怎么办? - Eduardo Pignatelli

18
  1. 首先选择您想要一切在其之后的提交。

git reflog
5976f2b HEAD@{0}: commit: Fix conflicts
80e85a1 HEAD@{1}: commit: Add feature
b860ddb HEAD@{2}: commit: Add something
  • 将代码库重置到你选择的提交(我选择了HEAD@{2}})

  • git reset b860ddb --soft
    
  • git status(只是为了确定)

  • 添加您的新提交

  • git commit -m "Add new commit"
    
    注意:HEAD@{0}HEAD@{1}现在合并为一个提交,多次提交也可以这样做。再次使用git reflog应该会显示:
    git reflog
    5976f2b HEAD@{0}: commit: Add new commit
    b860ddb HEAD@{1}: commit: Add something
    

    17

    切换到 main 分支并确保您已经更新。

    git checkout main
    

    git fetch 这可能是必要的(取决于您的 git 配置),以接收来自 origin/main 的更新。

    git pull
    

    将功能分支合并到主分支。
    git merge feature_branch
    

    将主分支重置为原始状态。

    git reset origin/main
    

    Git现在将所有更改视为未暂存的更改。 我们可以将这些更改添加为一个提交。 添加.也会添加未跟踪的文件。

    git add --all
    
    git commit
    

    参考:https://makandracards.com/makandra/527-squash-several-git-commits-into-a-single-commit


    3
    这个答案易于理解且非常直观易懂。 - jokab
    这是针对特定用例的一个非常好的技巧!与调整基础不同,这更直接且更少出错。 - Shinebayar G

    12

    首先,没有要求你每个分支每次推送只能有一个提交:推送是一种发布机制,允许你将本地历史记录(即一系列提交)发布到远程仓库。

    其次git merge --no-ff topical_xFeature 将在将主题工作推送到 master 之前,将其记录为单个提交。
    这样,你就可以保留 topical_xFeature 以进行进一步的演进,并在下一次使用 --no-ff 合并后将其记录为单个新提交放入 master 中。
    如果要摆脱 topical_xFeature,则 git merge --squash 是正确的选项,详见 Brian Campbell回答


    我认为--squash而不是--no-ff是你想要的。--no-ff会创建一个合并提交,但也会保留所有来自topical_xFeature的提交。 - Brian Campbell

    1
    其他答案可能是正确的。然而这里有一个简单的解决方案,我已经多次使用并且效果良好。 情境:我们在功能分支中创建了多个提交(可能是一些试错)。现在,在向主分支提交最终的拉取/合并请求之前,我们想将所有提交合并为单个提交,并在最新的主分支上更新和有用的提交消息之上。
    步骤:1
    go to the feature branch where you want to perform this action ( Better to make backup branch before proceed )
    

    步骤:2

    git reset --soft origin/main
    

    步骤三(可选,仅用于显示状态)

    git status
    

    步骤:4

    git commit -m "New updated and Final commit msg"
    

    1

    自动化多次提交为一次的工具

    正如Kondal Kolipaka所说,使用“git rebase -i”命令。

    “git rebase”的逻辑

    在使用“git rebase -i”命令时,git会在当前的.git/rebase-merge目录中生成git-rebase-todo文件,并调用git编辑器让用户编辑git-rebase-todo文件进行处理。因此,该工具需要满足以下要求:

    1. 将默认的git编辑器修改为我们提供的工具;
    2. 工具处理git-rebase-todo文件。

    修改默认的git编辑器

    git config core.editor #show current default git editor
    git config --local --replace-all  core.editor NEW_EDITOR # set the local branch using NEW_EDITOR as git editor
    

    因此,该工具需要更改git编辑器并处理git-rebase-todo文件。 该工具使用以下Python:

    #!/usr/bin/env python3
    #encoding: UTF-8
    
    import os
    import sys
    
    def change_editor(current_file):
        os.system("git config --local --replace-all  core.editor " + current_file) # Set current_file as git editor
        os.system("git rebase -i") # execute the "git rebase -i" and will invoke the python file later with git-rebase-todo file as argument
        os.system("git config --local --replace-all core.editor vim") # after work reset the git editor to default
    
    def rebase_commits(todo_file):
        with open(todo_file, "r+") as f:
            contents = f.read() # read git-rebase-todo's content
            contents = contents.split("\n")
            first_commit = True
            f.truncate()
            f.seek(0)
            for content in contents:
                if content.startswith("pick"):
                    if first_commit:
                        first_commit = False
                    else:
                        content = content.replace("pick", "squash") # replace the pick to squash except for the first pick
                f.write(content + "\n")
    
    def main(args):
        if len(args) == 2:
            rebase_commits(args[1]) # process the git-rebase-todo
        else:
            change_editor(os.path.abspath(args[0])) # set git editor
    
    if __name__ == "__main__":
        main(sys.argv)
    

    参考:https://liwugang.github.io/2019/12/30/git_commits_cn.html


    6
    请减少网站推广。另请参阅如何避免成为垃圾邮件发送者 - tripleee

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