Git 合并提交:附带示例的指南

“及早提交,经常提交”是使用 Git 进行软件开发时的一个流行口号。这样做可以确保每次更改都有良好的记录,增强协作,并且更容易追踪项目的发展。然而,这也可能导致提交数量过多。

这就是合并提交的重要性所在。合并提交是将多个提交条目合并为一个单一、连贯的提交的过程。

例如,假设我们正在开发一个实现登录表单的功能,并且创建了以下四个提交:

一旦功能完成,对于整个项目来说,这些提交记录太过详细。我们未来不需要知道在开发过程中遇到了并修复了一个错误。为了确保主分支的历史记录清晰,我们将这些提交合并为一个单独的提交:

如何在 Git 中合并提交:交互式变基

合并提交最常见的方法是使用交互式变基。我们使用以下命令来启动它:

git rebase -i HEAD~<number_of_commits>

<提交数> 替换为我们想要合并的提交数。

在我们的例子中,我们有四个提交,所以命令是:

git rebase -i HEAD~4

执行这个命令将会打开一个交互式命令行编辑器:

上半部分显示提交记录,而下半部分包含关于如何合并提交的评论。

我们看到四个提交。对于每一个,我们必须决定执行哪个命令。我们关注的是pickp)和squashs)命令。要将这四个提交合并为一个提交,我们可以选择第一个,然后将剩下的三个合并。

我们通过修改每个提交前面的文本来应用命令,具体是将第二个、第三个和第四个提交的pick改为ssquash。为了进行这些编辑,我们需要在命令行文本编辑器中进入“插入”模式,通过按键盘上的i键:

按下i后,屏幕底部会出现-- INSERT --字样,表示我们已经进入了插入模式。现在,我们可以使用箭头键移动光标,删除字符,以及像在标准文本编辑器中一样进行输入:

一旦我们对更改满意,我们需要通过按下键盘上的Esc键退出插入模式。下一步是保存我们的更改并退出编辑器。为此,我们首先按下:键,向编辑器发出我们打算执行命令的信号:

现在编辑器底部会出现一个分号:,提示我们输入命令。要保存更改,我们使用命令w,代表”write”(写入)。要关闭编辑器,使用q,代表”quit”(退出)。这两个命令可以组合在一起输入wq

要执行命令,我们按下Enter键。这个操作将关闭当前的编辑器并打开一个新的编辑器,允许我们输入新压缩提交的提交信息。编辑器会显示一个默认信息,包括我们从四个被压缩的提交中提取的信息:

我建议修改消息以准确反映这些合并提交所实施的变化——毕竟,合并的目的就是保持清晰且易于阅读的历史记录。

要与编辑器交互并编辑消息,我们再次按i进入编辑模式,然后按我们的喜好编辑消息。

在这种情况下,我们将提交消息替换为“实现登录表单”。要退出编辑模式,我们按Esc。然后通过按:,输入wq命令并按Enter来保存更改。

如何查看提交历史

通常,回忆整个提交历史可能会有挑战。要查看提交历史,我们可以使用git log命令。在提到的例子中,在进行合并之前执行git log命令将会显示:

要浏览提交记录列表,请使用上下箭头键。要退出,请按q

我们可以使用git log来确认压缩操作是否成功。在压缩操作后执行它将显示一个带有新消息的单个提交:

推送压缩后的提交

上述命令将对本地仓库进行操作。要更新远程仓库,我们需要推送我们的更改。但是,因为我们更改了提交历史,所以我们需要使用--force选项来进行强制推送:

git push --force origin feature/login-form

强制推送将覆盖远程分支上的提交历史,并可能破坏正在该分支上工作的其他人的工作。在实际操作前与团队沟通是一个好习惯。

一种更安全的强制推送方式,可以减少打乱协作者的风险,是使用--force-with-lease选项:

git push --force-with-lease origin feature/login-form

这个选项确保只有在远程分支自我们最后一次获取或拉取后没有更新时,我们才进行强制推送。

合并特定的提交

假设我们有五个提交:

假设我们想要保留第1、2和5个提交,并将第3和4个提交合并。

使用交互式变基时,标记为合并的提交将与直接前一个提交合并。在这个案例中,意味着我们想要合并Commit4,使其合并到Commit3中。

为了做到这一点,我们必须启动一个交互式变基操作,包括这两个提交。在这种情况下,三个提交就足够了,所以我们使用以下命令:

git rebase -i HEAD~3

然后,我们将Commit4设置为s,以便它与Commit3合并:

执行这个命令并列出提交后,我们观察到提交3和4已经被合并在一起,而其余的保持不变。

从特定的提交开始合并

在命令git rebase -i HEAD~3中,HEAD部分是最近提交的简写。而~3语法用于指定一个提交的祖先。例如,HEAD~1指的是HEAD提交的父提交。

在交互式变基中,考虑的提交包括指定命令中提交的所有祖先提交。注意,指定的提交不包括在内:

我们可以直接指定一个提交哈希,而不是使用HEAD。例如,Commit2的哈希值为dbf3cc118d6d7c08ef9c4a326b26dbb1e3fe9ddf,所以命令:

git rebase -i dbf3cc118d6d7c08ef9c4a326b26dbb1e3fe9ddf

将开始一个变基,考虑所有在Commit2之后的提交。因此,如果我们想从特定的提交开始变基并包含该提交,我们可以使用命令:

git rebase -i <commit-hash>~1

解决合并提交时的冲突

当我们合并提交时,会将多个提交的更改合并为一个提交,如果更改有重叠或者差异很大,可能会导致冲突。以下是一些可能产生冲突的常见场景:

  1. 重叠更改:如果两个或更多被压缩的提交修改了文件中的相同行或紧密相关的行,Git可能无法自动调和这些更改。
  2. 更改状态不同:如果一个提交添加了某段代码,而另一个提交修改或删除了相同的代码,压缩这些提交可能会导致需要解决的冲突。
  3. 重命名和修改:如果一个提交重命名了一个文件,而后续的提交对旧名称进行了更改,压缩这些提交可能会使Git感到困惑,导致冲突。
  4. 二进制文件的更改:二进制文件使用基于文本的diff工具合并效果不佳。如果多个提交更改了同一个二进制文件,我们尝试将它们压缩时,可能会发生冲突,因为Git无法自动协调这些更改。
  5. 复杂的提交历史:如果提交历史复杂,其中包含多次合并、分支或变基,压缩它们可能会导致冲突,因为更改具有非线性特性。

在压缩时,Git会尝试逐一应用每个更改。如果在过程中遇到冲突,它会暂停并允许我们解决这些冲突。

冲突将被标记为冲突标记<<<<<<>>>>>>。为了处理冲突,我们需要打开文件并手动解决每一个冲突,通过选择我们想要保留的代码部分。 

解决冲突后,我们需要使用git add命令将解决后的文件暂存。然后我们可以使用以下命令继续变基:

git rebase --continue

关于Git冲突的更多信息,查看这个关于如何解决Git合并冲突的教程如何解决Git中的合并冲突

变基时替代压缩的方法

使用git merge --squash命令是合并多个提交为一个提交的另一种方法,它是git rebase -i的替代方案。当我们希望将一个分支的更改合并到主分支,并将所有的单独提交压缩为一个时,这个命令特别有用。以下是如何使用git merge进行压缩的概述:

  1. 我们导航到要合并更改的目标分支。
  2. 我们执行命令git merge --squash <分支名>,将<分支名>替换为分支的名称。
  3. 我们使用git commit提交更改,以创建一个表示特性分支所有更改的单个提交。

例如,假设我们要将分支 feature/login-form 的更改作为一个单独的提交合并到 main 中:

git checkout main git merge --squash feature-branch git commit -m "Implement login form"

与使用 git rebase -i 相比,这种方法有以下限制:

  • 控制粒度: 对单个提交的控制较少。使用 rebase 时,我们可以选择要合并哪些提交,而合并操作则强制将所有更改合并为一个提交。
  • 中间历史:使用合并时,特性分支中的个人提交历史在主分支中丢失。这可能会使得跟踪特性开发期间所做的逐步更改变得更加困难。
  • 预提交审查:由于它将所有更改作为一个变更集进行暂存,我们在压缩之前无法单独审查或测试每个提交,这与交互式重置基础不同,在交互式重置基础中可以依次审查和测试每个提交。

结论

将频繁且小的提交纳入开发工作流程可以促进协作和清晰的文档记录,但也可能使项目历史变得杂乱。压缩提交可以达到平衡,保留重要的里程碑,同时消除小幅度迭代更改的噪音。

了解更多关于 Git 的信息,我推荐以下资源:

Source:
https://www.datacamp.com/tutorial/git-squash-commits