title: Git实战: git工作流程

description: git协同开发

mathjax: true

tags:

- git

categories:

- git

date: 2025-05-17 00:42:00

updated: 2025-05-17 00:42:00

Git实战: git工作流程

大家好!今天我们来聊聊 Git。很多刚接触 Git 或者团队协作不久的开发者,对于如何正确、高效地使用 Git 分支进行开发可能会有些困惑。最近就有朋友问到:

“我不是很会用 Git,我想问我开发的时候是不是 clone 下来代码之后在 develop 分支上新建一个分支,比方说是 feature-auth,然后我开发完以后我是不是在本地把 feature-auth 分支上的内容 Merge 到 develop 分支,然后把 develop 分支推到 GitHub 上去?”

这是一个非常好的问题,也代表了很多开发者的实际操作场景。答案是:是的,这个思路是正确的,并且是 Git 中一种非常常见且推荐的工作模式,我们通常称之为“功能分支工作流”(Feature Branch Workflow)。

下面我们就来详细拆解一下这个流程,并补充一些最佳实践。

功能分支工作流:一步步解析

假设我们的目标是开发一个新的用户认证功能(feature-auth)。

1. 克隆代码库

首先,你需要将远程的代码库克隆到你的本地机器。

1
git clone <repository-url>

2. 切换到主开发分支(通常是 develop

大多数项目会有一个主要的开发分支,比如 develop 或者 main。确保你基于这个最新的分支来创建你的功能分支。

1
git checkout develop

3. 拉取最新代码(重要!)

在创建新分支之前,务必确保你的本地 develop 分支是最新的,同步远程仓库的最新变更。

1
git pull origin develop

4. 创建并切换到你的功能分支

现在,从最新的 develop 分支创建你的功能分支 feature-auth-b 参数会同时创建并切换到新分支。

1
git checkout -b feature-auth

5. 在功能分支上安心开发

所有的代码编写、修改、调试都在 feature-auth 分支上进行。你可以随时提交你的更改。

1
2
3
4
5
6
# ...编写代码...
git add .
git commit -m "feat: 实现用户登录基础功能"
# ...继续编写代码...
git add .
git commit -m "feat: 增加JWT令牌生成"

6. 开发完成,准备合并回 develop

当你的功能开发测试完毕,就可以准备将其合并回 develop 分支了。这里有两种主要的方式:

方式一:本地合并后推送 (如提问者所述)

这是提问者描述的流程,完全可行。

  • a. (推荐) 先将 develop 的最新更改同步到你的功能分支:
    为了避免在合并到 develop 时产生过多冲突,或者确保你的功能是基于最新的 develop 代码,可以先将 develop 的最新代码合并到你的 feature-auth 分支。

    1
    2
    3
    4
    5
    git checkout develop      # 切换回 develop 分支
    git pull origin develop # 拉取 develop 最新代码
    git checkout feature-auth # 切换回你的功能分支
    git merge develop # 将 develop 合并到 feature-auth
    # 如果有冲突,此时在 feature-auth 分支解决

    或者,有些团队更喜欢使用 rebase 来保持提交历史的线性:

    1
    2
    3
    # ... 前两步同上 ...
    git checkout feature-auth
    git rebase develop

    注意:如果你的 feature-auth 分支已经推送到远程,rebase 后需要强制推送 (git push -f),请谨慎使用并确保团队成员了解其影响。

git mergegit rebase 的详细区别和适用场景:

我们先来详细聊聊 git mergegit rebase 的区别与适用场景,力求通俗易懂。

想象一下,你和你的同事都在基于项目的同一个版本(比如 develop 分支的某个提交点)开始各自开发新功能。你开发了 feature-A,同事开发了 feature-B

现在,你们都完成了各自的功能,需要将这些代码合并回 develop 分支。

git merge:忠实记录,汇聚合流

git merge 的核心思想是:将两个分支的最新快照(以及从共同祖先开始的历程)整合到一起,并创建一个新的“合并提交”(merge commit)来记录这次整合。

它是如何工作的?

假设 develop 分支在你开始开发 feature-A 后,也有了新的提交。

1
2
3
      A---B---C feature-A
/
D---E---F---G develop

当你执行 git checkout develop 然后 git merge feature-A 时:

  1. Git 会找到 feature-Adevelop 分支的共同祖先(上图中的 E)。
  2. 它会把 feature-A 分支上的所有更改(A, B, C 相对于 E 的更改)和 develop 分支自共同祖先以来的更改(F, G 相对于 E 的更改)合并起来。
  3. 如果顺利,它会创建一个新的提交 H(合并提交)在 develop 分支上。这个提交 H 会有两个父提交:G (来自 develop) 和 C (来自 feature-A)。

历史记录会变成这样:

1
2
3
      A---B---C feature-A
/ \
D---E---F---G---H develop

特点:

  • 保留历史: 它完整地保留了分支的原始提交历史。你可以清晰地看到 feature-A 是如何被合并进来的,以及它在合并前的所有提交。
  • 非线性历史: 合并提交会引入额外的提交点,使得提交历史图看起来像一个分叉然后汇合的图形,不是一条直线。
  • 简单直接: 对于理解和操作来说,merge 相对简单。

适用场景:

  1. 合并公共分支: 当你将一个功能分支合并回主要的共享分支(如 develop, main, master)时,使用 merge 是非常常见的。它清晰地记录了功能的集成点。
  2. 保留分支的上下文: 如果你希望明确地看到一个功能分支的完整开发脉络以及它何时被合并,merge 能够提供这种信息。
  3. 团队协作: 当多个人在同一个分支上工作并需要合并彼此的工作时(虽然通常推荐在各自的特性分支上工作),merge 是标准做法。

通俗比喻:

想象两条河流(分支)各自流淌,当它们需要汇合时,git merge 就像是在汇合点建造了一个新的交汇处(合并提交),两条河的水都流向了这个交汇处,并且我们能清楚地看到是哪两条河汇入的。

git rebase:重新上演,线性历史

git rebase 的核心思想是:将你的分支上的提交“重新播放”或“重新应用”到另一个分支的顶端。 它会修改你的提交历史,使其看起来像是在目标分支的最新提交之后直接进行的开发。

它是如何工作的?

还是上面的例子:

1
2
3
      A---B---C feature-A
/
D---E---F---G develop

当你检出 feature-A 分支 (git checkout feature-A) 然后执行 git rebase develop 时:

  1. Git 会找到 feature-Adevelop 分支的共同祖先(E)。
  2. 它会“暂存”feature-A 分支上从共同祖先之后的所有提交(A, B, C)。
  3. 它会将 feature-A 分支的指针移动到 develop 分支的最新提交(G)。
  4. 然后,它会把之前暂存的提交(A, B, C)逐个重新应用到 develop 分支的顶端,生成新的提交 A', B', C'。这些新的提交虽然内容和 A, B, C 一样,但是它们的 SHA-1 哈希值是不同的,因为它们的父提交变了。

历史记录会变成这样:

1
2
3
              A'--B'--C' feature-A
/
D---E---F---G develop

现在,feature-A 分支看起来就像是直接从 develop 分支的最新状态(G)开始开发的。

之后,如果你想把 feature-A 合并到 develop,通常会切换到 develop 分支,然后执行 git merge feature-A。由于 feature-A 的基底已经是 develop 的最新提交,这次合并会是一个“快进合并”(Fast-forward merge),不会产生新的合并提交,develop 分支指针直接指向 C'

1
D---E---F---G---A'--B'--C' develop, feature-A

特点:

  • 线性历史: rebase 的主要目的是创建一条干净、线性的提交历史。项目历史看起来像是一条直线,更容易阅读和理解。
  • 修改历史: rebase 会重写提交历史。原始的提交(A, B, C)实际上被废弃了(或者说变成了悬空提交,最终会被垃圾回收),取而代之的是新的提交(A', B', C')。
  • 潜在风险: 永远不要对已经推送到公共仓库并被其他人使用的分支执行 rebase 因为它改变了提交历史,如果其他人基于旧的历史进行开发,再次同步时会导致非常混乱的局面和大量的冲突。

适用场景:

  1. 保持本地功能分支更新:

    在你的本地功能分支上开发时,定期rebase到目标分支(如develop)的最新版本,可以帮助你及早发现和解决冲突,并保持你的功能分支与主线同步。

    1
    2
    3
    git checkout feature-A
    git fetch origin develop # 获取远程 develop 的最新代码,但不合并
    git rebase origin/develop # 将 feature-A 的提交 rebase 到最新的 origin/develop 之上
  2. 清理本地提交历史: 在将本地功能分支推送到远程或创建 Pull Request 之前,可以使用交互式 rebase (git rebase -i) 来整理、合并、编辑你的提交,使提交历史更清晰、更有意义。

  3. 个人分支或短期分支: 当你独自在一个分支上工作,并且希望在合并前拥有一个干净的历史记录时。

通俗比喻:

想象你在写一本书的某个章节(你的功能分支),而主编辑也在同时修订书的其他部分(develop 分支)。当你写完一部分,发现编辑已经更新了书的前面章节。

  • git merge 就像是你把你的章节直接加到书的末尾,并加个批注说“这是基于旧版前言写的章节,现在和新版前言一起放进来了”。
  • git rebase 就像是你拿起编辑修订后的最新版本,然后把你的章节内容,在新的基础上重新誊写一遍,让它看起来就像是你从一开始就是基于最新版本写的。

总结:merge vs rebase

特性 git merge git rebase
目标 合并分支,保留历史 创建线性历史,整合变更
历史记录 非线性(产生合并提交),保留分支的确切历史 线性(重写提交),使历史更简洁
提交ID 保留原始提交ID,新增一个合并提交ID 原始提交ID被改变,生成新的提交ID
冲突解决 在合并提交时一次性解决所有冲突 在每个被重新应用的提交上都可能需要解决冲突
安全性 对公共分支安全 不要在公共/共享分支上使用,除非你非常清楚后果
简单性 概念和操作相对简单 概念稍复杂,操作需要更小心

何时选择?

  • 当你希望保留分支的精确历史记录,并且不介意历史图谱中出现合并提交时,使用 git merge 这是将功能分支合并到 developmain 分支的默认和安全方式。
  • 当你希望在将本地更改推送到远程或合并到主分支之前,清理你的提交历史,使其变得线性且易于阅读时,可以在你的本地(私有)分支上使用 git rebase
  • 在将你的功能分支与主开发分支(如 develop)同步时:
    • 如果你想把 develop 的最新更改集成到你的功能分支,git rebase develop (在你的功能分支上执行) 是一个好选择,它能让你的功能分支保持在 develop 的最前端,减少最终合并回 develop 时的复杂性。
    • git merge develop (在你的功能分支上执行) 也可以,但会在你的功能分支历史上创建一个合并提交。

许多团队会结合使用两者:在本地特性分支上使用 rebase 来保持更新和清理提交,然后使用 merge (通常是通过 Pull Request) 将特性分支合并到主开发分支,并保留一个合并提交来标记功能的集成。

记住黄金法则:只对尚未推送到公共仓库的本地分支执行 rebase

如果你看完还是似懂非懂那我就再举一个例子:

假设我们的主开发分支是 develop

场景: 你需要开发一个新功能 feature-logging

详细步骤和代码示例:

Phase 1: 开始新功能开发,并保持与 develop 同步 (使用 rebase)

  1. 确保你的 develop 分支是最新的:

    1
    2
    3
    4
    5
    6
    # 切换到 develop 分支
    git checkout develop

    # 从远程仓库拉取最新的 develop 分支代码
    # (假设你的远程仓库名为 origin)
    git pull origin develop

    这能确保你基于最新的代码创建功能分支。

  2. 创建你的功能分支 feature-logging

    1
    2
    # 从当前的 develop 分支创建并切换到新的 feature-logging 分支
    git checkout -b feature-logging

    现在你位于 feature-logging 分支,可以开始编码了。

  3. 进行一些开发提交:
    假设你添加了日志模块并做了一些修改。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # (你在这里编写代码,比如创建 logging_service.py)
    echo "Initial logging setup" > logging_service.py
    git add logging_service.py
    git commit -m "feat: Add initial logging service"

    # (继续编写代码,比如更新 main.py 以使用日志服务)
    echo "import logging_service" > main.py
    git add main.py
    git commit -m "feat: Integrate logging in main module"

    你的 feature-logging 分支现在有了一些你自己的提交。

  4. 与此同时,develop 分支可能有了新的提交:
    可能你的同事合并了其他功能或修复了 bug 到 develop 分支。
    为了模拟这个,我们假设 develop 分支有了一个新提交。
    (在真实场景中,这会通过其他人推送到远程 develop 分支发生)

  5. 在将你的功能推送到远程或准备合并之前,用 rebase 更新你的 feature-logging 分支:
    这是关键步骤,目的是将你的本地提交“叠加”在最新的 develop 分支之上,保持线性历史并及早处理冲突。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # 1. 确保你的本地 develop 分支副本是最新的
    git checkout develop
    git pull origin develop # 获取 develop 的最新更改

    # 2. 切换回你的功能分支
    git checkout feature-logging

    # 3. 将 feature-logging rebase 到最新的 develop 分支上
    # 这会把你 feature-logging 分支上独有的提交 (上面两个 "feat: ..." 提交)
    # 重新在最新的 develop 分支的顶端播放一遍
    git rebase develop
    • 发生了什么?
      • Git 会找到 feature-loggingdevelop 的共同祖先。
      • 它会“暂存”你在 feature-logging 上独有的提交(“Add initial logging service”, “Integrate logging in main module”)。
      • 然后,它将 feature-logging 分支指向 develop 分支的最新提交。
      • 最后,它将你暂存的提交逐个应用到新的基点上。你的提交哈希值会改变,因为它们的父提交变了。
    • 冲突处理: 如果在 rebase 过程中发生冲突 (比如 develop 上的新更改和你本地的更改修改了同一行代码),rebase 会暂停,让你解决冲突。解决后,使用 git add <conflicted-file> 然后 git rebase --continue。如果想中止 rebase,可以使用 git rebase --abort
    • 黄金法则体现: 此时,feature-logging 分支只存在于你的本地仓库。你还没有把它推送到远程共享给其他人。所以,即使提交历史被“重写”了(提交哈希变了),也不会影响到其他人。这是安全的。
  6. (可选) 清理你的本地提交历史 (交互式 Rebase):
    rebasedevelop 之后,或者在你觉得需要整理提交时,可以使用交互式 rebase 来合并、修改、排序你的本地提交,让它们更清晰。

    1
    2
    # 假设你想整理 feature-logging 上的最近2个提交
    git rebase -i HEAD~2

    这会打开一个编辑器,让你选择如何处理这些提交 (比如 pick, squash, reword, edit 等)。
    例如,你可以将两个小的提交 squash (压缩) 成一个更有意义的提交。
    再次强调: 这也是在本地分支上进行的操作,推送前进行。

Phase 2: 功能完成,准备合并到 develop (通常通过 Pull Request 和 merge)

  1. 推送你的功能分支到远程仓库:
    当你对 feature-logging 分支满意,并且它已经基于最新的 develop (通过 rebase) 并且提交历史也干净了,就可以推送它了。

    1
    2
    # 第一次推送本地的 feature-logging 分支到远程 origin
    git push -u origin feature-logging

    如果之前已经推送过,并且因为 rebase 修改了历史,你可能需要强制推送 (git push --force-with-lease origin feature-logging)。但要非常小心,只有当你确定这个分支没有其他人正在使用时才能这样做。 如果是通过 Pull Request 工作流,通常在 PR 创建后,后续的 rebasepush 会更新 PR。

  2. 创建 Pull Request (PR) / Merge Request (MR):
    在 GitHub, GitLab, Bitbucket 等平台上,你会从 feature-logging 分支向 develop 分支创建一个 Pull Request。
    你的团队成员会审查你的代码。

  3. 合并 Pull Request (通常使用 merge 策略):
    一旦 PR 被批准,通常会通过平台的 “Merge” 按钮来合并。大多数平台默认或推荐使用合并提交 (merge commit) 的方式来合并。

    如果是在本地手动模拟这个合并(不推荐用于团队共享的 develop 分支,通常由 PR 系统处理):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # 1. 切换到目标分支 develop
    git checkout develop

    # 2. 确保 develop 是最新的
    git pull origin develop

    # 3. 合并 feature-logging 分支到 develop
    # 使用 --no-ff 确保即使可以快进合并,也创建一个合并提交
    # 这保留了 feature-logging 分支存在的明确记录
    git merge --no-ff feature-logging

    这时,develop 分支的历史图会显示一个合并提交,清楚地表明 feature-logging 在何时被集成进来。由于 feature-logging 之前已经 rebase 到了 develop 的最新版本,所以它的提交会整齐地排列在 develop 的历史之上,然后通过一个合并提交连接起来。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    # develop 分支历史可能看起来像这样 (简化版):
    # ... --- OldCommit1 --- OldCommit2 (develop 之前的状态)
    # \
    # --- FeatureCommitA' --- FeatureCommitB' (来自 rebase 后的 feature-logging) --- MergeCommit (合并 feature-logging 到 develop)
    #
    # 或者更准确地说,因为 rebase 使得 feature-logging 的基底是 develop 的最新提交:
    #
    # D1---D2---D3 (develop)
    # \
    # F1'---F2' (feature-logging, rebased onto D3)
    #
    # 当合并时,使用 --no-ff:
    #
    # D1---D2---D3------------------M (develop after merge)
    # \ /
    # F1'---F2'------ (feature-logging)
    #
    # M 是合并提交。F1', F2' 是 rebase 后的 feature-logging 的提交。
  4. 推送合并后的 develop 分支:

    1
    git push origin develop
  5. 删除已被合并的本地和远程功能分支 (可选):

    1
    2
    git branch -d feature-logging      # 删除本地分支
    git push origin --delete feature-logging # 删除远程分支

为什么这个流程好?

  • rebase 的好处:
    • 线性历史:feature-logging 被合并到 develop 之前,它的历史是相对于 develop 的线性历史,易于理解。
    • 及早发现冲突: 定期将 develop 的更改 rebase 到你的功能分支,可以让你在本地、小范围内解决冲突,而不是等到最后合并时面对一个巨大的冲突。
    • 干净的提交: 交互式 rebase 允许你整理提交,使它们更有意义。
  • merge (带 --no-ff 或通过 PR) 的好处:
    • 保留上下文: 合并提交清晰地标记了一个功能分支的完成和集成点。你可以从 develop 的历史中轻易看出某个功能是什么时候合并进来的。
    • 不改写公共历史: develop 分支的历史不会被改写,这对于团队协作至关重要。

总结黄金法则的体现:

  • git rebase feature-logging develop (或者 git rebase developfeature-logging 分支上时): 这是在 feature-logging 分支上操作的,并且是在你推送这个分支供他人审查/使用之前。这是安全的,因为它只修改了你本地的、尚未公开的提交历史。
  • 一旦 feature-logging 被推送到远程并可能被其他人拉取,或者一旦基于它创建了 Pull Request,就应该避免对这个已公开的分支再执行 rebase (除非你和团队有明确的约定,并且知道如何处理强制推送带来的影响)。如果需要集成 develop 的新更改,此时更安全的做法是在 feature-logging 分支上 git merge develop,或者等待 PR 合并。

这个流程结合了两者的优点,使得单个功能的开发历史保持整洁,同时主分支的集成点清晰可追溯。

  • b. 切换到 develop 分支并合并:

    1
    2
    3
    git checkout develop
    git merge feature-auth # 将 feature-auth 分支合并到 develop
    # 如果步骤 a 未做或 develop 在此期间又有更新,此处可能产生冲突,需解决
  • c. 推送 develop 分支到远程:

    1
    git push origin develop
  • d. (可选) 删除功能分支:
    合并完成后,如果不再需要,可以删除本地和远程的功能分支。

    1
    2
    git branch -d feature-auth          # 删除本地分支
    git push origin --delete feature-auth # 删除远程分支

方式二:通过 Pull Request (PR) / Merge Request (MR) 合并 (更推荐)

这是目前团队协作中更为主流和推荐的方式,尤其是在使用 GitHub, GitLab, Bitbucket 等平台时。

  • a. 将你的功能分支推送到远程仓库:

    1
    git push origin feature-auth
  • b. 在 Git 平台创建 Pull Request:
    打开你的 GitHub (或类似平台) 仓库页面,你会看到一个提示,可以从 feature-auth 分支创建一个到 develop 分支的 Pull Request (PR)。

  • c. 代码审查与讨论:
    PR 提供了一个非常好的代码审查(Code Review)机会。团队成员可以查看你的代码变更,提出修改建议,进行讨论。自动化检查(如单元测试、代码风格检查)通常也会在此时运行。

  • d. 合并 PR:
    一旦 PR 通过审查并得到批准,通常由拥有权限的成员(或者你自己)通过平台的界面点击“Merge Pull Request”按钮。平台会自动将 feature-auth 的代码合并到 develop 分支。

  • e. 更新本地 develop 分支:
    PR 合并后,远程的 develop 分支已经更新了。你需要将这些变更同步到你的本地 develop 分支。

    1
    2
    git checkout develop
    git pull origin develop
  • f. (可选) 删除功能分支: 同方式一。

为什么推荐 Pull Request 工作流?

虽然本地合并简单直接,但 Pull Request 工作流带来了诸多好处:

  • 代码审查: 这是最重要的优点之一。其他开发者可以审查你的代码,帮助发现潜在问题、提出改进建议,从而提高代码质量。
  • 讨论与协作: PR 提供了一个围绕代码变更进行讨论的集中场所。
  • 自动化集成: 可以集成 CI/CD 工具,在合并前自动运行测试、检查代码风格等。
  • 更清晰的合并历史: 许多平台在合并 PR 时会创建一个合并提交,使得追踪功能何时被并入主线更加清晰。
  • 权限控制: 可以设置分支保护规则,例如要求 PR 必须通过审查才能合并。

还有一个问题: 为什么会产生冲突以及如何优雅地解决 Git 合并冲突?

为什么会产生冲突?

Git 合并冲突发生在 Git 无法自动决定如何合并两个分支上的更改时。当两个不同的分支修改了同一个文件的同一部分,或者一个分支删除了一个文件而另一个分支修改了它时,Git 就会感到困惑。

主要原因包括:

  1. 并发修改同一行/区域:
    • 场景: 你在你的 feature-A 分支修改了 config.txt 的第 5 行。与此同时,你的同事在 feature-B 分支也修改了 config.txt 的第 5 行。
    • 冲突: 当试图将 feature-Afeature-B 合并到 develop 分支(或者一个合并到另一个)时,Git 不知道应该保留哪个版本的第 5 行,或者如何组合它们。
  2. 一个分支修改文件,另一个分支删除同一文件:
    • 场景: 你在 feature-cleanup 分支删除了一个不再需要的旧文件 old_utils.py。但你的同事在 feature-add-doc 分支为 old_utils.py 添加了文档。
    • 冲突: 当合并时,Git 不确定是应该保留被修改的文件(因为它有新内容),还是应该执行删除操作。
  3. 合并不同历史的分支时,对同一文件有不同的演变:
    • 这种情况更复杂,但本质上还是归结于 Git 在某个文件的特定部分看到了来自不同源头的、无法自动调和的更改。

如何优雅地解决 Git 合并冲突?

“优雅地”解决冲突意味着:

  • 准确性: 确保最终合并的代码是正确的,并且包含了所有预期的更改。
  • 清晰性: 解决冲突的过程和结果应该是易于理解的。
  • 最小化干扰: 尽快解决冲突,避免阻塞其他人的工作。
  • 沟通: 如果冲突涉及他人的代码,与他们沟通以确保解决方案是双方都同意的。

以下是解决冲突的步骤和一些技巧:

步骤:

  1. 开始合并操作:
    假设你当前在 main 分支,想要合并 feature 分支:

    1
    2
    3
    git checkout main
    git pull origin main # 确保 main 是最新的
    git merge feature

    如果出现冲突,Git 会在终端提示,并且 git status 会显示哪些文件处于冲突状态。

  2. 识别冲突文件:
    运行 git status。它会列出 “Unmerged paths”(未合并的路径),这些就是包含冲突的文件。

    1
    2
    3
    Unmerged paths:
    (use "git add <file>..." to mark resolution)
    both modified: config.txt
  3. 打开冲突文件并解决冲突:
    Git 会在冲突的文件中用特殊的标记来标示冲突区域:

    1
    2
    3
    4
    5
    6
    7
    8
    <<<<<<< HEAD (Current Change)
    # 这是你当前分支 (例如 main) 的内容
    port = 8080
    =======
    # 这是你正在合并的分支 (例如 feature) 的内容
    port = 9000
    host = "localhost"
    >>>>>>> feature (Incoming Change)
    • <<<<<<< HEAD:表示冲突区域的开始,后面是当前分支 (HEAD 指向的分支) 的内容。
    • =======:分隔了两个分支的冲突内容。
    • >>>>>>> branch-name:表示冲突区域的结束,后面是被合并分支 (例如 feature) 的内容。

    你的任务是:

    • 仔细阅读冲突标记之间的代码。
    • 决定你想要保留哪个版本,或者如何将两个版本结合起来。
    • 手动编辑文件,删除 <<<<<<< HEAD=======>>>>>>> branch-name 这些标记,并保留或修改代码,使其成为你期望的最终版本。

    例如,如果你决定保留 feature 分支的端口号,并也想保留 host 设置,你可以修改成:

    1
    2
    port = 9000
    host = "localhost"

    或者,如果你想保留 main 的端口并添加 host

    1
    2
    port = 8080
    host = "localhost"

    关键是你要做出决定并清理掉 Git 的标记。

  4. 使用可视化合并工具 (可选但推荐):
    许多 IDE (如 Visual Studio Code) 和专门的 Git GUI 客户端 (如 GitKraken, Sourcetree) 提供了强大的三向合并 (three-way merge) 工具。这些工具通常会并排显示你的版本、对方的版本以及共同祖先的版本,并提供一个结果窗格让你更方便地选择和组合更改。

    在 VS Code 中,当检测到冲突时,它通常会在文件名旁边显示标记,并且在编辑器中提供 “Accept Current Change”, “Accept Incoming Change”, “Accept Both Changes”, “Compare Changes” 等便捷操作。

  5. 标记冲突已解决:
    在你手动编辑并保存了所有冲突文件后,你需要告诉 Git 你已经解决了这些冲突:

    1
    2
    3
    git add <resolved-file-1> <resolved-file-2> ...
    # 或者,如果你解决了所有冲突文件
    git add .

    git status 现在应该显示这些文件为 “Changes to be committed”。

  6. 完成合并:
    一旦所有冲突都已解决并暂存 (staged),就可以提交合并了:

    1
    git commit

    Git 通常会自动为你生成一个合并提交信息,例如 “Merge branch ‘feature’ into main”。你可以修改它,但通常默认的就足够了。
    注意: 如果你是在 rebase 过程中遇到冲突,解决后使用 git rebase --continue 而不是 git commit

  7. 测试!
    合并后,务必运行测试(单元测试、集成测试等)以确保你的更改没有破坏任何东西,并且合并后的代码按预期工作。

优雅解决冲突的技巧:

  • 经常拉取和变基/合并:

    • 如果你在特性分支上工作,定期从主开发分支(如 developmainpull (如果主分支是共享的,通常是 pull --rebase 到你的特性分支,或者 merge 主分支到你的特性分支) 可以帮助你及早发现并解决小冲突,而不是等到最后面对一个巨大的冲突。
  • 小而专注的提交和分支:

    • 较小的、逻辑上独立的提交和分支通常更容易合并,因为它们涉及的更改范围较小。
  • 沟通,沟通,再沟通:

    • 如果你看到一个冲突,并且不确定如何正确解决它,特别是当它涉及到其他人的代码时,立即与相关同事沟通。他们最了解自己的更改意图。一起查看冲突可以更快、更准确地解决问题。
  • 理解更改的意图:

    • 不要仅仅机械地选择“我的”或“他们的”代码。尝试理解每个更改试图实现什么。有时,正确的解决方案是两者的结合,或者是一个全新的方案。
  • 使用 git log --merge -p <file>

    • 在合并冲突后,但在解决之前,这个命令可以显示导致冲突的具体提交以及它们对文件的更改。这有助于理解冲突的来源。
    • git diff <file> 也可以显示冲突标记。
  • 不要害怕 git merge --abort

    • 如果你在解决冲突的过程中感到困惑或搞砸了,只要你还没有

      git commit(或git rebase --continue),你通常可以安全地中止合并:

      git merge --abort

      这会将你的分支恢复到合并之前的状态,你可以重新尝试。对于rebase,使用git rebase --abort。

  • 利用 IDE 的合并工具:

    • VS Code 等现代 IDE 内置了非常好的冲突解决界面,可以让你逐个处理冲突点,选择接受当前更改、传入更改或两者都接受,甚至手动编辑结果。这通常比直接在文本编辑器中处理原始冲突标记更直观。

示例:

假设 main 分支的 app.py

1
2
3
4
5
# main 分支
def start_server():
print("Starting server on port 8000")

start_server()

feature 分支的 app.py (基于 main 的某个旧版本创建并修改):

1
2
3
4
5
6
7
8
9
# feature 分支
def start_server(debug_mode=False):
port = 8080
if debug_mode:
print(f"Starting server in DEBUG mode on port {port}")
else:
print(f"Starting server on port {port}")

start_server(debug_mode=True)

当你尝试 git merge feature (在 main 分支上):

  1. Git 会报告 app.py 冲突。

  2. app.py
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23



    内容会变成:

    ```python
    <<<<<<< HEAD
    # main 分支
    def start_server():
    print("Starting server on port 8000")

    start_server()
    =======
    # feature 分支
    def start_server(debug_mode=False):
    port = 8080
    if debug_mode:
    print(f"Starting server in DEBUG mode on port {port}")
    else:
    print(f"Starting server on port {port}")

    start_server(debug_mode=True)
    >>>>>>> feature
  3. 解决:

    你需要决定最终的start_server函数和调用。也许你想要feature分支的函数定义,但保留main分支的简单调用,或者更新调用以使用新参数。

    假设你决定采用 feature分支的函数,并让它默认以非调试模式启动:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 解决后的 app.py
    def start_server(debug_mode=False):
    port = 8080 # 或者你决定用 8000,或者从配置读取
    if debug_mode:
    print(f"Starting server in DEBUG mode on port {port}")
    else:
    print(f"Starting server on port {port}")

    start_server() # 决定默认不以 debug 模式启动
  4. 然后 git add app.pygit commit

通过遵循这些步骤和技巧,你可以更自信和有效地处理 Git 合并冲突。

总结

所以,最初提问的 Git 操作流程是完全正确的,并且是功能分支开发的基础。在此基础上,引入 Pull Request 的概念会让团队协作更加规范和高效。

选择哪种方式取决于你的团队规模、项目复杂度和协作习惯。但无论如何,清晰的分支策略是高效 Git 使用的关键。

希望这篇博客对你有所帮助!