1. 先从分支和提交信息开始把团队仓库的“规矩”立起来团队协作这件事我最早是在一个模拟项目里吃苦头吃出来的。当时五六个人同时改同一个仓库分支名字五花八门有人叫fix有人叫dev还有人直接叫test2。提交信息更是离谱一水儿的update、modify、commit。到了周五合并谁也不知道哪个分支对应哪个需求只能挨个翻代码去猜。后来我硬性定了一套分支命名和提交规范一个星期之后整个仓库的混乱程度肉眼可见地下降。1.1 分支命名规则一眼看出“这是谁、在干什么、对应什么需求”分支名的核心作用不是“好看”而是让人在不用点开代码的情况下就能判断这条分支的意图和风险等级。我目前用的规则是类型/需求号-简短描述类型限定四到五种就够feature、fix、refactor、docs、chore。举例来说git checkout -b feature/PAY-2218-order-export git checkout -b fix/AUTH-1042-login-timeout git checkout -b refactor/ORDER-0910-split-service好处很明显GitHub/GitLab 的列表页按类型前缀做视觉分组feature和fix一眼分开带上需求号之后任何人看到分支都能去项目管理系统里查上下文。还有一个容易被忽略的好处后续做自动生成 changelog、按分支类型跑不同流水线规则都是现成的。如果你的团队还没到“每个分支对应一个需求”的粒度至少先做到“一个分支只干一件事”。我最常看到的问题就是一条分支既改 bug 又加功能还顺带改了配置文件评审的人根本无从下手。分支越“纯”后续的 revert、cherry-pick、code review 就越轻松。1.2 提交信息与提交粒度小步提交一句“为什么”胜过十句“做了什么”提交信息我强烈建议统一成这种结构type(scope): 简短描述 详细说明解释为什么这么做第一行不超过 72 个字符type 和分支类型保持一致。关键是后面的正文别粘一段 changelog要写变更动机。比如fix(auth): 修复登录超时后无提示的问题 原因是 token 过期后前端没有监听 401 响应 用户停留在当前页面以为系统卡死了。 现在统一在响应拦截器里处理超时跳转。这样写三个月后有人翻git log能直接理解当时的决策背景而不是只能看到“改了什么文件”。提交粒度上我用一个简单标准一次提交能回滚且回滚时不带入无关改动。说人话就是不要攒一天的工作量一次性提交。每完成一个完整的逻辑单元比如一个函数的重构、一个接口的对接就提交一次。按功能拆分提交还有一个好处git blame的时候定位代码的引入原因会快很多因为 diff 范围小。如果你刚接手一个年代久远、提交信息一团乱的分支不要急着重写历史。先跟团队确认这个分支是不是只有你自己在动再考虑rebase -i否则可能把别人的提交也卷进去。2. 合并策略不吵架merge、rebase、squash 各自该用在什么场景关于合并方式团队里几乎每个月都要争论一轮。争论的本质不是“谁对”而是把不同的合并工具用错了地方。我的态度很明确merge、rebase、squash 都是工具关键是搞清楚它们分别擅长什么、代价是什么。2.1 merge —— 保留真实的历史脉络适合“多人协作的功能分支”git merge会生成一个额外的 merge commit把两条分支的祖先历史原样保留。这个方式最贴近“事情真实发生的过程”——某个功能是并行开发后合进来的历史里就有这个分叉和汇合点。但很多人忽略了merge的默认行为。直接从主干执行git merge feature/xxx如果当前分支能快进fast-forwardGit 默认不会生成 merge commit而是直接把指针往前挪。这样“这条分支是独立开发的”这条信息就丢了历史变成一条直线看起来是顺序开发实际是并行开发后续做版本回溯容易产生误导。所以我要求团队在合并功能分支时统一加--no-ffgit checkout main git pull --ff-only origin main git merge --no-ff feature/PAY-2218-order-export--no-ff强制生成 merge commit功能分支的完整历史被保留在一条独立的弧线里。代价是历史会出现多一些的“菱形”结构但如果团队习惯用git log --graph查看这种结构恰恰最直观。2.2 rebase —— 让个人分支的“骨架”保持干净但要遵守铁律rebase的本质是“把一条分支的起点换到另一个位置”然后逐个重放提交。优点是提交历史近乎线性review、二分定位都会更舒服。但我对 rebase 有一个铁律只能对“自己的分支、自己的提交”使用且该分支未推送到共享远端。原因很简单rebase 会重写提交哈希。假设你已经把分支推到远端别人也拉下来基于它开发你这边一 rebase两边就直接分道扬镳之后的合并不是一般的痛苦。实操中我常用的动作是# 先把主干更新到最新 git fetch origin git checkout main git pull --ff-only origin main # 回自己的分支把 main 的改动垫到自己提交下面 git checkout feature/PAY-2218-order-export git rebase origin/mainrebase 过程中遇到冲突按顺序逐个改就好。改完后不要用git commit要用git add 解决好的文件 git rebase --continue如果中途发现思路不对想退出git rebase --abort这条命令会恢复到 rebase 之前的完整状态不用怕搞坏。2.3 squash merge —— 把整个分支压成一条提交适合“合并回主干”squash merge 的价值是把一条分支上几十个琐碎的中间提交压缩成一个有意义的提交再合入主干。它适合 feature 分支、hotfix 分支这种“一个主题只留一个结果”的场景。git checkout main git pull --ff-only origin main git merge --squash feature/PAY-2218-order-export git commit -m feat(pay): 新增订单导出功能这里的技巧是--squash只把改动内容放到暂存区不自动创建 merge commit提交信息由你自己写。这样主干历史无比干净每个功能一个提交每个提交都能独立回滚git log --oneline扫一眼就能看懂。三种策略各有代价我用一张表来收口团队认知策略历史形态适用场景主要代价merge --no-ff保留分叉汇合多人并行的大型功能分支历史图较复杂rebase线性重放个人未推送的分支同步主干改写提交哈希误用风险高squash merge一键收敛成单提交功能已完成合回主干中间过程丢失不适合需要细粒度回溯的长期分支团队内部一定要把“什么分支用哪种合并方式”写进 README并且把规则说明确到具体命令。省得每次合并前都要开个会讨论“这次该用哪个”。3. 冲突解决的实战顺序从“怕冲突”到“会拆冲突”冲突是 Git 协作绕不开的坎。很多人一看到CONFLICT就慌其实冲突的本质很简单两个分支对同一段内容做了不同修改Git 不知道听谁的。解决冲突的目标也不是“消除冲突”而是“做一次正确的合并决策”。3.1 降低冲突频率的三个习惯比“会解冲突”更重要冲突不是技术问题更多是协作节奏问题。降低冲突频率我先说三个最有效的手段第一PR 越小越好。一个几百行改动的 PR跟主干重叠的概率肯定高于一个几十行改动的 PR。把大功能拆成多个可独立评审的小 PR冲突天然变少即使有冲突范围也小。第二及时把主干拉入自己的分支。不要等分支开发完了再一次性同步主干那样等于把自己跟主干之间的差距拉得巨大冲突必然爆发。我个人的节奏是分支存在超过两天每天上班后先git fetch origin git rebase origin/main或git merge origin/main一次。第三改动前先看是否有其他人正在动同一块代码。团队小的时候直接在群里问一句“这个模块有人改吗”比什么都管用。3.2 冲突发生时按“三看”顺序处理真正出现冲突我不建议一上来就打开文件乱改。我会按“三看”走先看冲突文件清单。git status会把 unmerged 的文件列表列出来先判断冲突涉及几个文件大致评估工作量。再看冲突结构。打开文件会看到类似这样的标记 HEAD 当前分支的代码 另一分支的代码 feature/xxx冲突标记本身不会骗人。先看清楚两边各写了什么我经常会发现很多冲突只是同一处逻辑的两种等价写法删掉一边就完事。最后看改动意图。如果是复杂冲突光看代码片段不够需要结合提交信息判断两边改动的目的。这就要用到上面提到的提交规范写得越清楚冲突解决越容易。如果冲突双方确实都改了同一处逻辑我会拉上相关人当面或语音对齐确认保留哪边的内容或者重新整理。3.3 三个常用的辅助命令git log --merge列出当前冲突双方各自的最新提交帮你理解两边分别做了什么。git diff --cc查看合并后的冲突文件的详细差异。git mergetool唤起图形化三路合并工具比如 Beyond Compare、Kdiff3可视化对比 base、local、remote 三个版本。一个重要提醒解决完冲突后一定要跑一次编译和测试不要只靠肉眼判断“看起来没问题”。合并冲突是逻辑错误的高发区特别是两边都做了结构调整时代码可能编译通过但运行行为已经变了。我见过不少案例都是在 merge 里把某个重复定义漏删导致线上问题。4. 不小心搞坏分支reflog 与远端保护是最后的船票团队协作中“翻车”是常态区别只在于有人知道怎么爬起来有人只能靠重新 clone。这一节讲三个保命技巧。4.1 reflog比 log 更真实的操作记录git log只看得到提交历史的“结果”git reflog记录的是 HEAD 每一次移动的“操作流水”。哪怕你 reset 掉了提交、rebase 了分支、误删了分支reflog 里都能找到当时的提交哈希。举例假设有人误执行了git reset --hard HEAD~5之后发现丢了一堆代码别慌先看操作记录git reflog输出里能看到类似HEAD{0}: reset: moving to HEAD~5再往上是之前的commit操作。找到 reset 之前的哈希直接给它建个新分支git checkout -b recover-branch 丢失的commit哈希这样就找回了一个小时前的完整状态。关键是发现误操作后马上停止其他操作先看 reflog防止后续操作把那条 reflog 记录覆盖掉。即使 push 到了远端reflog对本地操作也永远有效。唯一需要担心的是太久远的记录被 Git gc 清理掉所以不要拖个把月后再来救。4.2 stash临时切换场景的“桌面整理术”当手头改到一半突然需要切到其他分支处理紧急需求git stash就是保命工具git stash push -m 订单导出功能改造中 # 切换分支、做别的 git checkout fix/AUTH-1042-login-timeout # 干完回来 git checkout feature/PAY-2218-order-export git stash pop有个小技巧容易被忽略stash 也可以按“只弹某个特定改动”来处理。比如你 stash 了三次想找回其中一次先看列表git stash list然后只应用某一条而不删除它git stash apply stash{1}注意stash pop在遇到冲突时不会自动丢 stash它会保留内容让你先解决冲突。很多人以为 pop 失败 stash 就没了其实不是除非你手动git stash dropstash 会一直在。4.3 远端保护与强制推送的正确姿势团队环境里main分支必须开保护规则——禁止直接 push只允许通过 merge request / pull request 合入。这能避免大量低级事故。但总有人要紧急修一个已经推送的分支需要强制推送。很多人只会用git push -f这很容易误伤如果在你 push -f 之前别人已经推送了新提交你的强制推送会直接抹掉别人的提交。正确的做法是--force-with-leasegit push --force-with-lease origin fix/AUTH-1042-login-timeout这个参数的意思是只有当远端分支的当前状态和你本地看到的状态一致时才允许覆盖如果远端有新提交直接拒绝推送。相当于给强制推送加了一个“安全带”。我统一要求团队的强制推送只能用这个参数宁可多 push 一次也不允许用裸-f。5. 用 Git Hooks 和自动检查把“低级错误”挡在提交之前规范这东西靠自觉大多数时候靠不住。要让规范真正落地得靠自动化。这里说的是代码入库前的最后一道闸Git Hooks 和配套的检查工具。5.1 pre-commit hooks提交前先跑一次格式化与静态检查Git Hooks 是 Git 内置的事件回调机制其中pre-commit在每次git commit之前触发。你可以在.git/hooks/pre-commit里写 shell 脚本但更好的做法是用工具来管理 hooks团队统一安装。以 Python 项目为例我常用的组合是pre-commit框架配置文件.pre-commit-config.yaml大致长这样repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml - repo: https://github.com/psf/black rev: 23.3.0 hooks: - id: black团队每个人克隆仓库后执行一次pre-commit install从此每次 commit 前工具会自动帮你清理行尾空格、检查语法、格式化代码。不合格的提交会被拦截在本地不会污染仓库历史。5.2 commitlint让提交信息按规范“强制”生成commit message 规范也可以强制。我用的方式是在仓库里配置 commitlint配合 husky前端场景或者 pre-commit其他语言场景。校验规则大致是type 必须在允许列表里subject 不能为空正文不能以大写开头或反之按团队习惯定义。只要设置了规则不合规的提交信息在 commit 时会直接被拒绝逼着大家把提交信息写规范。这里有一个团队落地时的实际教训直接把 hook 装进镜像、强制所有成员执行效果并不好。很多老成员会觉得“又慢又烦”甚至绕过 hook。我的做法是先在新成员身上启用再从老成员里找一两个意见领袖推动跑两周后所有人都习惯了再全员强制开启。5.3 服务端检查最后的防线本地 hooks 可以被绕过所以远端也要设防线。在代码托管平台上可以配置提交前检查比如限制提交信息格式、禁止提交包含敏感信息的文件、要求 PR 必须通过 CI 才能合入。别把这些检查设成“建议”一律设成“强制”。现在的托管平台普遍支持在合并请求上配置必须关联需求/工单编号必须至少一个评审人 approve必须通过 CI 管道必须进行合并前自动测试这几条结合起来团队里的低级问题会被压缩到很低的概率。我在实际团队中的体感是hooks 加 CI 之后“提交信息不规范”“代码未格式化”“测试不过就合入”这三类问题几乎绝迹。6. 代码评审环节的 Git 细节fetch、PR 迭代与历史整理代码评审是团队质量的重要抓手但这里的 Git 细节很多人反而忽略了。这一节聊评审当天和评审过程中最实用的 Git 习惯。6.1 永远不要在“过时的本地分支”上评审评审前第一件事永远是更新本地分支。我会用git fetch而不是直接git pull。区别在于fetch只更新远端跟踪分支不会自动动本地工作区更安全先看看远端到底发生了什么再决定怎么合并。评审别人代码时最稳的做法是拉一个干净的评审分支git fetch origin git checkout -b review-feature-PAY-2218 origin/feature/PAY-2218-order-export这样你在评审分支上看代码、试运行都不会污染自己的开发分支。评审完直接删掉这个分支就行。如果只是看 diff不切分支也行git diff origin/main...origin/feature/PAY-2218-order-export这里用三个点...表示“从 main 和 feature 的最近共同祖先到 feature 顶部的差异”正好对应这个功能分支自己产生的改动不会把 main 上别人的改动也带进来。6.2 PR 迭代过程中如何整理历史很多团队的 PR 流程是评审人提出意见开发者不断追加新的提交最后 PR 里堆了十几个fix review comments之类的提交。这种做法不违法但它会影响后续的历史可读性。更好的做法是在评审人二次确认之前尽量把新增的修正提交 squash 进原始提交。方案有两种看场景如果分支还没推送到远端直接用git rebase -i origin/main在交互式 rebase 里把新增的修正squash或fixup进原始提交。这里的-i界面里squash会把提交合并并保留提交信息供你编辑fixup则直接保留原提交的信息不弹出编辑窗口适合纯改代码不改提交描述的场景。如果分支已经推送到远端也不要用裸-f用第一节提到的--force-with-lease保护性推送git push --force-with-lease origin feature/PAY-2218-order-export评审人拉取更新后看到的是一个干净清晰的提交序列而不是一堆“垃圾提交”。6.3 容易被忽略的三个评审小命令git show commit查看某个提交的完整改动评审时比看 UI 上的 diff 更灵活。git log --oneline --graph --all快速理解整个仓库的分支拓扑评审前先看一眼避免在错误的时间点基于错误的分批判断。git blame file定位某行代码的来源评审时看到“看不懂的代码”可以立刻查明是谁、为什么引入的。这三个命令看起来基础但大量评审流于形式的原因就是只盯着 UI 看 diff没去理解“这段代码在什么上下文里长出来的”。最后分享一点我自己的体会这几个技巧看似零散但核心只有一条Git 协作的终极目标不是“会用命令”而是“让仓库历史成为团队交流的工具”。我踩过最深的坑就是重命令、轻规范——命令再华丽分支乱、提交乱、历史乱团队照样天天吵架。如果你现在还在小团队里我建议从第一条分支命名和提交规范开始不需要一步到位把十招全用了。跑通两周之后再逐步引入合并策略、hooks、CI 门槛让规则跟着团队的成长节奏走。最重要的还是那句话规范要写出来、要自动化、要在服务端强制不能只靠口头约定。等这些习惯都长在身上你回头看那些撕扯不清的分支图会轻松很多。