16. Git 版本控制 — 生信工程师必备技能¶
一句话说明: Git 是一个记录代码"谁在什么时候改了什么"的工具,让你随时可以回退、协作、不怕改坏代码。
一、核心概念(白话版)¶
1.1 什么是版本控制¶
白话比方: 你写毕业论文的时候,是不是经常"另存为"?
论文_v1.docx
论文_v2_导师修改.docx
论文_v3_终版.docx
论文_v3_终版_真的终版.docx
论文_v3_终版_打死不改了.docx
这就是手动版本控制——又乱又容易搞混。
版本控制系统(VCS, Version Control System) 就是自动帮你管理这些版本的工具。它会记录每次修改的内容、时间、作者,你随时可以"时光倒流"回到任何一个版本。
1.2 Git vs GitHub/GitLab 的区别¶
| 名称 | 是什么 | 白话比方 |
|---|---|---|
| Git | 安装在你电脑上的版本控制软件 | 本地的日记本,记录每次改动 |
| GitHub | 在线代码托管平台(微软旗下) | 把日记本放到云端,别人也能看 |
| GitLab | 另一个在线代码托管平台 | 另一个云端,功能类似 GitHub |
关键区别: Git 是工具,GitHub/GitLab 是平台。没有网络你也能用 Git,但 GitHub 需要联网。
1.3 三个工作区域¶
Git 有三个核心区域,理解它们是用好 Git 的关键:
工作区(Working Directory) → 暂存区(Staging Area) → 仓库(Repository)
你正在改的文件 git add 后的文件 git commit 后的文件
白话比方: - 工作区 = 你的书桌,正在写的草稿纸 - 暂存区 = 信封,把写好的稿子装进去,准备寄出 - 仓库 = 邮局档案室,寄出去的信永久保存
1.4 分支(Branch)是什么¶
白话比方: 你写论文想试一个新的分析方法,但又怕搞砸现有的结果。
- 没有分支: 复制一整份论文文件夹来改 → 改完还要手动合并
- 有分支: Git 帮你创建一个"平行宇宙",你在新分支里随便改,改好了合并回来,改坏了直接删掉,主分支完全不受影响
main ────●────●────●────●──── (主线,稳定版本)
\ /
feature ●────●────● (新功能分支,试验用)
二、日常使用命令(最常用,重点掌握)¶
2.1 创建仓库:git init / git clone¶
# 方式一:在当前目录创建一个新的 Git 仓库(从零开始)
git init
# 白话:告诉 Git "从现在起帮我管这个文件夹"
# 方式二:从远程仓库克隆一份到本地(下载别人的项目)
git clone https://github.com/用户名/项目名.git
# 白话:把 GitHub 上的项目完整复制一份到你电脑上
# 使用场景:
# git init → 自己从零开始写一个新项目
# git clone → 下载别人的开源工具(如 MetaPhlAn、HUMAnN)
2.2 添加和提交:git add / git commit¶
# 查看当前状态(哪些文件改了、哪些是新的)
git status
# 白话:看看书桌上有哪些草稿还没装进信封
# 把文件添加到暂存区
git add analysis.py # 添加单个文件
git add . # 添加当前目录所有改动的文件
git add scripts/ # 添加整个文件夹
# 白话:把写好的稿子装进信封
# 提交(保存一个版本快照)
git commit -m "添加物种丰度分析脚本"
# 白话:把信封寄出去,并在信封上写一句备注
# -m 后面是提交信息(commit message),描述你这次改了什么
# 好的提交信息:"fix: 修复Shannon指数计算公式中的除零错误"
# 差的提交信息:"改了一些东西" "update" "asdf"
# 使用场景:每完成一个小功能或修复就 add + commit 一次
2.3 同步远程:git push / git pull¶
# 把本地的提交推送到远程仓库(上传到 GitHub)
git push origin main
# 白话:把你本地写好的代码上传到 GitHub
# origin = 远程仓库的名字(默认叫 origin)
# main = 分支名(主分支)
# 从远程仓库拉取最新代码到本地(下载别人的更新)
git pull origin main
# 白话:把 GitHub 上别人的最新改动下载到你电脑上
# 使用场景:
# git push → 写完代码后上传备份,或者分享给同事
# git pull → 每天开始工作前先拉取最新代码
2.4 查看状态和历史:git status / git log / git diff¶
# 查看工作区状态
git status
# 白话:看看哪些文件改了但还没提交
# 红色 = 改了但没 add
# 绿色 = 已经 add 等待 commit
# 查看提交历史
git log # 完整日志
git log --oneline # 精简版,每条提交只显示一行
git log --oneline -10 # 只看最近 10 条
git log --graph --oneline # 图形化显示分支合并历史
# 白话:翻阅之前所有的"快递记录"
# 查看改动了什么内容
git diff # 查看工作区和暂存区的差异(还没 add 的改动)
git diff --staged # 查看暂存区和仓库的差异(已经 add 但没 commit 的)
git diff HEAD~1 # 和上一次提交对比
# 白话:对比两份稿子,看看哪里不一样
# + 开头的行 = 新增内容(绿色)
# - 开头的行 = 删除内容(红色)
# 使用场景:提交前先 git diff 检查改动是否正确
2.5 分支操作:git branch / git checkout / git merge¶
# 查看所有分支(* 标记当前所在分支)
git branch
# 白话:看看有几条"平行时间线"
# 创建新分支
git branch feature-diversity
# 白话:开一条新的平行时间线叫 feature-diversity
# 切换到某个分支
git checkout feature-diversity
# 白话:跳到那条平行时间线上去工作
# 创建并立即切换(上面两步合一步)
git checkout -b feature-diversity
# 白话:开一条新时间线,同时跳过去
# 合并分支(先切回主分支,再合并)
git checkout main # 先回到主分支
git merge feature-diversity # 把 feature-diversity 的改动合并过来
# 白话:把平行宇宙的成果搬回主世界
# 删除已合并的分支
git branch -d feature-diversity
# 白话:用完的平行时间线可以关掉了
# 使用场景:
# 开发新功能 → 建分支 → 开发 → 测试通过 → 合并回 main → 删除分支
2.6 临时保存:git stash¶
# 场景:你正在写一半代码,突然需要切分支修 bug
git stash # 暂存当前未提交的改动
# 白话:把桌上写了一半的草稿先放抽屉里
git checkout main # 切到其他分支去修 bug
# ... 修完 bug ...
git checkout feature-xxx # 切回来继续工作
git stash pop # 取出之前暂存的改动
# 白话:把抽屉里的草稿拿回桌上继续写
git stash list # 查看所有暂存的内容
# 白话:看看抽屉里有几份草稿
2.7 忽略文件:.gitignore¶
# 在项目根目录创建 .gitignore 文件
# 里面写上不想被 Git 追踪的文件/文件夹
# .gitignore 文件内容示例:
*.log # 忽略所有 .log 结尾的文件
*.pyc # 忽略 Python 编译文件
__pycache__/ # 忽略 Python 缓存文件夹
.DS_Store # 忽略 Mac 系统文件
data/raw/ # 忽略原始数据文件夹(太大了)
results/ # 忽略结果文件夹(可以重新生成)
.env # 忽略环境变量文件(可能有密码)
# 使用场景:把大文件、临时文件、敏感信息排除在版本控制之外
三、进阶操作¶
3.1 解决冲突(Merge Conflict)¶
什么时候会冲突? 两个人(或两个分支)同时改了同一个文件的同一行代码。
白话比方: 你和同事同时修改了同一份报告的同一段话,Git 不知道该听谁的,就报"冲突"让你手动选择。
冲突长什么样:
<<<<<<< HEAD
shannon_index = diversity(data, method="shannon") # 你的版本
=======
shannon_index = calc_diversity(data, type="shannon") # 同事的版本
>>>>>>> feature-branch
解决步骤:
# 1. 合并时发现冲突
git merge feature-branch
# 输出:CONFLICT (content): Merge conflict in analysis.py
# 2. 打开冲突文件,手动选择保留哪个版本(或两者合并)
# 删掉 <<<<<<< ======= >>>>>>> 这些标记
# 3. 解决完后,标记为已解决
git add analysis.py
# 4. 完成合并提交
git commit -m "resolve: 合并 feature-branch 解决分析函数冲突"
3.2 git rebase vs git merge¶
| 特性 | git merge | git rebase |
|---|---|---|
| 做什么 | 创建一个合并提交 | 把你的提交"搬到"目标分支最新位置后面 |
| 历史记录 | 保留完整分支历史(有分叉) | 线性历史(没有分叉,更干净) |
| 白话 | 两条路交汇 | rebase 就像你写了 3 页作业,发现老师又发了新版讲义,你就把自己的 3 页撕下来重新贴到新讲义后面,看起来像一直基于最新版写的 |
| 推荐场景 | 团队协作(安全) | 个人分支整理(干净) |
| 注意 | 简单安全 | 不要对已推送的提交做 rebase |
# merge(推荐新手使用)
git checkout main
git merge feature-branch
# rebase(让提交历史更干净)
git checkout feature-branch
git rebase main
3.3 git reset vs git revert¶
| 特性 | git reset | git revert |
|---|---|---|
| 做什么 | 回退到某个版本,丢弃之后的提交 | 创建一个新提交来撤销某次改动 |
| 白话 | 时光倒流,后面的历史消失 | 用一个新的"反操作"来修正 |
| 会改历史吗 | 会(危险) | 不会(安全) |
| 推荐场景 | 本地还没推送的提交 | 已推送到远程的提交 |
# reset:回退到上一个版本(本地使用)
git reset --soft HEAD~1 # 回退提交但保留改动在暂存区
git reset --mixed HEAD~1 # 回退提交,改动放回工作区(默认)
git reset --hard HEAD~1 # 回退提交,改动全部丢弃(危险!)
# revert:安全地撤销某次提交(可以推送到远程)
git revert abc1234 # abc1234 是你要撤销的那次提交的 ID
3.4 tag 打标签¶
# 给重要版本打个标签(比如论文投稿时的代码版本)
git tag v1.0 # 轻量标签
git tag -a v1.0 -m "论文投稿版本" # 附注标签(推荐,有描述信息)
# 查看所有标签
git tag
# 推送标签到远程
git push origin v1.0 # 推送单个标签
git push origin --tags # 推送所有标签
# 使用场景:论文投稿、pipeline 发布正式版本时打标签
四、生信项目中的 Git 最佳实践¶
4.1 哪些文件应该/不应该放进 Git¶
| 应该放(追踪) | 不应该放(忽略) |
|---|---|
| 脚本代码(.py .R .sh) | 原始测序数据(.fastq .fq .bam .sam) |
| 配置文件(config.yaml) | 中间结果文件(.bai(BAM索引文件) .idx(通用索引文件)) |
| 小型注释文件(metadata.tsv) | 大型数据库文件(.dmnd(DIAMOND比对数据库) .db(通用数据库文件)) |
| 文档说明(README.md) | 临时文件(.tmp .log) |
| Snakefile / Nextflow 流程(生信流程管理工具,像自动化菜谱) | 编译/缓存文件(pycache) |
| 环境配置(environment.yml) | 敏感信息(密码、API key) |
4.2 生信项目 .gitignore 模板¶
# ========== 测序数据(太大,不应追踪) ==========
*.fastq
*.fastq.gz
*.fq
*.fq.gz
*.bam
*.bam.bai
*.sam
*.sra
*.bcf
*.vcf.gz
# ========== 比对索引文件 ==========
*.bt2
*.fai
*.dict
*.amb
*.ann
*.bwt
*.pac
*.sa
# ========== 数据库文件 ==========
*.dmnd
*.nhr
*.nin
*.nsq
# ========== 中间/临时文件 ==========
*.tmp
*.log
*.o
*.e
slurm-*.out
# ========== Python ==========
__pycache__/
*.pyc
.ipynb_checkpoints/
# ========== R ==========
.Rhistory
.Rdata
.RData
# ========== 系统文件 ==========
.DS_Store
Thumbs.db
# ========== 环境/敏感信息 ==========
.env
credentials.json
# ========== 大型结果(可选择性忽略) ==========
# results/figures/ 可以追踪(方便查看)
# results/tables/ 可以追踪
# results/intermediate/ 不追踪
4.3 commit message 规范¶
推荐格式: 类型: 简短描述
feat: 添加Alpha多样性计算模块 # 新功能
fix: 修复Shannon指数公式除零错误 # 修 bug
docs: 补充README中的使用说明 # 文档
refactor: 重构数据清洗函数 # 重构代码(把代码重新整理一遍,功能不变——像整理杂乱书房,书还是那些书)
data: 更新样本元数据表 # 数据变更
style: 统一代码缩进为4空格 # 代码格式
test: 添加分类器准确率测试 # 测试
chore: 更新conda环境配置 # 杂项
4.4 生信 pipeline 版本管理¶
# 1. 用 tag 标记 pipeline 的稳定版本
git tag -a v2.0 -m "MetaG pipeline v2.0: 支持PE150数据"
# 2. 用 environment.yml 锁定软件版本
conda env export > environment.yml
git add environment.yml
git commit -m "chore: 锁定conda环境软件版本"
# 3. 在论文/报告中注明代码版本
# "分析使用 pipeline v2.0 (commit: abc1234)"
五、常用 Git 工作流¶
5.1 个人开发流程¶
写代码 → git add → git commit → git push → 继续写 → 循环
1. 写/改代码
2. git status # 看看改了什么
3. git diff # 确认改动内容
4. git add . # 添加到暂存区
5. git commit -m "描述" # 提交
6. git push # 推送到 GitHub 备份
5.2 团队协作(Feature Branch Workflow)¶
流程图(文字版):
main 分支(稳定版本,所有人共享)
│
├── git checkout -b feature-diversity (开个新分支写新功能)
│ │
│ ├── 写代码 → add → commit(在分支上反复开发)
│ ├── 写代码 → add → commit
│ │
│ ├── git push origin feature-diversity(推到远程)
│ │
│ └── 在 GitHub 上提 Pull Request(PR,就是"我写好了,请大家检查一下再合并"——像交作业前先找同学检查)
│ │
│ ├── 同事 Code Review(代码审查)
│ ├── 讨论 / 修改
│ └── 审核通过 → Merge 合并到 main
│
├── 下一个功能:git checkout -b feature-xxx(再开新分支)
│ ...
核心原则: - main 分支永远保持可用状态 - 每个新功能/修复在独立分支上开发 - 通过 Pull Request 合并,团队成员互相审查代码
六、实操演练:完整流程¶
以下是一个完整的从零开始的 Git 使用流程:
# ==========================================
# 第一步:初始化项目
# ==========================================
mkdir t2d_metagenome # 创建项目文件夹
cd t2d_metagenome # 进入文件夹
git init # 初始化 Git 仓库
# ==========================================
# 第二步:配置用户信息(首次使用需要设置)
# ==========================================
git config --global user.name "Wenqiang Peng"
git config --global user.email "你的邮箱@example.com"
# ==========================================
# 第三步:创建项目文件
# ==========================================
mkdir scripts data results docs # 创建子目录
touch README.md # 创建说明文件
touch scripts/01_qc.sh # 创建质控脚本
touch .gitignore # 创建忽略规则文件
# ==========================================
# 第四步:设置 .gitignore
# ==========================================
echo "*.fastq.gz" >> .gitignore
echo "*.bam" >> .gitignore
echo "data/raw/" >> .gitignore
echo "results/intermediate/" >> .gitignore
# ==========================================
# 第五步:首次提交
# ==========================================
git add . # 添加所有文件到暂存区
git status # 确认状态
git commit -m "feat: 初始化T2D宏基因组项目结构"
# ==========================================
# 第六步:写代码,开发功能
# ==========================================
# (编辑 scripts/01_qc.sh 添加质控代码)
git add scripts/01_qc.sh
git commit -m "feat: 添加fastp质控脚本"
# ==========================================
# 第七步:连接远程仓库并推送
# ==========================================
# 先去 GitHub 创建一个同名空仓库
git remote add origin https://github.com/你的用户名/t2d_metagenome.git
git branch -M main # 把主分支重命名为 main
git push -u origin main # 首次推送(-u 设置默认上游分支)
# ==========================================
# 第八步:分支开发
# ==========================================
git checkout -b feature-diversity # 创建并切换到新分支
# (编辑代码...)
git add .
git commit -m "feat: 添加Alpha多样性分析脚本"
git push origin feature-diversity # 推送分支到远程
# 在 GitHub 上创建 Pull Request,审核通过后合并
# ==========================================
# 第九步:合并后清理
# ==========================================
git checkout main # 切回主分支
git pull origin main # 拉取合并后的最新代码
git branch -d feature-diversity # 删除已合并的分支
# ==========================================
# 第十步:打标签发布
# ==========================================
git tag -a v1.0 -m "T2D pipeline v1.0 完成质控和多样性分析"
git push origin --tags # 推送标签到远程
七、面试怎么答¶
Q1: 请简单介绍一下 Git 是什么?你在项目中怎么用的?¶
白话回答:
Git 是一个版本控制工具,可以记录代码的每一次修改。我在宏基因组项目中用 Git 管理分析脚本和流程文件。每完成一个分析步骤就 commit 一次,commit message 写清楚改了什么。代码同步推送到 GitHub 做备份。这样做的好处是:第一,代码改坏了可以回退;第二,导师和同事可以看到我的分析过程;第三,论文投稿时可以打 tag 标记对应的代码版本,保证可复现。
Q2: git merge 和 git rebase 有什么区别?¶
白话回答:
两者都是合并分支的方法。merge 会保留完整的分支历史,生成一个合并提交,可以看到"哪条分支什么时候合进来的"。rebase 会把你分支上的提交"搬到"目标分支最新位置后面,历史是一条直线,更干净。我个人习惯是:团队协作用 merge(安全、有记录),个人整理提交用 rebase(干净)。有一条铁律:已经推送到远程的提交不要 rebase,否则会把队友搞崩溃。
Q3: git reset 和 git revert 有什么区别?¶
白话回答:
reset 是"时光倒流",直接回退到某个版本,后面的提交历史都没了。revert 是"做一个反向操作",创建一个新提交来撤销之前的改动,但历史记录还在。如果代码还没推送到远程,用 reset 没问题。如果已经推送了,只能用 revert,因为 reset 会改变历史,导致别人的代码对不上。
Q4: 生信项目中哪些文件应该放进 Git?哪些不应该?¶
白话回答:
应该放的是:分析脚本(Python、R、Shell)、流程文件(Snakefile)、配置文件、conda 环境配置、README 文档。不应该放的是:原始测序数据(.fastq 几个 G 太大了)、比对文件(.bam)、大型数据库文件。这些大文件通过 .gitignore 排除掉。如果真的需要版本管理大文件,可以考虑 Git LFS(Large File Storage,把大文件存在专门服务器上,仓库里只保留一个链接,仓库不会被大文件撑爆)。
Q5: 你能描述一下 Feature Branch Workflow 吗?¶
白话回答:
这是团队协作最常用的 Git 工作流。核心思路是:main 分支始终保持稳定可用,每个新功能或 bug 修复都在独立的 feature 分支上开发。流程是:从 main 拉一个新分支,在上面写代码并提交,写完推到远程,然后在 GitHub 上提 Pull Request。团队成员做 Code Review,审核通过后合并到 main,最后删掉 feature 分支。这样做的好处是不会互相干扰,代码质量也有保障。
Q6: 遇到 merge conflict 怎么解决?¶
白话回答:
冲突是因为两个人同时改了同一个文件的同一行。Git 会在文件里用特殊标记标出冲突的部分,HEAD 下面是你的版本,等号下面是对方的版本。解决方法是:打开文件,手动选择保留哪个版本(或者两者合并),删掉那些标记符号,然后 git add 标记为已解决,最后 git commit 完成合并。
Q7: git fetch 和 git pull 有什么区别?¶
白话回答:
git fetch 是只从远程仓库下载最新的数据,但不会自动合并到你当前的代码里,相当于"看看远程有什么更新"。git pull 等于 git fetch + git merge,下载完直接合并。我个人习惯是如果不确定远程有什么改动,先 fetch 看看再决定要不要合并,更安全一些。
八、延伸阅读¶
- Pro Git 中文版(免费电子书) — 最权威的 Git 教程
- 官方网站:https://git-scm.com/book/zh/v2
- PLOS 论文:A Quick Introduction to Version Control with Git and GitHub — 生物学家视角的 Git 入门
- DOI: 10.1371/journal.pcbi.1004668
- Atlassian Git Tutorial — 配图精美的 Git 教程(英文)
- https://www.atlassian.com/git/tutorials
- Learn Git Branching — 交互式学习 Git 分支操作的网站
- https://learngitbranching.js.org/?locale=zh_CN
- GitHub Git Cheat Sheet — GitHub 官方 Git 速查表
- https://education.github.com/git-cheat-sheet-education.pdf
- Oh Shit, Git!?! — Git 常见问题急救指南
- https://ohshitgit.com/zh
最后提醒: Git 不需要一次学完所有命令。日常开发掌握
add → commit → push → pull这四个命令就够用了。遇到问题再查其他命令,用多了自然就熟了。