Git 版本控制系统详解

Git 是全球最流行的分布式版本控制系统,由 Linus Torvalds 于 2005 年为 Linux 内核开发而创建。它将数据视为快照流(stream of snapshots),而非基于文件的增量差异(delta),几乎所有操作均为本地执行,速度极快。

1. 概述

1.1 Git 是什么

Git 本质上是一个内容寻址文件系统(content-addressable filesystem),其核心是一个简单的键值(key-value)数据库。任何内容都可以通过 git hash-object 存入 Git,Git 会返回一个 40 字符的 SHA-1 哈希作为唯一键。

核心特性:

1.2 三种状态与三个区域

Git 中文件有三种状态——已修改(modified)、已暂存(staged)、已提交(committed),对应三个工作区域:

区域说明
工作目录(Working Tree)从 Git 目录解压出来的某个版本的单一检出,放在磁盘上供使用和修改
暂存区(Staging Area / Index)存储在 .git/index,记录下一次提交的内容信息
Git 目录(Repository / .git)存储项目的元数据和对象数据库,clone 时复制过来的核心部分

基本工作流:修改文件 → 暂存变更 → 提交快照。

1.3 Git 与 SVN/CVS 的核心差异

传统集中式 VCS(如 SVN、CVS)以文件差异列表(delta-based)的方式存储数据:记录每个文件随时间变化的增量。Git 则将数据视为小型文件系统的快照流:每次提交时对整个项目拍照,未变化的文件不重复存储,仅存一条链接。

2. 行为图(系统"做什么")

2.1 用例图 — Git 用户交互全景

Git Use Case Diagram
图 1:Git 用例图

2.2 活动图 — 基本工作流

Git Activity Diagram
图 2:Git 基本工作流活动图(带泳道)

2.3 状态机图 — 文件生命周期

Git 文件中文件在不同区域之间流转,构成完整的状态机:

Git File Lifecycle State Machine
图 3:文件生命周期状态机图

2.4 序列图 — 提交过程

当用户执行 git commit 时,Git 内部如何创建 blob、tree、commit 三种对象:

Git Commit Process Sequence Diagram
图 4:git commit 内部序列图

3. 结构图(系统"由什么组成")

3.1 组件图 — 核心架构

Git 内部由四种核心对象类型(blob、tree、commit、tag)和引用系统(refs、HEAD)组成,协同工作构成完整版本控制系统:

Git Internal Architecture Component Diagram
图 5:Git 内部架构组件图

3.2 对象模型

Git 底层四种对象类型及其关系:

对象类型存储内容对应概念
blob文件内容(不含文件名)文件内容 / inode
tree目录条目列表:文件名 + blob/tree 引用 + 模式目录
committree 引用、父 commit、作者、提交者、时间戳、消息快照
tag对象引用、标签类型、标签名、标签者、消息(仅 annotated)不可移动的分支引用
Git Object Model
图 6:Git 对象模型关系图

对象存储机制:每个对象存储时,Git 会构造一个 header(格式:"对象类型 内容长度\0"),与内容拼接后计算 SHA-1 哈希,再经 zlib 压缩写入 .git/objects/<前2字符>/<后38字符>。

3.3 部署图 — 分布式拓扑

Git 是分布式系统,多个仓库通过推拉操作实现同步:

Git Distributed Topology Deployment Diagram
图 7:Git 分布式协作部署图

4. 实战速查手册

4.1 仓库操作

命令说明
git init初始化新仓库,创建 .git 目录
git clone <url>克隆远程仓库到本地,完整复制对象数据库
git remote add <name> <url>添加远程仓库引用

4.2 基本快照

命令说明
git status查看工作区 & 暂存区状态
git diff查看工作区相对于暂存区的差异
git diff --staged查看暂存区相对于最新 commit 的差异
git add <file>将修改添加到暂存区(计算 blob 哈希加入 index)
git commit -m "msg"将暂存区内容创建为新的 commit 对象
git rm <file>从工作区和暂存区删除文件
git mv <old> <new>移动/重命名文件

4.3 分支与合并

命令说明
git branch列出本地分支
git branch <name>创建新分支(在 .git/refs/heads/<name> 写入 commit SHA-1)
git checkout <branch>切换分支(更新 HEAD 和工作目录)
git merge <branch>将指定分支合并到当前分支
git rebase <branch>将当前分支变基到目标分支
git cherry-pick <commit>将指定 commit 应用到当前分支
git stash暂存当前工作区修改

4.4 远程协作

命令说明
git fetch <remote>从远程拉取对象和引用(不合并)
git pull <remote> <branch>fetch + merge(或 rebase)
git push <remote> <branch>推送本地 commit 到远程
git push --tags推送所有标签到远程

4.5 历史与追溯

命令说明
git log查看 commit 历史
git log --oneline --graph --all简洁图形化全分支历史
git blame <file>逐行显示文件修改作者和 commit
git bisect二分查找引入 bug 的 commit
git tag -a v1.0 -m "msg"创建带注释的标签(含 tag 对象)

4.6 撤销与修复

命令说明
git reset HEAD <file>从暂存区移除文件
git checkout -- <file>丢弃工作区的修改
git reset --soft HEAD~1撤销最近一次 commit,保留暂存
git reset --mixed HEAD~1撤销 commit + 暂存,保留工作区
git reset --hard HEAD~1完全撤销 commit、暂存和工作区(危险)
git revert <commit>生成一个新 commit 反向操作指定 commit
git reflog查看 HEAD 和分支引用的变化记录

4.7 底层对象检查 (Plumbing)

命令说明
git cat-file -t <sha1>查看 Git 对象的类型(blob/tree/commit/tag)
git cat-file -p <sha1>打印 Git 对象的内容
git hash-object -w <file>将文件内容写入对象数据库,返回 SHA-1
git update-index --add --cacheinfo 100644 <sha1> <file>手动向 index 添加条目
git write-tree根据 index 内容创建 tree 对象
git commit-tree <tree-sha> -p <parent-sha>手动创建 commit 对象
git update-ref refs/heads/<branch> <sha1>手动更新分支引用
git gc垃圾回收,生成 packfile 优化存储

4.8 分支工作流示例

GitHub Flow

git checkout -b feature/xxx       # 从 main 创建功能分支
# ... 开发 & 提交 ...
git push origin feature/xxx       # 推送功能分支
# 在 GitHub 上创建 Pull Request
# 审查通过后合并到 main
git checkout main
git pull origin main              # 拉取最新 main
git branch -d feature/xxx         # 删除本地功能分支

Git Flow(适用于有版本发布周期的项目)

# 主分支: main (生产) + develop (开发)
git checkout -b release/1.0 develop  # 创建发布分支
# ... 修复 bug ...
git checkout main
git merge --no-ff release/1.0     # 合并到主分支
git tag -a v1.0 -m "Release 1.0"  # 打版本标签
git checkout develop
git merge --no-ff release/1.0     # 合并回开发分支
git branch -d release/1.0

4.9 回滚到指定 commit

当需要将分支回滚到某个历史 commit 并推送到远程时,有三种方案,按安全性从低到高排列:

方案一:reset --hard + force push(最直接)

git reset --hard <commit-sha>
git push --force origin <branch>

原理:直接将 HEAD 和分支引用移动到目标 commit,然后 force push 覆盖远程。

适用场景:个人仓库、feature 分支,无其他协作者依赖被丢弃的 commit。
风险:被丢弃的 commit 从远程消失,若他人已基于这些 commit 开发,会导致冲突。

方案二:git revert(安全,不改写历史)

# 假设当前在 9b58a06,目标回滚到 6eef430
# 6eef430 之前有 2 个 commit 需要撤销
git revert --no-commit HEAD~1..HEAD
git commit -m "Revert to <commit-sha>"
git push origin <branch>

原理:生成一个新的 revert commit,反向操作目标 commit 之后的所有变更,不修改已有历史。

适用场景:多人协作的共享分支(如 main/master),不能 force push。
缺点:历史中多出一个 revert commit,不如方案一干净。

方案三:备份分支 + reset + force push(推荐)

# 1. 备份当前状态
git branch backup-<branch>

# 2. 硬回滚
git reset --hard <commit-sha>

# 3. 强制推送
git push --force origin <branch>

原理:在方案一的基础上先创建备份分支,保留当前 HEAD 的引用(仅本地),万一需要恢复可以随时切回。

恢复方法:git reset --hard backup-<branch> && git push --force origin <branch>
适用场景:个人仓库的首选方案,兼顾干净历史和可恢复性。

三种方案对比

方案改写历史需 force push可恢复性适合场景
reset + force是是仅本地 reflog个人仓库
git revert否否天然保留共享分支
备份 + reset + force是是备份分支个人仓库(推荐)

4.10 git rebase 详解

git rebase 将一个分支的提交记录按顺序重放到目标分支的顶端,从而获得一条线性、整洁的历史。它本质上是"复制"提交——每个提交都会基于新基底重新生成,得到全新的 SHA-1 哈希,因此属于改写历史的操作。

4.10.1 rebase 与 merge 对比

对比项git rebasegit merge
历史形状线性(无分叉节点)存在分叉 + 合并节点(merge commit)
改写历史是(重放产生新 commit)否(新增 merge commit,原提交不变)
冲突处理逐个提交重放,冲突逐次出现一次合并,冲突集中处理
安全性不适用已推送/共享分支可安全用于共享分支
可追溯性丢失"何时、为何合并"的节点merge commit 保留合并上下文
Merge vs Rebase Comparison
图 8:git merge 与 git rebase 对比

4.10.2 基本用法

git rebase <branch>                 # 将当前分支的提交重放到 <branch> 顶端
git rebase origin/main              # 同步远程最新,常用在 push 被拒后
git pull --rebase origin main       # fetch + rebase(等价于上面两步)
git rebase -i HEAD~3                # 交互式重写最近 3 个提交
git rebase --continue               # 解决冲突后继续
git rebase --skip                   # 跳过当前提交(放弃该提交)
git rebase --abort                  # 放弃整个 rebase,回到操作前状态

典型场景:本地 push 被拒绝(远程有新提交)时,用 git rebase origin/main 将本地提交重放到远程最新提交之上,再 push,避免产生多余的 merge commit。

4.10.3 交互式 rebase(rebase -i)

git rebase -i <base> 打开一个编辑列表,每行是一个待重放的提交,可通过修改命令词来改写历史:

命令缩写作用
pickp保留该提交(默认)
rewordr保留提交,但修改提交信息
edite停在该提交,允许修改内容后继续
squashs将该提交合并到上一个提交(合并提交信息)
fixupf合并到上一个提交(丢弃本提交信息)
dropd删除该提交
execx在指定位置运行 shell 命令
git rebase -i HEAD~3   # 示例:合并最近 3 个提交为 1 个
# 编辑器中调整:
#   pick  1a2b3c feat: 功能 A
#   fixup 4d5e6f 补丁修正
#   fixup 7g8h9i 格式调整
# 保存退出后,三个提交合并为一个
注意:rebase -i 的编辑列表从上到下是旧 → 新(最上面是最老的提交)。squash/fixup 只能把当前提交并入其上方的提交。

4.10.4 冲突处理

rebase 是逐提交重放,冲突会多次出现。处理流程:

git rebase origin/main       # 触发冲突
git status                   # 查看冲突文件(<<<<<<< HEAD ======= >>>>>>> 标记)
# 手动编辑冲突文件,保留正确内容
git add <file>              # 标记已解决
git rebase --continue        # 继续下一个提交
# 反复直到全部重放完成;想放弃则: git rebase --abort
与 merge 冲突的区别:merge 只需解决一次;rebase 可能每个被重放的提交都触发冲突。git rebase --abort 可随时安全退回操作前状态(rebase 前 Git 已记录 ORIG_HEAD)。

4.10.5 黄金法则与风险

黄金法则:永远不要 rebase 已经推送到共享分支的提交。
rebase 会改写 SHA-1,使其他协作者基于旧提交的工作与你的历史分叉,导致混乱。只对尚未推送的本地提交执行 rebase。

4.11 git pull 原理与冲突详解

4.11.1 pull 的内部原理

git pull 是一个组合命令,它从来不是单一操作,而是 fetch + merge 两个步骤的快捷方式(加 --rebase 则等价于 fetch + rebase):

git pull origin main            # 等价于:git fetch origin && git merge origin/main
git pull --rebase origin main   # 等价于:git fetch origin && git rebase origin/main
步骤命令做了什么是否改动工作区
第一步git fetch从远程下载缺失的对象,更新 refs/remotes/origin/<branch> 引用否(只拉数据)
第二步git merge把远程分支合并进当前本地分支是
第二步(--rebase)git rebase把本地未推送的提交重放到远程分支顶端是
要点:fetch 只做"下载",永远安全;真正的"改变工作区"发生在第二步 merge/rebase。若只想看远程更新而不改动本地,用 git fetch 而非 git pull。

4.11.2 为何会冲突:三方合并

Git 合并时不是简单的"逐行覆盖",而是采用三方合并(three-way merge),需要三个输入:

merge base(共同祖先) + 我方提交(HEAD) + 对方提交(origin/main)
# 对比三方后,Git 自动合并双方互不影响的改动;
# 只有三方中"我方和对方都改了同一处",才产生冲突
文件行状态共同祖先我方 HEAD对方 origin/mainGit 行为
仅一方修改原内容改了未改自动采用该方的修改
双方改不同行原内容改了 A 处改了 B 处自动合并两处改动
双方改同一行原内容改了改了(不同内容)冲突,需人工解决
一方删文件、一方改文件存在删除修改冲突,需人工决定
冲突的本质:只有当双方在合并点都修改了同一文件的同一区域(或一方删、一方改)时才会冲突。修改不同文件、或同一文件不同区域,Git 都能自动合并。冲突是同一行"都要改"的问题,与代码多少无关。

4.11.3 冲突如何解决

冲突后 Git 暂停合并,在冲突文件中写入标记:

<<<<<<< HEAD          ← 上方是我方版本
你写的代码
=======                  ← 分隔线
别人写的代码             ← 下方是对方版本
>>>>>>>> origin/main

解决流程:

git status                  # 查看哪些文件冲突(Unmerged paths)
# 手动编辑冲突文件:保留正确内容,删除 <<<<<<< ======= >>>>>>>> 标记行
git add <file>              # 标记为已解决
git merge --continue        # merge 时:完成合并提交
git rebase --continue       # rebase 时:继续重放下一个提交
# 想放弃本次合并,回到操作前状态:
git merge --abort           # 或 git rebase --abort
提示:多人协作应多 git pull(保持同步)、少长时间停留在同一文件,可显著降低冲突概率。共享分支冲突用 merge 处理;本地未推送的提交冲突用 rebase 处理。

4.12 git worktree 原理详解

git worktree 让同一个仓库同时把多个分支检出到不同目录。一个仓库有 1 个主工作树(main worktree,即 git init/git clone 生成的那个),外加 0 到多个链接工作树(linked worktree)。所有工作树共享同一份对象库和 refs,但各有独立的 HEAD 和 index。

4.12.1 为什么需要 worktree

经典场景:你正在 feature 分支做一半重构,工作区一堆半成品,此时线上出现紧急 bug 需要立刻在 main 上修复。传统做法是 git stash 暂存改动再切分支,但工作区可能乱到不敢动;而 worktree 的解法是——另开一个目录检出 main,改完直接删掉目录,原来的工作区原封不动。

相比 git clone 多份仓库,worktree 的优势是:零拷贝对象库、共享分支命名空间,磁盘占用和"仓库间同步"成本都更低。

一个容易误解的点:worktree 复制的是「工作区文件」,不是「仓库本体」。每个工作树目录里都是一份完整的源码检出,所以磁盘上源码确实有 N 份;但 .git/objects 与 refs 只有一份,被所有工作树共享。这正是它和"切换分支"、clone 的本质区别:相比切换分支(工作区始终 1 份)它更贵——工作区从 1 份变 N 份;相比 clone(对象库也翻倍)它更省——对象库不复制。

4.12.2 存储原理(核心)

链接工作树的顶层是一个普通目录,但其中的 .git 是一个文件而非目录,内容只有一行:

gitdir: /path/main/.git/worktrees/<id>

主仓库的 .git/worktrees/<id>/ 目录则保存该工作树的私有管理文件。进入链接工作树后:

内容共享(common)还是每树私有(per-worktree)
objects/ 对象数据库共享——所有工作树看到同一份历史
refs/(heads、tags、remotes)共享——同名分支在所有树中指向同一个 commit
config共享(开启 extensions.worktreeConfig 后可用每树 config.worktree)
HEAD(伪引用)每树私有——各工作树可处于不同分支
index 暂存区每树私有——各自独立的暂存状态
refs/bisect、refs/worktree、refs/rewritten每树私有(refs 共享规则的例外)
Git Worktree Structure
图 9:git worktree 存储结构与元数据关联

关键推论:因为 refs/heads 共享、分支是"某条 ref 指向的 commit"这一全局状态,所以同一个分支不能被两个工作树同时检出(否则两处的提交会互相打乱该分支的指向)。Git 默认拒绝,除非显式 --force。

Git Worktree vs Clone
图 10:git worktree 与 git clone 多副本对比

4.12.3 常用命令

命令说明
git worktree add <path>在 path 新建链接工作树,自动创建并检出与目录同名的新分支
git worktree add <path> <branch>在 path 检出已存在的分支
git worktree add -b <new> <path> <base>基于 base 新建分支 new 并检出到 path
git worktree add -d <path>创建 detached HEAD 的临时工作树(试验/测试用)
git worktree list列出所有工作树;-v 显示锁定/可清理原因,--porcelain 供脚本解析
git worktree move <wt> <new-path>移动工作树到新位置
git worktree remove <wt>删除工作树(仅限干净状态,强制用 -f)
git worktree lock <wt>锁定工作树,防止其管理文件被自动清理;可加 --reason
git worktree unlock <wt>解锁
git worktree prune清理工作目录已被手动删除的工作树的残留管理文件
git worktree repair [path]工作树被手动移动/仓库被搬走后,重建 gitdir 双向链接
# 紧急修 bug:不动当前工作区,另开目录在 main 上修
git worktree add -b emergency-fix ../repo-hotfix main
cd ../repo-hotfix
# ... 修改并提交 ...
git commit -am "emergency fix"
cd -
git worktree remove ../repo-hotfix        # 修完即删

# 查看所有工作树
git worktree list
# /repo             abcd1234 [main]
# /repo-hotfix      5678abc  [hotfix]
# /tmp/repo-test    1234abc  (detached HEAD)
# 并行开发:当前在 main,另开目录做 feature,两处同时推进
git worktree add -b feature/login ../repo-feature main   # 基于 main 新建分支并检出
cd ../repo-feature
# ...在 feature/login 上开发并提交...
git add -A && git commit -m "feat: login"
cd -                                                     # 回到主工作树(仍在 main)

# 方式一:本地合并(不需要 push,合并是独立的一步,push 从不自动合并)
git merge feature/login                                  # 在 main 工作树里把 feature 合进来
git push origin main                                     # 视需要再把合并结果推到远端

# 方式二:走远端 PR/MR(团队协作,多一道评审)
git push -u origin feature/login                         # 只上传 feature 分支,main 不受影响
# 在平台上开 PR 合并;合并后再本地更新:
# git fetch && git merge origin/main

# 收尾:删除工作树,再删已合并分支
git worktree remove ../repo-feature
git branch -d feature/login                              # 已合并才可 -d,否则用 -D

4.12.4 与 clone / stash 的对比

对比项git worktreegit clone(多副本)git stash(切换)
工作区文件N 份(各分支各一份完整检出)N 份(各自完整检出)始终 1 份(同一时刻只落地一个分支)
对象库共享同一份各自独立,磁盘翻倍同一份
同时编辑的文件多目录并存多目录并存只能一处
分支切换成本无需切换无需切换需 stash + checkout
多仓库间同步无需(共享 refs)需各自 fetch/push无需
适用场景同仓库并行多分支完全隔离的实验/CI临时打断

4.12.5 注意事项