← 返回模块
3.6.2.2beta 可读 · 未来付费校验通过内容版本 2026-05-24

协作、拉取请求与历史卫生

3.6.2 · Git 与代码质量 · 编程

周二上午十一点。你的分支 feature/risk-factor-z 在本地终于跑通——沪深300 因子归因回测 Sharpe 三位小数都对上了审查者期望。你 git push,在内网 GitLab 开了 MR,一小时内审查者要求改两处,并指出你的分支已落后 main 四个提交,因为同事刚合了另一个修同模块的 MR。第 1 课教你在一台机器上操作 git;本课教一队人怎么一起送代码上线、互相不踩坏对方的工作。

主干开发:一条共享分支 + 多条短命特性分支

A-股 量化团队 绝大多数采用​​主干开发 + 短命特性分支​​(trunk-based development with short-lived feature branches)。main 永远是绿的——main 上每个提交都过 CI、随时可发布。特性工作在分支上,生命周期以小时到几天计、而非周。分支就绪后开 PR(GitLab 叫 MR;Gerrit 叫 change),至少一名审查者读 diff、要么批准要么提改,CI 必为绿,PR 合入 main

作为对照:GitFlow 长生命周期 develop + release 分支是为按季度发盒装软件设计的。量化迭代远比这快,长分支只会堆合并债。GitFlow 适合有长 release 分支的项目;买方节奏下少合适。立场说一遍,翻篇。

PR 工作流:从头到尾走一遍

工作样例:feat(risk): 添加 沪深300 因子-z 列至业绩归因。走这条七步流程:

git switch main
git pull --rebase
git switch -c feature/risk-factor-z
# 编辑、本地测试、提交 2-3 次
git push -u origin feature/risk-factor-z
# 在平台 UI 开 PR
# 推送更多提交以回应审查意见
# 通过审查 + CI 绿之后,在 UI 上 squash-merge

第 1–2 步:从最新 main 起步(带 rebase 的 pull 保持线性)。第 3 步:开分支。第 4 步:开发——预期 2-5 次提交,小、原子、可描述。第 5 步:推送并设上游跟踪。第 6 步:在 https://gitlab.firm.local/research/research-alpha/-/merge_requests/new?merge_request[source_branch]=feature/risk-factor-z 开 MR,描述讲清​​为什么​​:解决了什么、考虑过什么备选、怎么测、还有什么风险。一行「加了 risk_z.py」浪费审查者时间;六行带 bullet 才换来认真审查。第 7 步:同分支再推提交回应审查——每次 push 原地刷新 MR。批准后选策略:​​merge-commit​ 保全部提交;​​squash-merge​ 压成 main 上一个提交,是 快节奏团队 最常见的默认;​​rebase-merge​ 线性重放、无合并提交。大多数 内部 GitLab 团队 默认 Squash and merge、并在分支保护中关闭 Allow force push

代码审查纪律:五条清单

以下五条让一个代码审查文化能跑起来;说一次,方便学习者一遇到就认得:

  1. 审查 diff,不审查作者——评论代码本身,不评论人。
  2. 建议,不命令——「考虑用 X」胜于「就用 X」。
  3. 小 PR 落地快——300 行的 PR 会被认真读,3000 行的 PR 要么被橡皮图章一过、要么被一拖再拖。
  4. LGTM 不能替代真正读过 diff——如果你没时间认真审就直说,让别人接手。
  5. 客气一点——目标是把代码送上线,不是辩论赛的胜负。

按这五条来的资深审查者,一周能落地的 PR 更多,还顺带把团队里每个年轻人都训练成能写「易审 PR」的人。不按这五条来的审查者,会把每一个 PR 变成一场吵架。

冲突解决:从头到尾

git mergegit rebase 遇到两条分支改同一行的时候会停下来。出现冲突的文件里会出现三段以标记符(marker)分隔的区域:

<<<<<<< HEAD
    risk_z = winsorize(risk_z, 0.01, 0.99)
=======
    risk_z = factor_z
>>>>>>> feature/risk-factor-z

HEAD 这一侧是你当前分支的版本;feature/risk-factor-z 这一侧是要并入分支的版本。处理:在编辑器里打开文件,决定合并结果​​应该​​是什么(很多时候既不是这边逐字、也不是那边逐字,而是两边合在一起、或者其中一边胜出、或者干脆重写一段),把三行标记一并删掉,保存,git add <file>,然后 git commit(对应 git merge)或 git rebase --continue(对应 git rebase)。

退出口:git merge --abortgit rebase --abort 把工作区退回到冲突发生前的状态——当冲突大到当下解决不来、你想退一步去问原作者意图再重来时,就用它。git mergetool 打开 IDE 集成的可视化三方合并界面;硬冲突时好用,一行级别的小冲突用编辑器更快。

历史卫生:git rebase -i

交互式变基(interactive rebase)打开一个编辑器,每行一个提交,前面带一个操作关键字。常用五个:pick(保持原样)、squash(与上一条提交合并、两条信息都留下)、fixup(与上一条提交合并、丢弃这条的信息——给 fix typo 这种用)、reword(保留提交但改信息)、drop(删除该提交)。走一遍清理 + 救命的命令:

git rebase -i HEAD~5
# 编辑器打开;把两个 'fix typo' 提交标记为 fixup
git log --oneline -5
git push --force-with-lease origin feature/risk-factor-z
git reflog
git reset --hard <sha-from-reflog>

五个提交的分支里有两个是 fix typoaddress review comment 的清理提交,做完 fixup 之后变成干净的三个提交。然后用 --force-with-lease 推送——永远不要裸 --force。这条不可破的纪律,加粗念一遍:​​只变基你还没有共享出去的提交;永远不要变基别人已经拉取过的分支历史​​。变基会改写提交的 SHA;同事 clone 出去的副本会因此与共享历史分叉,然后整个周三上午你要挨个帮他们救场。

语义化提交:审查者一眼能扫的格式

Conventional Commits 约定为 <type>(<scope>): <subject><type> 取自 featfixchorerefactordocstestperf。在 A-股 研究仓库 里能见到的三个例子:

  • feat(risk): 添加 沪深300 因子-z 列至业绩归因
  • fix(ingest): 处理 涨跌停 当日 空 tick 文件
  • chore(deps): bump pandas 到 2.2.1

不是强制的,但用的团队多到这个格式值得记住;很多自动生成 changelog 的工具就按这个形状来解析。挑一组 <type> 在团队 README 里贴一张速查表,审查环节顺手提醒,一周之内 新成员 就会按这个格式写。

git revert vs git reset --hard,以及 --force-with-lease

这两条命令外观相似、行为完全不同。git revert <sha> 创建一个​​新提交​​,diff 是 <sha> 的反向——公开、可追溯、安全推到共享分支——是撤销已合入提交的正确工具。git reset --hard <sha>​HEAD 倒带​​到 <sha>,从那以来的所有提交与未提交的修改都被丢掉——本地、迅速、有破坏性——只在被丢掉的提交「不存在于别人机器上」时才能用。

团队红线:​​绝不 git push --forcemain 或 别人的分支​​。在你自己一个人用的特性分支上,git push --force-with-lease 是更安全的选项——只要远端在你上次 fetch 之后变了,它就拒绝推送,你不会踩到没看见的工作。多数团队会把 main 的分支保护配成机械上拒绝 force-push,但开发者端的纪律同样还是这一条。

救命:git reflog

每一次 HEAD 移动——commit、reset、checkout、rebase、merge、switch——都被记在 reflog 里,保留大约 90 天。灾难模板:你 git reset --hard HEAD~3 想丢三个提交,结果意识到要保留。跑 git reflog、找到 reset 之前那一刻 HEAD 对应的 SHA、git reset --hard <sha> 回去——或者更保守地 git switch -c recovery <sha> 在丢失的位置上建一条 recovery 分支,不打扰当前分支。把这个模板记进肌肉记忆里;你真正需要它的那一天,会很庆幸自己会用。

Exercise

Exercise

你在 research-alpha 的克隆里继续做 feature/factor-z。(a) main 上同事刚刚把一个 PR 合了进去,恰好改了 src/factors/risk_z.py 里你这条分支也要改的那一行。拉 main,把你的分支 rebase 到它之上;解决冲突的方式是把两边的修改合并(你这边加一个 winsorize 步骤,对方那边把 factor_z 改名成 risk_z);git add 然后 git rebase --continue。(b) 你的分支现在有 5 个提交,其中第 2 和第 4 是 fix typo。跑 git rebase -i HEAD~5,把第 2 和第 4 标成 fixup,保存,确认 git log --oneline -10 在 rebase 目标之上恰好显示 3 个提交。(c) 用 git push --force-with-lease origin feature/factor-z 把变基后的分支推上去;用一句话解释为什么这里裸 --force(没有 -with-lease)是危险的。(d)(出于练习目的)假设你刚才 fixup 错了一条提交;跑 git reflog,找到 rebase 开始之前那一刻的 SHA,用 git switch -c recovery <sha> 在 rebase 前的状态上建一条 recovery 分支。

提示
切到 feature/factor-z 之后,用 git fetch origin maingit rebase origin/main。冲突停下时,marker 里 HEAD 这一侧是你 rebase 正在重放的提交;把两边意图合并成一行,保存、git addgit rebase --continue
提示
(d) 里,git reflog 列出每一次 HEAD 移动并附一句话;你要的 SHA 是 reflog 里写「checkout: moving from feature/factor-z」那一条,或者 rebase -i 刚开始那条之前的一条。git switch -c recovery <sha>git reset --hard 更安全,因为它不动当前分支。

下一课

团队这层——PR、审查纪律、冲突、交互式变基、revert、reflog——把人这边的协作管住。下一课接上​​机械​​这层:把风格之争消化掉的格式化器(formatter)、在审查之前抓未使用 import 的 静态检查(linter)、提交时就抓 None 解引用的 类型检查(type checker),以及把这些都串在每一次 git commit 上的 pre-commit 框架。最终的小抄是三份配置文件——pyproject.toml.pre-commit-config.yaml.github/workflows/ci.yml——一起接到你一直在用的 research-alpha 仓库上。

阅读清单

  • 《Pro Git》第二版 中文版(git-scm.com/book/zh/v2),第 3 章(Git 分支)与 第 7.6 节(重写历史)。
  • GitLab 中文 文档 关于 Merge Request、Code Review 与 分支保护 的章节。
  • 廖雪峰 Git 教程 中「多人协作」「冲突解决」「变基」三节。
  • Conventional Commits 1.0.0 规范(官方 中文 翻译)。
  • 一篇 在 A-股 量化社区 被广泛 转载 的「git reflog 救命」短文。

一条额外注释:内部 Gerrit 工作流(patchset、Change-Id、+2 审批)在大型私募与券商自营越来越常见,命令层面与 GitHub PR / GitLab MR 一致。一句话带去未来每一个仓库:​​只变基未共享的提交;永远不要变基他人已拉取的历史​​。

速查卡

本课反复出现的展示形态——抄进自己的笔记:

  • Fenced ```bash 块——七步 PR 工作流;六行交互式变基 + 救命序列。
  • Fenced ```text 块——带三个 marker(<<<<<<< HEAD=======>>>>>>> feature/risk-factor-z)的冲突文件。
  • Inline 代码 Conventional Commits 例子(feat(risk): ...fix(ingest): ...chore(deps): bump pandas 到 2.2.1)。
  • Inline 五条审查纪律清单,其中第 4 条带 LGTM
  • Exercise——feature/factor-z 变基 + reflog 演练,附 Two 条渐进 Hints。