科研代码很少会直接保持在“最终版”。改一个参数、换一份数据、补一个评价指标,结果可能就变了。几个月后再回头看,难找的往往是这张图究竟来自哪版代码、哪组配置,以及中间改过什么。

Git 解决的正是这类版本和协作问题。它既可以管理一个人反复试验的过程,也可以让多位合作者在同一个项目中留下可检查的修改记录。

现在,Codex、Claude Code、GitHub Copilot 这类工具已经可以读取仓库上下文并同时修改多个文件。代码改得更快以后,版本边界反而更值得重视:人需要看清 AI 改了什么、哪些修改准备保留;AI 在被允许读取仓库信息时,也能借助清楚的目录、差异和提交历史理解当前项目。以 VS Code 为例,其 workspace context 文档 已经把整个工作区和代码搜索作为智能体获取上下文的重要来源。

Summary

对科研项目来说,Git 最实用的价值有四个:记录代码和配置变化、隔离不同实验、追踪协作过程、把结果对应到明确版本。

科研代码最容易丢失的是上下文

不少科研项目都出现过类似的文件:

plot.py
plot_new.py
plot_final.py
plot_final2.py
plot_final_修改.py

这套方法应用在少量文件时还行得通,但文件量一多时间一长就会遇到几个问题:

  • 文件名只能表达很少的信息,很难说明具体改了哪些内容;
  • 代码、配置和说明文档各自复制,版本之间容易错位;
  • 删除过的逻辑很难找回,也不知道为什么删除;
  • 多个人(多个来源)同时修改时,只能人工比较多个文件;
  • 论文返修或结果复查时,很难还原当时的运行状态。

Git 会把一次提交保存为项目状态的一个快照,并让后续提交指向前一个提交。大多数历史查看、比较和提交操作都可以在本地完成,不依赖持续联网。这个设计使代码变化形成了一条可追踪的历史,而不是散落在多个“最终版”文件里。Pro Git 对 Git 数据模型的说明 中将其概括为一系列项目快照。

这里需要提前说清:Git 的核心任务是版本控制。远程仓库、服务器快照和异地备份仍然要单独安排。没有提交的修改也可能丢失,所以“装了 Git”本身不会自动保护文件。

从一个项目认识 Git

为了把这些情况讲得具体一些,我准备了一个小型气象项目示例。它读取粗网格温度、网格海拔和站点观测,使用温度垂直递减率做订正,再计算 MAE、RMSE,并输出观测—预测对比图。

仓库中主要有这些内容:

configs/                    模型和路径配置
data/raw/                   原始数据
tests/                      模型与指标测试
weather_downscale_demo/     数据处理、模型、评估与绘图
outputs/                    本地生成结果
pyproject.toml              Python 项目配置
uv.lock                     依赖锁定结果

项目使用 uv 管理 Python 环境,这里推荐在构建新 python 项目时优先使用 uv 管理。拿到仓库后可以直接运行 uv sync 会根据项目配置和锁文件同步环境,具体行为可以参考 uv 官方文档

从初始化到第一次提交

安装 Git 后先确认命令可用,并设置提交身份:

git --version
git config --global user.name "Your Name"
git config --global user.email "you@example.com"

用户名和邮箱会进入提交记录。公开仓库如果不希望展示常用邮箱,可以使用代码托管平台提供的匿名邮箱,但不要随便填写一个无法区分作者的公共身份。

学会看仓库状态

Git 的日常使用可以先记住两条命令:

git status
git diff

git status 会区分已经跟踪、尚未跟踪、已经暂存和仍在工作区中的变化;

git diff 用来查看具体改动了哪些行。

当然, 不学命令也完全没问题! VS Code 中暂存、提交、创建分支、查看历史和解决冲突都可以在图形界面完成。对刚接触 Git 的用户来说,先理解状态变化,再使用界面操作,通常比记住大量命令更顺手。

在 vscode 侧边栏打开“源代码管理”,可以看到代码相对于上次提交后的最新变动(也可以在目录中直接看到标记)

示例中有多种未提交状态,它们都只存在于工作区,尚未提交:

 D 表示一个已跟踪文件被删除
 M 表示文件内容发生修改
 U 表示新文件还没有进入 Git 管理

在 vscode 中能看到具体的新增行、删除行和修改位置。通常绿色表示新增,红色表示删除,黄色表示修改。

或者在修改对照界面查看,更直观

分清工作区与暂存区

在仓库中的每次修改,其内容会先保存在工作树中。点击 VS Code 左侧的“源代码管理”,这些还没有暂存的文件会出现在 Changes 区域。此时只是本机文件发生了变化,Git 历史里还没有新的版本。

Git 的几个区域很容易混在一起,可以先按下面这条路径理解:

工作区 / 工作树
正在编辑的项目文件
        ↓ 暂存
暂存区(Index / Staging Area)
下一次提交准备包含的内容
        ↓ 提交
本地仓库(.git)
提交历史、分支、标签等对象
        ↓ 推送 / 同步
远程仓库
GitHub、GitLab 或课题组服务器上的副本

“工作区”通常指 VS Code 当前打开、可以直接编辑的项目目录;Git 官方术语更常写作 working tree(工作树),也就是从某个提交检出到磁盘上的那套文件。大多数普通仓库只有一棵工作树,所以这两个说法经常被混用。

“仓库”一般指整个项目文件夹。严格来说,本地仓库的历史和对象主要保存在 .git 中,工作树是从这份历史中检出的可编辑文件。Git Glossary 对 working tree、index、repository 和 HEAD 有更严格的定义。

暂存区并不是另一个需要手动寻找的文件夹。它保存的是“下一次提交准备记录的快照”。在 VS Code 中,点击文件右侧的 +,文件就会从 Changes 移到 Staged Changes。点击 - 只是取消暂存,文件修改仍然保留。

最后还有一个经常出现的词:HEAD。它可以理解为“当前工作位置”,通常指向正在使用的分支及其最新提交。切换分支以后,HEAD 会跟着移动,工作树也会更新为对应分支的内容。

一个文件为什么会同时出现在两处

这是暂存区最值得单独解释的情况。

假设先修改 configs/default.toml,随后把它加入暂存区。此时暂存区保存了这个文件的当前版本。如果继续编辑同一个文件,新的修改会再次出现在 Changes(更改),而刚才暂存的内容仍然留在 Staged Changes(暂存的更改)

于是,同一个文件名可能同时出现两次:

  • Staged Changes:已经选入下一次提交的部分;
  • Changes:暂存以后又产生的新修改。

这并不是重复文件。Git 正在同时保存两个不同状态。点击提交时,默认只记录暂存区中的内容,后续修改会继续留在工作树。

对于科研代码,这个能力很实用。一个脚本里可能同时包含“修复单位错误”和“尝试新的配色”。可以只暂存单位修复,先形成一个容易审查的提交,绘图试验留到下一次。

完成一次清楚的提交

将一次修改的文件添加到暂存区后,就可以正式提交到仓库了,提交时要求必须要有说明,不想每次手动写的话可以让 VS Code 根据暂存内容自动生成提交信息。

提交信息应说明这次修改的目的,例如“修正温度递减率方向”会比“更新代码”更有用。一个提交尽量只表达一个完整意图。“增加高海拔配置”和“重写验证图”最好拆开。以后查看历史、撤销修改,或者让 AI 分析某次变化时,边界清楚的提交会更容易理解。

这里还要区分两个看起来相近的操作:

  • Unstage Changes / 取消暂存:只把内容移回 Changes,不会删除修改;
  • Discard Changes / 放弃更改:会丢弃尚未提交的修改,使用前必须确认。

Warning

暂存和取消暂存通常可逆;放弃更改会直接影响工作树。界面按钮很方便,但仍要先看清操作对象。

用分支隔离科研实验

科研代码经常需要同时保留一条稳定基线和几条探索路线。比如主分支已经能生成温度订正结果,现在想增加湿度订正、替换损失函数,或测试一套新的站点筛选规则。分支可以让这些尝试沿着不同历史向前推进。

在 VS Code 中,当前分支会显示在底部状态栏和 Source Control Graph 中。点击分支名,或在命令面板选择 Git: Create Branch,就可以从当前版本建立实验分支。实验完成后,再从 Git: Merge Branch 选择需要合并的分支。

对科研项目,可以考虑这样区分:

  • main或 master:可以运行、通过基本检查的稳定版本;
  • exp/…:结论还不确定的试验;
  • feature/…:目标明确、准备合并的功能;
  • fix/…:针对具体问题的修复。

分支名称只是一种约定,重点是让其他人一眼知道这条线处于什么状态。当然这并不是必须的,对于我们自己的仓库完全可以一条线走到尾

在这里推荐使用 VS Code 中 Git 管理的插件,可以提供更美观、功能更强大的界面:

  1. GitLens
  2. Git Graph

版本冲突时 Git 会提醒你处理

两条分支修改了同一行、一个分支删除文件而另一个分支继续修改,Git 都可能暂停合并并要求人工处理。冲突表示自动合并缺少足够信息,项目本身并没有因此损坏。

VS Code 会把冲突文件列在 Merge Changes 中。简单冲突可以选择 Current、Incoming 或 Both;复杂情况更适合打开三方合并编辑器,同时查看当前分支、传入分支和最终结果。

当前 VS Code 还提供实验性的 AI 冲突解决入口。AI 可以分析共同祖先和两边变化并给出候选结果,但最终版本仍要由人检查。物理含义、数据口径和实验边界往往不在冲突代码片段里。

多人协作时要区分本地和远程

一次 Commit 只会进入本地仓库。点击 Push 或 Sync Changes 后,提交才会发送到 GitHub(或其它远程仓库)。远程仓库负责交换代码和保存共享历史,本地工作树仍然可以离线修改和提交。

当历史真正有用

Git 的优势不只体现在“保存过几个版本”。当出现以下可能的情况时,Git 提交历史会变成排查和复现的证据链,这里不过多赘述:

  1. 找到问题从哪次修改开始
  2. 已经共享的错误需要撤销
  3. 临时工作和阶段版本随时保存和回顾

Git 管理科研项目的边界

Git 很适合保存脚本、配置、文档、小型测试数据和环境锁文件。对于 WRF 或 AI 降尺度项目,下面这些内容通常值得进入仓库:

  • 数据预处理、训练、评估和绘图脚本;
  • namelist、YAML、TOML 等配置文件;
  • pyproject.tomluv.lockenvironment.yml 等环境文件;
  • README、实验索引等说明文件(这就体现出 markdown 文件的优势了)。

大型 NetCDF、 Zarr、模型输出以及生成的图片文件需要排除在外。它们会让仓库迅速膨胀,也不适合频繁提交。确实需要随仓库管理大文件时,可以评估 Git LFS;它在 Git 中保存指针,把实际大文件放在远程存储。

Git 仓库通过 .gitignore 将不需要追踪的文件排除,例如:

__pycache__/
outputs/
*.nc
*.png

令牌、密码、私钥、内部服务器地址和未公开数据也不要进入提交。

Warning

上传仓库或把项目交给 AI 处理前,检查数据授权、个人信息、内部路径和合作协议。公开代码不代表所有输入与结果都适合公开。

常用命令和易混淆概念

VS Code 已经覆盖日常 Git 操作,命令行仍然适合快速检查、远程服务器和故障排查。下面保留一份小型速查,不需要一次全部记住。

常用命令速查

目的命令
查看仓库状态git status
查看尚未暂存的逐行变化git diff
查看下一次提交将包含什么git diff --staged
暂存指定文件git add <file>
创建提交git commit -m "说明修改目的"
查看图形化历史git log --oneline --graph --decorate --all
更新远程状态但不修改当前分支git fetch
推送本地提交git push
临时收起修改git stash push -u -m "WIP: 说明"
撤销一个已提交的修改git revert <commit>
在好坏版本间定位错误git bisect start <bad> <good>

最容易混淆的概念

概念区别
工作区与工作树日常语境里常指同一套可编辑文件;严格术语中 working tree 是检出的文件,worktree 还包含独立的 HEAD、暂存区等元数据
暂存与提交暂存是在选择下一次提交的内容;提交才会在本地历史中形成新快照
Commit 与 PushCommit 写入本地仓库;Push 才把本地提交发送到远程
Unstage 与 DiscardUnstage 只取消暂存;Discard 会丢弃工作树修改
Fetch 与 PullFetch 只更新远程信息;Pull 会把远程变化继续整合到当前分支
Restore 与 RevertRestore 主要处理工作树或暂存区文件;Revert 用新提交撤销已有提交
Branch 与 WorktreeBranch 是指向提交的分支引用;Worktree 是把某条分支检出到独立目录后的工作实例
Stash 与 CommitStash 适合短期收起未完成修改;Commit 适合长期、可解释的项目历史
Merge 与 RebaseMerge 保留两条历史的汇合;Rebase 会重放提交并改变提交基点,共享分支上需要更谨慎

回到 AI 辅助编程这个背景,Git 的价值还在继续增加。AI 可以在短时间内改动更多文件,人需要用状态、差异、暂存和提交边界来审查这些变化;清楚的 Git 历史也能为后续的人和 AI 提供更可靠的仓库上下文。无论代码由谁写,最终都应该能回答:改了什么、为什么改、验证过什么,以及这组结果对应哪个版本。

相关阅读