HomeArchiveBlog


Original contents are licensed under CC BY-NC 4.0. All rights reserved © 2026 Kai.
Back to Archives
Basic Skills: Git

A beginner-friendly introduction to Git's working model, daily commands, branches, remote repositories, rollback tools, submodules, Git LFS, and GitHub CI/CD basics.

Tue Apr 28 2026
Tue Apr 28 2026
GitVersion Control
On this page
  • Basic Skills: Git
    • Introduction
    • 应该达成的目标
    • Git 工作模型
      • Overview
      • 暂存, 提交与历史
      • 分支, 合并与冲突
      • 临时保存改动: Stash
      • 变基
      • 推送, 拉取与远程仓库
      • 回退
      • 救命稻草: Reflog
      • 子模块
      • .gitignore
      • 当你想要传大文件: Git LFS
      • 进阶: GitHub CI/CD流水线

Basic Skills: Git

Introduction

Git 是一个分布式版本控制系统, 主要用来追踪文件的修改历史, 协助多人协作开发. Git 和网盘类工具最大的区别在于, Git 可以追踪每次修改的内容变更, 从而方便回退到历史版本上; 并且 Git 支持分支 (Branch) 和合并 (Merge) 的概念, 方便多人同时开发同一个项目, 修改同一个文件.

在 AI Agent 时代, Git 反而出乎意料地更加重要. 因为 Agent 快速生成代码能力带来的快速迭代能力, 需要一个版本控制工具来追踪迭代的演进过程. 而且 Agent 的能力也远远没有达到 One-Shot 就能生成完美代码的程度, 我们常常需要对 Agent 生成的代码进行修改和完善. 另外还有一点是随着 Subagent 的出现, Git Worktree 变得非常有用, 多个 Subagent 可以在 Git Worktree 里面有独立的工作区并行工作, 最后再合并到主分支上.

综上, Git 是一个在 AI 时代仍然非常重要的工具, 并且即使 Agent 自己也能使用 Git 来管理代码, 人类开发者仍然有必要掌握这项基本功.

应该达成的目标

作为 CS/ECE 的学生, 你应该至少掌握以下 Git 基础技能:

目标
  • 理解 Git 的工作模型, 包括工作区, 暂存区, 本地仓库和远程仓库的关系.
  • 理解头指针 (HEAD) 和分支指针
  • 会使用 git add, git commit, git push 提交改动到远程仓库
  • 会使用 git branch, git switch 管理分支
  • 会使用 git merge, git rebase 合并分支
  • 会使用 git stash 临时保存改动
  • 会使用 git reset 回退内容到历史版本

而不是完全将 Git 任务交给 Agent 去做, 或者把 GitHub/GitLab 等当成网盘一样用 ‼️

Git 工作模型

Overview

理解 Git 最重要的一步, 是先理解它把代码放在几个不同的区域里管理. 可以把 Git 想成一个带有草稿桌, 打包台, 本地档案柜和云端备份柜的系统.

  • 工作区 (Working Tree): 你正在编辑的项目目录. 你用编辑器修改文件时, 改动首先出现在这里.
  • 暂存区 (Staging Area / Index): 下一次提交的准备区. git add 不是提交, 而是把某些改动放进暂存区.
  • 本地仓库 (Local Repository): 已经提交的历史记录. git commit 会把暂存区里的内容保存成本地历史中的一个新提交.
  • 远程仓库 (Remote Repository): 通常托管在 GitHub/GitLab 等平台上, 用于多人协作和备份. git push 把本地提交推送上去, git fetch / git pull 把远程变化取回来.

在 Git 里, 提交 (Commit) 可以理解成项目在某一刻的快照. 每个提交都有一个 Hash, 例如 a1b2c3d, 用来唯一标识这个快照. 分支名本质上是一个会移动的指针, 比如 main 指向当前主分支最新的提交. 每次在 main 上提交, main 这个指针就会向前移动到新的提交.

初学时经常会遇到几个特殊名字:

  • HEAD: 当前所在位置. 大多数时候它指向当前分支, 例如你在 main 上工作时, HEAD -> main, main 再指向某个提交. 所以 HEAD 可以近似理解成"我现在站在哪里".
  • HEAD~1: HEAD 的上一个提交. 类似地, HEAD~2 表示再往前两个提交. 这在 git diff HEAD~1 HEAD 或 git reset HEAD~1 里很常见.
  • Detached HEAD: 如果你直接切到某个 commit hash, HEAD 就不再指向分支名, 而是直接指向一个提交. 这适合临时查看历史版本, 但如果要继续开发, 应该创建新分支保存后续提交.
  • origin/main: 远程跟踪分支. 它不是远程服务器上的 main 本身, 而是你本地记录的"上一次 fetch 时 origin/main 在哪里".
  • FETCH_HEAD: 最近一次 git fetch 取回来的提交记录. 它是一个临时引用, 会被下一次 fetch 更新. 平时更常用 origin/main 这类远程跟踪分支, 但看到 FETCH_HEAD 不需要紧张, 它只是 Git 记录"刚刚取到了什么".
  • ORIG_HEAD: 一些危险操作前 Git 帮你记下的原位置, 例如 merge, rebase, reset 前的位置. 如果操作后发现不对, 它有时可以用来回退.

一个常见流程是:

编辑文件 -> git add -> git commit -> git push
 工作区      暂存区      本地仓库      远程仓库

为什么我们需要将工作区和暂存区分开呢? 设想一下我们同时修改了很多文件, 但我们想把它们拆成几个逻辑相关的提交. 如果没有暂存区, 我们必须一次性提交所有改动, 这样提交历史就会很乱. 有了暂存区, 我们可以只将一部分改动放进去, 提交成一个清晰的历史记录.

暂存, 提交与历史

Git 的日常使用通常围绕三个命令展开:

git status
git add <file>
git commit -m "message"

git status 用来查看当前项目处于什么状态: 哪些文件被修改了, 哪些改动已经进入暂存区, 当前在哪个分支上. 初学 Git 时, 每做几步就运行一次 git status 是很好的习惯.

git status --short
#  M .gitignore
#  M AGENTS.md
#  M requirements.txt
# ?? CLAUDE.md
# ?? plan.md

这是一个 git status 的示例, M 表示文件被修改了但还没有进入暂存区, ?? 表示新文件还没有被 Git 追踪.

如果使用 git status --short, 左右两列分别表示暂存区和工作区的状态. 例如 M file.cpp 表示修改已经暂存, M file.cpp 表示修改还在工作区, MM file.cpp 表示这个文件既有已经暂存的修改, 又有暂存后继续编辑出来的新修改. 初学时看不懂也没关系, 直接运行完整的 git status 通常会有更清楚的提示.

git add 把工作区中的改动放入暂存区. 暂存区可以理解成下一次提交的购物车: 你可以只把相关的修改放进去, 不相关的修改先留在工作区.

git add main.cpp
git add src/
git add .

如果你想一次性将所有修改添加到暂存区, 可以回到仓库的根目录, 使用 git add . 来添加当前目录下的所有改动.

如果一个文件里同时包含两类修改, 例如你一边修了 bug, 一边顺手改了格式, 可以用 git add -p 按代码块选择要暂存的部分:

git add -p main.cpp

这会让 Git 一段一段地询问是否加入暂存区. 这个命令很适合用来拆分干净的提交.

git commit 会把暂存区中的内容保存为一个提交. 一个提交应该尽量表达一个完整的小改动, 例如 "Fix: 修复参数解析问题". 不建议把很多无关修改混在同一个提交里, 否则后续回看历史和定位问题都会变困难.

git commit -m "Add user config parser"

如果你在提交完后发现提交信息写错了, 或者提交内容不完整(漏提交一些文件了), 可以使用 git commit --amend 来修改最近一次提交. 这个命令会把暂存区的内容重新打包成一个新的提交, 替换掉之前的提交. 但需要注意 --amend 会重新计算提交的 Hash, 因此尽量在推送到远程仓库之前使用, 避免影响其他人看到的历史.

git add missing_file.cpp
git commit --amend

如果只想修改最近一次提交信息:

git commit --amend -m "Fix config parser edge case"

查看提交历史可以使用:

git log # 查看提交历史, 包括提交的message, 作者, 日期等信息
git log --oneline --graph --decorate # 以简洁的方式查看提交历史, 包括分支和合并信息

查看具体改动可以使用:

git diff          # 查看工作区中尚未暂存的改动
git diff --staged # 查看已经进入暂存区的改动
git diff /path/to/file # 查看某个文件的改动(精确到行)
git diff HEAD~1 HEAD # 查看最近两次提交之间的所有改动(精确到行)
git diff --stat HEAD~5 HEAD # 查看最近5次提交之间的改动统计信息(精确到文件)

查看某一次提交的完整信息可以使用:

git show <commit-hash>
git show --stat <commit-hash>

所以一个最小的完整工作流通常是:

git status # 确认当前状态
git diff # 确认自己到底改了什么
git add <file> # 把改动放入暂存区
git diff --staged # 确认下一次提交会包含什么
git commit -m "Describe the change" # 提交到本地仓库

分支, 合并与冲突

分支 (Branch) 可以理解成一条独立的开发线. 你可以在一个新分支上实现功能, 同时不影响主分支上的稳定代码.

git branch                 # 查看本地分支
git branch -a              # 查看本地分支和远程跟踪分支
git switch -c feature-x    # 创建并切换到新分支
git switch main            # 切换回 main 分支
git switch -               # 切回上一个分支

当一个分支上的工作完成后, 可以把它合并回另一个分支:

git switch main
git merge feature-x

如果 main 从创建 feature-x 之后没有新的提交, 这次合并通常会是 Fast-forward, 也就是 Git 只需要把 main 指针向前移动到 feature-x 的位置. 如果两边都有新提交, Git 会创建一个 merge commit, 表示两条历史线在这里汇合.

下面的图只画提交历史, 不画工作区和暂存区. 图中的 A, B, C, D, E 都表示提交, 分支名指向这条分支当前最新的提交. 为了让 Mermaid 更容易渲染, 图中用 feature 代表命令里的 feature-x.

git merge feature-x 前, main 和 feature-x 从 B 之后分开开发:

Rendering diagram...

在 main 上执行 git merge feature-x 后, Git 会在 main 上创建一个新的合并提交 M. M 同时连接 C 和 E, 表示两条历史线在这里汇合:

Rendering diagram...

如果是 Fast-forward merge, 则不会产生新的 merge commit. 例如 main 停在 B, feature-x 已经走到 E, 合并时 Git 只需要把 main 从 B 移到 E. 这种情况下历史仍然是一条直线.

Fast-forward merge 前:

Rendering diagram...

Fast-forward merge 后:

Rendering diagram...

也就是说, Fast-forward merge 改变的是分支指针位置, 而不是新增一个提交.

可以用下面的命令观察分支关系:

git log --oneline --graph --decorate --all

如果两个分支修改了同一个文件的同一段内容, Git 无法自动判断应该保留哪一版, 就会产生冲突 (Conflict). 冲突文件里通常会出现类似这样的标记:

<<<<<<< HEAD
current branch content
=======
incoming branch content
>>>>>>> feature-x

解决冲突的过程是:

  1. 打开冲突文件, 手动改成最终想要的内容.
  2. 删除 <<<<<<<, =======, >>>>>>> 这些冲突标记.
  3. 使用 git add <file> 标记冲突已经解决.
  4. 继续完成合并提交.
git add <file>
git commit

很多图形化 Git 工具和 IDE 都提供冲突解决界面, 但它们背后的本质仍然是手动选择最终文件内容.

需要注意的是, 在尝试切换分支的时候, 如果当前工作区有未提交的改动, Git 可能会拒绝切换, 因为新分支上的代码可能和当前改动冲突. 这时你可以选择先提交改动, 或者使用 git stash 临时保存改动, 切换分支后再 git stash pop 恢复改动.

冲突是 Git 里面很常见的情况, 因此不必害怕遇到冲突.

功能分支合并完成后, 如果已经不再需要, 可以删除本地分支:

git branch -d feature-x

如果 Git 提示分支还没有合并, 说明直接删除可能丢失提交. 这时应该先确认历史, 不要急着使用强制删除.

临时保存改动: Stash

git stash 用来临时保存工作区和暂存区里的改动, 让工作区回到干净状态. 它适合这种场景: 你正在写一半功能, 突然需要切到另一个分支修紧急 bug, 但当前改动还不适合提交.

最常用的流程是:

git status
git stash push -m "wip: config parser"
git switch main
# do something else
git switch feature-x
git stash pop

git stash push 会把当前改动收起来. git stash pop 会恢复最近一次 stash, 并且在恢复成功后从 stash 列表里删除它.

如果 git stash pop 过程中发生冲突, Git 会保留这条 stash 记录, 避免恢复失败时内容丢失. 你需要像解决普通冲突一样修改文件, 然后 git add 标记解决.

查看已有 stash:

git stash list

输出类似:

stash@{0}: On feature-x: wip: config parser
stash@{1}: On main: temporary debug logs

如果不想恢复后立刻删除 stash, 可以使用 apply:

git stash apply stash@{0}

如果确认某个 stash 不再需要:

git stash drop stash@{0}

默认情况下, git stash 不会保存未被 Git 追踪的新文件. 如果需要连新文件一起保存, 使用:

git stash push -u -m "wip with new files"

stash 很方便, 但它不是长期保存工作的地方. 如果一个改动已经有明确意义, 更推荐提交到一个临时分支上. 提交比 stash 更容易查看, 也更不容易忘.

变基

变基 (Rebase) 的作用是把一串提交挪到另一个基点后面. 如果说 merge 是把两条开发线汇合, 那么 rebase 更像是把当前分支的提交重新排到目标分支之后.

常见用法是让当前功能分支基于最新的 main:

git switch feature-x
git fetch origin
git rebase origin/main

这样做之后, feature-x 上的提交会像是从最新的 main 之后开始开发的一样, 历史会更线性.

一个简单例子是, rebase 前 feature-x 从 B 分出来, 而 main 后来又新增了 C:

Rendering diagram...

在 feature-x 上执行 git rebase main 后, Git 会把 D 和 E 的改动重新应用到 C 后面, 形成新的提交 D' 和 E':

Rendering diagram...

这里的 D' 和 E' 不是原来的 D 和 E, 而是 Git 重新应用修改后生成的新提交, 因此 Hash 会变化.

需要注意的是, rebase 会重写提交历史. 因此一个简单规则是: 只 rebase 自己本地还没有共享给别人的分支. 如果一个分支已经被多人基于它开发, 随意 rebase 会让其他人的历史对不上.

rebase 过程中也可能遇到冲突. 解决方式和 merge 冲突类似: 修改文件, git add, 然后继续 rebase.

git add <file>
git rebase --continue

如果发现这次 rebase 不应该继续, 可以回到 rebase 前:

git rebase --abort

交互式变基可以用来整理本地提交, 例如合并多个临时提交, 修改提交信息:

git rebase -i HEAD~3

这表示整理最近 3 个提交. 初学时不需要频繁使用交互式变基, 但理解它能帮助你在提交 PR 前整理出更干净的历史.

推送, 拉取与远程仓库

远程仓库通常叫 origin, 它是本地仓库对应的远程地址. 克隆一个项目时, Git 会自动配置好 origin.

git clone https://github.com/user/project.git
# 将 user/project 克隆到 my-project 目录, 并且只克隆最近一次提交的历史
git clone https://github.com/user/project.git my-project --depth=1
git remote -v

git remote -v 会显示远程仓库地址, 通常能看到 origin 的 fetch 和 push 地址. origin 只是默认名字, 不是特殊魔法. 如果你愿意, 也可以把远程仓库命名成别的名字, 只是大多数项目都沿用 origin.

把本地提交推送到远程:

git push origin main

第一次推送一个新分支时, 通常会设置 upstream, 之后可以直接使用 git push:

git push -u origin feature-x

从远程获取更新有两个常用命令:

git fetch origin
git pull

git fetch 只把远程的新提交下载到本地, 不会自动修改当前分支. 它更像是先看看远程发生了什么.

git pull 通常等价于 git fetch 加上一次 merge, 会把远程分支的变化合并到当前分支. 有些团队会配置 git pull --rebase, 让拉取时使用 rebase 而不是 merge. 团队协作时应该和项目习惯保持一致.

fetch 之后, 可以比较本地分支和远程跟踪分支:

git fetch origin
git log --oneline HEAD..origin/main      # 远程有, 当前分支还没有的提交
git log --oneline origin/main..HEAD      # 当前分支有, 远程还没有的提交
git diff HEAD..origin/main               # 两边文件内容的差异

如果你只想取远程某个分支:

git fetch origin main

这次 fetch 的结果会记录在 FETCH_HEAD 中. 如果使用普通的 git fetch origin, Git 通常还会按照远程配置更新 origin/main 这类远程跟踪分支. 所以有时你会看到:

git merge FETCH_HEAD

这表示把刚刚 fetch 到的提交合进当前分支. 不过日常使用中, 写 git merge origin/main 通常更直观.

多人协作中推荐的基本习惯是:

git fetch origin
git switch main
git pull
git switch -c feature-x

先从最新主分支开始工作, 可以减少后续冲突.

查看本地分支和 upstream 的对应关系:

git branch -vv

如果某个本地分支还没有设置 upstream, 可以使用:

git branch --set-upstream-to=origin/feature-x feature-x

回退

Git 的回退命令很多, 初学者最容易混淆. 可以先按用途区分:

  • git restore: 丢弃工作区或暂存区中的未提交改动.
  • git reset: 移动当前分支指针, 常用于撤销本地提交或取消暂存.
  • git revert: 新建一个反向提交, 用来撤销已经发布的历史提交.

丢弃某个文件在工作区里的修改:

git restore <file>

丢弃所有工作区修改:

git restore .

把已经 git add 的文件从暂存区拿出来, 但保留工作区修改:

git restore --staged <file>

撤销最近一次本地提交, 但保留文件改动:

git reset --soft HEAD~1

无论使用 --soft, 默认的 --mixed, 还是 --hard, reset 在提交图上的核心变化都是移动当前分支指针. 它们的区别在于暂存区和工作区如何处理被撤销出来的改动. 例如 reset 前 main 和 HEAD 都指向 C:

Rendering diagram...

执行 git reset HEAD~1 或 git reset --soft HEAD~1 后, main 和 HEAD 回到 B. C 不再属于 main 的正常历史, 但刚操作完时通常还能通过 Reflog 找回:

Rendering diagram...

撤销最近一次本地提交, 并把改动退回工作区:

git reset HEAD~1

git reset 默认是 mixed 模式, 也就是移动分支指针, 并把撤销出来的内容放回工作区. 因此下面两条命令效果相近:

git reset HEAD~1
git reset --mixed HEAD~1

如果提交已经推送到了远程, 并且可能被别人拉取了, 通常不要用 reset 重写历史, 而是用 revert:

git revert <commit-hash>

revert 会生成一个新的提交, 内容正好抵消目标提交. 它不会删除历史, 因此更适合公共分支.

例如要撤销 C, revert 不会让分支退回 B, 而是在 C 后面新增一个反向提交 R:

Rendering diagram...

这里 R 的内容效果是抵消 C, 但历史上仍然能看到 C 曾经存在. 这也是公共分支上优先使用 revert 的原因.

git reset --hard 会同时丢弃提交和工作区修改. 这个命令很有用, 但也很危险, 运行前应该确认没有需要保留的未提交改动.

还有一个容易误用的命令是 git clean, 它会删除未被 Git 追踪的文件. 可以先预览:

git clean -n

确认后再删除:

git clean -fd

对于初学者, 回退前建议先运行 git status 和 git diff, 确认自己要丢掉的是哪一部分. 如果不确定, 可以先新建一个临时分支或者使用 git stash push -u 保存现场.

救命稻草: Reflog

Reflog 记录了本地 HEAD 和分支指针移动过的位置. 即使你做过 reset, rebase, 切换分支等操作, 很多时候仍然可以通过 Reflog 找回之前的位置.

查看 Reflog:

git reflog

输出中会看到类似:

abc1234 HEAD@{0}: reset: moving to HEAD~1
def5678 HEAD@{1}: commit: Add user config parser

如果你误删了一个本地提交, 可以先在 git reflog 里找到它的 commit hash, 然后创建一个分支把它保住:

git switch -c rescue def5678

也可以把当前分支回到那个位置:

git reset --hard def5678

Reflog 是本地记录, 不会自动同步到远程仓库. 它适合用来救自己机器上的误操作, 不能当成团队共享的备份机制.

HEAD@{0}, HEAD@{1} 这种写法表示 HEAD 的历史位置. HEAD@{0} 是当前位置, HEAD@{1} 是上一次位置. 所以下面这个命令可以查看上一次位置对应的提交:

git show HEAD@{1}

Reflog 不是永久保险箱, Git 会在一段时间后清理过旧记录. 但对于刚发生的误操作, 它通常非常有用.

子模块

子模块 (Submodule) 用来在一个 Git 仓库中引用另一个 Git 仓库. 它常见于项目依赖某个外部库, 并且希望固定到那个外部库的某个具体提交.

添加子模块:

git submodule add https://github.com/user/lib.git third_party/lib
git commit -m "Add lib submodule"

克隆带子模块的项目时, 需要初始化并拉取子模块内容:

git clone https://github.com/user/project.git
cd project
git submodule update --init --recursive

如果已经克隆了项目, 后来发现子模块目录是空的, 通常也是运行:

git submodule update --init --recursive

查看子模块状态:

git submodule status

子模块的关键点是: 主仓库记录的不是子模块的分支名, 而是子模块当前指向的具体 commit. 因此你在子模块目录里更新到新提交后, 还需要回到主仓库提交这个指针变化.

cd third_party/lib
git pull
cd ../..
git status
git add third_party/lib
git commit -m "Update lib submodule"

如果想让所有子模块更新到它们远程分支上的最新提交, 可以使用:

git submodule update --remote --merge

但这会改变子模块指向的 commit, 所以之后仍然需要在主仓库里提交这个变化.

子模块适合固定外部依赖版本, 但会增加使用复杂度. 如果只是普通语言包依赖, 通常优先使用对应生态的包管理器.

.gitignore

.gitignore 文件用来告诉 Git 哪些文件不应该被追踪. 例如编译生成的二进制文件, 临时日志, IDE 配置文件等通常不需要提交到仓库. .gitignore 的语法很简单, 每行一个模式. 例如:

# 忽略所有 .log 文件
*.log
# 忽略 build/ 目录下的所有文件
build/
# 忽略根目录下的 secret.txt 文件
/secret.txt
# 忽略所有 .DS_Store 文件, 包括子目录
.DS_Store

需要注意的是, .gitignore 只能忽略未被 Git 追踪的文件. 如果一个文件已经被 git add 过了, 后来再添加到 .gitignore 是不会生效的. 这种情况下需要先把它从 Git 追踪中移除:

git rm --cached <file>

大多数情况下, 你的编译产物, 临时文件, IDE 配置, 各种各样的中间文件都应该被添加到 .gitignore 里, 避免它们被误提交到仓库. 这不仅能保持仓库的整洁, 也能避免泄露敏感信息. 理想情况下, 仓库应该只包含最小复现项目所需的源代码和资源, 以及相关的文档和配置文件.

当你想要传大文件: Git LFS

Git 不适合直接管理大型二进制文件, 例如模型权重, 视频, 大型图片素材, 数据集压缩包等. 因为 Git 会保存历史版本, 大文件一旦提交进历史, 仓库体积会很快膨胀.

Git LFS (Large File Storage) 的思路是: Git 仓库里只保存一个很小的指针文件, 真正的大文件由 LFS 存储系统管理.

基本使用方式:

git lfs install
git lfs track "*.psd"
git lfs track "*.zip"
git add .gitattributes
git add assets/example.psd
git commit -m "Add large asset with LFS"

.gitattributes 会记录哪些文件类型由 LFS 管理, 所以它也需要提交到仓库.

查看当前仓库里哪些文件由 LFS 管理:

git lfs ls-files

克隆使用 LFS 的仓库时, 通常 Git LFS 会自动下载对应的大文件. 如果没有自动下载, 可以手动运行:

git lfs pull

需要注意的是, GitHub 等平台对 LFS 存储和带宽通常有限额. 大型数据集或模型文件如果非常大, 更适合放到专门的对象存储或数据发布平台上, 然后在仓库中保存下载脚本和校验信息.

另一个常见坑是: 如果大文件已经作为普通 Git 文件提交进历史, 后来再 git lfs track 并不会自动清理旧历史. 这种情况需要额外迁移历史, 操作成本更高. 因此最好在第一次提交大文件之前就决定是否使用 LFS.

进阶: GitHub CI/CD流水线

CI/CD 是基于 Git 的自动化流程.

  • CI (Continuous Integration): 持续集成, 常见任务是自动编译, 运行测试, 检查格式.
  • CD (Continuous Delivery / Deployment): 持续交付或部署, 常见任务是自动发布包, 部署网站, 更新服务器.

在 GitHub 上, CI/CD 通常由 GitHub Actions 实现. 工作流文件放在 .github/workflows/ 目录下, 使用 YAML 描述.

一个非常简单的 CI 例子:

name: CI

on:
  push:
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 22
      - name: Install dependencies
        run: npm ci
      - name: Run tests
        run: npm test

这段配置表示: 当有人 push 或创建 Pull Request 时, GitHub 会启动一台临时的 Ubuntu 机器, 拉取代码, 安装依赖, 然后运行测试.

这个例子假设项目是 Node.js 项目, 并且仓库里有 package-lock.json, 所以可以使用 npm ci. 如果是 C++ 项目, 对应步骤可能会变成安装编译器, 配置 CMake, 然后运行测试:

      - name: Configure
        run: cmake -S . -B build
      - name: Build
        run: cmake --build build
      - name: Test
        run: ctest --test-dir build --output-on-failure

对于初学者来说, CI/CD 最重要的价值是把重复检查自动化. 只要测试和构建流程写进流水线, 每次提交和 PR 都会自动验证, 不需要依赖人工记忆.

实际项目中还会经常用到:

  • Status Check: PR 页面上显示测试是否通过.
  • Secrets: 保存部署密钥, Token 等敏感信息, 避免写进代码仓库.
  • Matrix: 在多个系统或多个语言版本上并行测试.
  • Artifacts: 保存构建产物, 日志, 覆盖率报告等文件.

CI/CD 本身不是 Git 的核心功能, 但它建立在 Git 提交和分支协作之上. 当项目稍微变大后, 它会成为保证代码质量和发布稳定性的基础工具.