4. 版本控制核心技能

在计算生物项目中,Git 的价值远不止“把代码存起来”。一个典型项目往往同时包含:分析脚本、Snakemake/Nextflow 流程、参考基因组与注释文件、样本元数据、conda 环境文件、中间结果与最终图表。这些对象的体量、变更频率和可复现要求差异极大。如果全部塞进同一个仓库、用同一种方式管理,仓库会迅速膨胀,克隆变慢,历史记录也会被大文件污染。本节围绕四个进阶能力展开:分支策略、标签、子模块与大文件管理,目标是让仓库既能承载协作开发,又能精确锚定“某次分析到底用了哪一版代码和数据”。

分支策略:让开发、验证与发布互不干扰

计算生物项目的分支设计应服务于“分析可复现”这一目标。推荐采用轻量化的主干加短生命周期分支模型:main 始终保持可运行、可发布状态;日常开发在 dev 上集成;每个具体任务(新增变异过滤步骤、修复比对参数、更新注释版本)从 dev 切出 feature/* 或 fix/* 分支,完成后通过合并请求回到 dev,验证通过再合入 main 并打标签。

关键在于把“流程代码变更”和“分析结果变更”分开。例如新增一个 filter_variants 规则时,分支内只改流程与配置,不提交中间 BAM/VCF;验证时在独立工作目录运行小样本测试集,确认输出行数、关键字段和校验和符合预期后,才合并。这样 main 上的每一次提交都对应一个可复现的分析版本。

git switch -c feature/add-variant-filter dev
# 修改 rules/filter.smk 与 config/config.yaml
git add rules/filter.smk config/config.yaml
git commit -m "feat: add GATK hard-filter step for SNPs"
git switch dev
git merge --no-ff feature/add-variant-filter
git switch main
git merge --no-ff dev
git tag -a v1.3.0 -m "release: variant calling pipeline with hard-filter"

标签:把“可复现”钉在具体提交上

标签是复现工程的锚点。论文、报告或交付给合作者的结果,都应能追溯到某个标签。建议使用语义化版本:v主版本.次版本.修订号。主版本表示流程结构或参考数据发生不兼容变更;次版本表示新增分析步骤或参数;修订号表示修复错误但不改变结果语义。标签应使用附注标签(-a),并写清参考基因组版本、关键工具版本和适用样本范围。

复现检查:拿到一个标签后,应能用 git describe --tags 确认当前提交,用 git show v1.3.0 查看标签说明,再结合环境文件重建运行环境。若标签说明中缺少参考基因组版本,复现就存在歧义。

子模块:复用共享流程与参考资源

当多个项目共用同一套流程模板或注释仓库时,子模块比复制粘贴更可靠。子模块把另一个 Git 仓库以固定提交的形式嵌入当前仓库,父仓库只记录子模块的 URL 和 commit,不复制其全部历史。典型用法是把公共流程仓库 pipelines-common 作为子模块挂到 tools/ 下。

git submodule add https://example.org/bio/pipelines-common.git tools/pipelines-common
git commit -m "chore: add shared pipeline submodule"
# 克隆含子模块的项目
git clone --recurse-submodules https://example.org/bio/project-x.git
# 更新子模块到父仓库记录的提交
git submodule update --init --recursive

子模块的要点是“父仓库锁定的是子模块的某个 commit”。因此升级公共流程时,需要在子模块内切换目标提交,再回到父仓库提交新的指针。这样任何一次父仓库检出,都能还原当时使用的公共流程版本。

大文件管理:Git LFS 与数据分层

参考基因组、比对索引、BAM/CRAM、大型矩阵等不应直接进入普通 Git 历史。推荐用 Git LFS 跟踪大文件,或更彻底地把数据放在对象存储/共享盘,仓库只保留校验和与获取脚本。Git LFS 通过指针文件替代真实内容,检出时再拉取实际数据。

git lfs install
git lfs track "*.bam" "*.cram" "*.fa" "*.faidx"
git add .gitattributes
git add data/reference/GRCh38.fa
git commit -m "data: track reference genome with LFS"

判断标准很直接:单个文件超过 50 MB、或总数据量超过仓库代码量一个数量级时,就应使用 LFS 或外置存储。对必须随仓库分发的注释文件,可用 LFS;对可重新下载的公共参考数据,优先写 fetch_data.sh 并记录 URL 与 SHA256,而不是提交文件本身。

计算生物项目 Git 进阶结构:分支、标签、子模块与大文件 分支策略 main dev feature/* fix/* 切出 开发 修复 标签锚点 v1.3.0 附注标签 记录参考基因组、工具版本 对应可复现的分析提交 子模块 父仓库 公共流程 父仓库锁定子模块 commit 检出即还原共享流程版本 大文件管理 Git LFS 外置存储 *.bam / *.cram / *.fa 仓库只留指针与校验和 复现数据流:从标签到结果 检出标签 恢复子模块 拉取 LFS 数据 重建运行环境 复现

把四件事串成一条复现链

分支、标签、子模块与 LFS 不是彼此独立的技巧,而是一条链:分支负责“怎么改”,标签负责“改到哪一版算数”,子模块负责“共享流程用哪一版”,LFS 负责“大文件怎么随版本走”。当合作者拿到一个标签时,标准动作是:git clone --recurse-submodules 检出仓库,git checkout v1.3.0 切到目标标签,git submodule update --init --recursive 恢复公共流程,git lfs pull 拉取参考数据,再按 environment.yml 重建环境。四步都成功,才谈得上结果可复现。

常见误区:把 BAM 直接 git add 进普通历史,之后即使删除文件,历史体积也不会减小;或者只提交了流程代码却忘了记录子模块指针,导致别人检出的公共流程版本与你的不一致。前者应改用 LFS 并考虑 git lfs migrate 清理历史,后者应在合并前检查 git submodule status 是否与预期 commit 一致。

最后给出一个可直接使用的标签说明模板,建议写入 CHANGELOG.md 或标签附注中:版本号、日期、参考基因组与注释版本、关键工具及版本、适用样本范围、已知限制。这样,半年后回看某个结果时,仍能凭标签说明快速判断它是否适用于当前分析。