git

1 版本控制系统演进与Git王者之路

夫以铜为镜,可以正衣冠;以史为镜,可以知兴替;以人为镜,可以明得失。《旧唐书·魏征传》

版本控制对于软件项目的团队协作至关重要,可显著提升效率。过去几十年中,有很多版本控制工具开发出来,为何git能脱颖而出成为当前的最佳实践?如何安装及设置​Git​。

1.1 版本控制

版本控制​是对​软件项目​中的​文件更新历史​进行记录,有了它,可以:

  • 将选定的文件回溯到之前的状态
  • 可以比较文件的变化细节,查出最后是谁修改了哪个地方,从而找出导致问题出现的原因
  • 查看是谁在何时报告了某个功能缺陷等

1.1.1 版本控制的重要性

不同于硬件开发或建筑工程,一旦施工,如果设计失误往往意味着高昂的返工甚至推倒重来,对前期设计的要求近乎苛刻。软件项目允许一定的失误,而版本控制在软件项目开发中具有不可替代的核心地位:

  • 支持多人高效协作:如linux内核开发,全球超千名开发者通过Git协作管理超3000万行代码
  • 保障项目安全与稳定性:生产环境问题可快速回滚至上一稳定版本

版本控制,可认为是软件开发的时空操纵术:

  • 空间维度:允许多双手在代码宇宙并行建造(Linux级协作)
  • 时间维度:赋予版本自由穿梭的能力(秒级回滚)
  • 进化本质:支持多人共造一辆行进中的原型车——在行驶中持续改造升级引擎,从自行车蜕变为法拉利

1.1.2 版本控制工具演进史

vcs_history.svg

Figure 1: VCS history

原始阶段​:手动对目录管理,复制整个项目目录来保存不同的版本,简单但容易出错,想象一下有成千上万个不同版本的目录,查找、比较、团队协作几乎不可能,文件混淆版本风险极高。

本地版本控制系统​(LVCS:= Local Version Control System):在单台计算机上管理文件的版本历史,通常采用“反向差异”存储(只存储版本间的差异),节省空间。仅支持本地操作,无法满足多人协作需求。典型代表是RCS, 于1982年由普渡大学的Walter F. Tichy首次发布,目前由GNU维护,最新发布是2022年。

集中化的版本控制系统​(CVCS:= Centralized VCS):通过引入一个单一的中央服务器存储项目的完整历史,允许多个开发者通过客户端连接到服务器取出最新的版本及提交更新,解决了多人协作的基本问题。但有几个显著缺点:

  • 单点故障:中央服务器宕机时,所有开发者无法提交更改,甚至可能无法查看历史。
  • 依赖网络:大部分操作需要网络连接。
  • 客户端限制: 开发者本地通常只保存文件的最新快照,完整历史需依赖服务器。

典型代表是SVN/Subversion,于2004年首次发布,目前由APACHE维护。

cvcs_arch.png

Figure 2: CVCS Arch

分布式版本控制系统​(DVCS:= Distributed VCS):通过分布式设计(每个开发者的本地仓库都是项目的完整镜像,包含所有文件的历史记录、分支和标签)进行了革命性变革:

  • 去中心化/高可用:无单点故障。即使中央服务器(常仍用于协调)宕机,本地工作与历史查询不受影响,提交可暂存本地。
  • 离线工作: 绝大多数操作(提交、分支、查看历史、差异比较)可在本地完成。
  • 高效灵活: 分支创建、合并操作通常非常快速和轻量。
  • 数据完整性: 使用内容寻址(如Git的SHA-1哈希)确保历史不可篡改。

典型代表有:

  • BitKeeper:第一个被广泛使用的DVCS,linux内核的开发曾在2002-2005使用bitkeeper管理版本,由于不是开源的,2005撤销了对开源社区的授权。Linus一怒之下带领一帮人开发设计了Git,从此git席卷世界。讽刺的是,BitKeeper在2016年选择了以Apache协议开源。有兴趣的可以看:git的前世,和BitKeeper
  • Git:由Linus Torvalds为Linux内核开发而创建(C语言实现)。凭借其强大功能、灵活性和Linux社区的巨大影响力,迅速成为最主流的DVCS。
  • Mercurial: 与Git同期诞生(Python语言实现)。设计哲学更强调简洁性和命令一致性,曾广受许多开发者喜爱并被不少公司采用,但市场份额逐渐被Git超越。
dvcs_arch.png

Figure 3: DVCS Arch

1.2 Git的诞生

Git的诞生与Linux内核开发的深度绑定,是开源工具与超大规模项目相互成就的经典范式。Linux 内核作为技术标杆,千万行代码量的版本管理压力、全球分布式开发者的高频协作需求、日均千次提交的工程迭代强度,直接锻造了Git的核心能力。

1.2.1 手动补丁管理

1991年-2002年间,Linux内核的版本维护主要依赖基于邮件列表的手动补丁管理,通过​diff​对比新旧代码生成补丁文件,审核通过后,通过​diff​进行补丁应用到本地源码,定期打包完整的内核源码。​diff/patch+邮件列表​的模式在早期支撑了Linux内核的雏形,但本质是“无版本控制”的临时方案。其低效与局限直接推动了BitKeeper的引入,进而为Git的诞生埋下伏笔。

1.2.2 BitKeeper: 商业化的DVCS

随着内核复杂度飙升,传统方式难以传统方式难以为继,分布式版本控制系统BitKeeper支持分布式开发,允许维护者独立管理子模块分支,显著提升合并效率;自动记录完整提交历史(如作者、时间),解决了补丁邮件的碎片化问题。2002年-2005年间,尽管社区对闭源的BitKeeper有争议,Torvalds坚持采用当时唯一满足高性能分布式协作的工具BitKeeper来管理和维护代码。

1.2.3 Git:开源的DVCS

2005年,开发BitKeeper的商业公司同Linux内核开源社区的合作关系结束,收回了Linux内核社区免费使用BitKeeper的权力。2005年 Linux 开源社区(特别是Linus Torvalds)基于使用 BitKeeper 时的经验教训,开发出自己的版本系统Git,从此Git开始席卷世界。

1.2.4 GitHub

最大程序员社区

GitHub是成千上万的开发者和项目能够合作进行的中心网站,本质是一个在线SaaS服务,提供 Git 仓库托管及扩展协作功能(通过Pull Requests完成代码审查与合并流程标准化,通过Issues完成任务跟踪与问题管理,通过Actions进行自动化CI/CD流水线),目前属于微软。尽管GitHub不是Git开源项目的直接部分,但如果想要专业地使用Git,将不可避免地与 GitHub打交道,同时GitHub也是一个宝藏,近十年火热的大数据、深度学习、大语言模型都离不开GitHub。

1.3 Git的设计思想

Git是分布式版本管理工具(DVCS,Linus的原文是: the stupid content tracker, 愚蠢的内容跟踪器)。核心设计要点如下:

1.3.1 分布式优先

  • 去中心化存储:每个开发者仓库都是完整的副本(含全部历史和分支),彻底消除单点故障风险。
  • 网络非必需:提交、分支、合并等核心操作均在本地完成,仅在同步时需网络(颠覆 CVCS 的集中式模型)。

1.3.2 基于快照的数据模型

内容寻址 (Content-Addressable)

  • 所有对象(文件/目录/提交)通过 SHA-1 哈希唯一标识(如 a5d8f324...),任何数据损坏均可被检测,相同文件内容仅存储一次(高效处理重复资源)。
  • 每次提交捕获整个项目的全量瞬时状态快照(而非增量差异),避免依赖历史链。git更像是一个文件系统,git逻辑上是基于不同时刻的全量快照来做版本管理。

    • 轻量级分支:创建一个新分支仅需要生成一个 41 字节的引用文件(如refs/heads/branch_name),这41个字节是一次提交的hash标识,本质上是一次整个项目的某个版本的全量快照,创建分支极快,鼓励开发人员频繁地创建和使用分支。
    • 分支切换、历史回溯高效极快,不需要在根据历史链回溯计算全量状态。

Note

Git直接记录快照,会不会导致存储空间浪费?

基础空间压缩机制:

  • 内容哈希化,相同内容必对应同一哈希值,天然避免冗余存储
  • 所有对象(Blob/Tree/Commit)均通过 zlib底层的DEFLATE 算法压缩存储于.git/objects。

但还是有一个挑战,大文件的微小修改:如修改16MB的源码的单行代码,会不会造成空间的明显浪费?

答案是通过增量优化机制,时间换空间。Git有两种存储格式:

  • 一种是松散对象(Loose Objects),独立存储每个文件的完整快照,​松散对象容忍临时冗余​。即使只修改一行代码,因为Hash值不同,也会生成一个新的对象,会导致空间浪费。
  • 第二种是包文件(PackFile)。Git会定期将松散对象(Loose Objects)打包为包文件 (Packefile),对相似文件进行Delta压缩:仅存储文件差异而非完整副本,显著减少空间占用。包文件自动重组相似内容(如代码的连续版本),通过增量算法优化存储效率。例如仅修改大文件中的少量字符时,新版本以原始格式存储,旧版本以差异形式存储(旧版本访问的先验概率小),可通过​git gc​手动触发。

本质上是空间和时间的交换,因为恢复历史版本的文件,需递归计算 Delta 链需要计算时间。

Git 通过​内容寻址防重 + Zlib 实时压缩 + Delta 差异打包​三重机制,将空间浪费转化为可接受的存储冗余。

1.3.3 时间切片机:三区机制与DAG

通过工作区暂存区版本库的设计,允许开发者精细化控制提交内容,将提交合并,分支形成DAG拓扑图,支持复杂开发流可视化。

git_wd_sa_repo.svg

Figure 4: git area

1.3.3.1 工作区

工作区(Working Directory) 的物理位置是项目根目录(除.git外所有文件),是用户直接编辑的源代码,是开发者可自由编辑的沙盒

1.3.3.2 暂存区

暂存区(Staging Area / Index)的物理位置是​.git/index​(二进制索引文件),是提交预览层,存储下一次提交的快照,作为缓冲区,隔离工作区版本库

1.3.3.3 版本库

版本库(.git Repository)的物理位置是​.git​目录,记录着每次提交记录,所有的提交记录是一张有向无环图(DAG)。可认为时间机器(不可变历史)

1.4 Git的安装及配置

1.4.1 安装

Linux系统:

sudo apt install git # debian
sudo yum install git # centos

MacOS: 安装Homebrew后,

brew install git 

Windows: 从官网下载Window版本的安装程序,双击安装即可。

如果能显示版本号,说明证安装成功。

git --version

1.4.2 配置加载机制

Git的配置项加载机制遵循 "层级配置、单值覆盖、多值合并"的原则。

Git会按​系统级、用户级、仓库级、命令行​的顺序读取配置文件:

  • 系统级配置(system): /etc/gitconfig,影响所有用户和项目的全局默认
  • 用户级配置(global): ~/.config/git/config,当前用户的个性化配置
  • 仓库级配置(local): <repo>/.git/config,仅作用于特定仓库的配置
  • 命令行配置(​-c <key>=<value>​),单次命令的最高优先级配置

对于单值配置项,同一键名只能存在一个有效值,后加载的配置会覆盖先前值。按上述的层级配置顺序读取,​​读取的配置会覆盖先前同名的配置项。

某些配置项支持多个值共存(通常是列表型配置,如​push.pushOption​),此时所有层级的同名配置会被合并。

总之,单值配置后读取值覆盖先读取的值,多值配置合并。

1.4.3 常见配置

# 提交用户名及邮箱
git config --global user.name "Your Name"
git config --global user.email your_name@example.com

# 默认分支名: main
git config --global init.defaultBranch main

# 自动为本地分支设置一个上游跟踪分支(upstream tracking branch),并在第一次推送时在远程仓库创建该分支
# 它自动化了 git push -u origin <branch-name> 这个命令
# 强烈推荐开发者全局启用此配置
# 在这个分支上你只需要输入 git push 和 git pull 即可,无需再指定远程和分支名 更流畅
git config --global push.autoSetupRemote true

# 编辑器: 二选一或配置其他编辑器
git config --global core.editor "emacs -nw -q"
# git config --global core.editor "vi"

# pull.rebase: 三选一
git config --global pull.rebase false
# git config --global pull.rebase true
# git config --global pull.ff only

# 查看配置
git config list --show-origin --show-scope

Note

术语“master”与“slave”被认为是隐含有主人和奴隶的含义,不符合多元社会对包容性的要求。后默认分支重命名为main

1.4.4 获取帮助

# 常用子命令
git
git help -a

# 全面的手册
git <verb> --help

# 快速参考
git <verb> -h

2 ONCE 告别混乱:Git 开发工作流程入门,拯救你的代码版本

故曰才不半古,功已倍之,盖得之于时世也。 《晋书·陆机传》

本章面向初学者,只涉及到开发工作流程中常用的命令,目标是学完能立刻上手。

下面的代码演示的是一个最基本的流程:

  • 前两行代码作用从远程仓库克隆一个本地仓库,已当前main分支为基准,新建一个分支​feature/login​,比如这个功能分支是用于创建新的​登录功能
  • 第三、四、五行是本地开发,含编辑、暂存、提交
  • 第六行是将本地提交推送到远程仓库,共享给其他团队成员
git clone git@github.com:cnglen/hi_git.git && cd hi_git
git switch -c "feature/login"
echo "login" >> login
git add login
git commit -m "feature: add login"
git push

大型项目中,推送后还有评审流程,完整流程见下图。

git_dev_flow.svg

Figure 5: git dev flow

2.1 准备阶段

通常是将已有的代码仓库克隆到本地,然后选取一个基准点新建一个功能​分支​,后续在这个分支上开发。

git clone git@github.com:cnglen/hi_git.git && cd hi_git
git switch -c "feature/login"

Note

分支​(branch)是Git解决多个特性功能并行开发的核心机制。

  • 隔离环境,避免冲突: 如张三在开发支付功能,李四在重构搜索算法,两人可以在​feature/payment​和​feature/search​分支上独立工作,代码合并前完全隔离,不会相互影响。
  • 保持主分支的稳定性:​main​分支的代码必须是随时可部署、可测试的稳定版。如果没有分支机制,开发人员会很焦虑,如张三开发一个搜索的功能,一周只玩成了30%的工作量,如果提交代码,可能会导致代码不能编译导致他人不能干活;如果j不提交,又怕代码丢失。有了分支,所有新特性的、未完成的、有潜在 Bug 的代码都只在特性分支上,只有经过测试和审查的、完整的代码才能被合并到 main。大家可以开心地在多个平行宇宙干活。

如果从零初始化一个新的仓库,假设我们刚刚用<account>在github上新建了一个代码仓库hi_git:

mkdir hi_git && cd hi_git && git init
git remote add origin git@github.com:<account>/hi_git.git

2.2 本地开发循环

本地机器上反复进行​编辑、暂存、提交​的循环,无需网络:

  • 编辑​代码:在工作区修改、创建、删除文件。
  • 暂存​代码: ​git add/rm/mv​ 将改动更新到暂存区,可能是修改、删除、重命名文件。
  • 提交​代码: ​git commit​ 创建一个新的提交(commit)保存到本地仓库的历史中。

2.2.1 文件状态

本地开发中,编辑、暂存、提交会导致文件的状态发生变化,可通过​git status -s​命令输出的两个字符​XY​快速判断文件的状态。

git_file_status.svg

Figure 6: git file status

Note

这张图看开发流程是从右往左看,顺序是​edit、add、commit​;看状态变化是从左往右看,和​git status -s​命令输出两列XY的顺序一致,可认为屏幕的左侧外部表示版本库

  • X(第一列):暂存区相对于版本库的变化,即已通过​git add​暂存、但尚未提交(commit)的内容
  • Y(第二列):工作区相对于暂存区的变化,即已修改、但尚未通过​git add​暂存的内容

具体XY的明细含义:

  • 如果处于合并成功或非合并状态

  • 如果是处于合并且冲突未解决状态,三方合并中,

    • X表示我们相对于共同祖先base版本的修改
    • Y表示他们相对于base版本的修改
Table 1: 状态码列表
M: 已修改(Modified) A: 已新增(Added) D: 已删除(Deleted)
R: 已重命名(Renamed) - ?: 未跟踪(untracked) - !: 已忽略(ignored)
␣: 无变化
Table 2: 常见状态组合说明(无合并冲突)
X:暂存区状态 Y:工作区状态 XY:说明 常规 git status 命令输出区域
? ? ?? 未跟踪,即未被git管理 Untracked files
! ! !! 被忽略的文件 Ignored files
M ␣M 工作区已修改,未暂存 Changes not staged for commit(modified)
M M␣ 已暂存(修改) Changes to be committed(modified)
M M MM 已暂存(修改)后又被修改 Changes to be committed; Changes not staged for commit
A A␣ 已暂存(新文件) Changes to be committed(new file)
D D␣ 已暂存(删除) Changes to be committed (deleted)
D ␣D 工作区已删除,未暂存 Changes not staged for commit (deleted)
R R␣ 已暂存(重命名) Changes to be committed (renamed)
Table 3: 常见状态组合说明(有合并冲突)
X:我方修改 Y:他方修改 XY:说明 常规 git status 命令输出区域
U U UU 未合并,我方和他方均修改 Unmerged paths(both modified)
U D UD 未合并,我方修改,他方删除 Unmerged paths(deleted by them)

2.2.2 参考命令

# 查看文件的XY状态,包含已忽略文件
git status -s --ignored

# 查看暂存区的所有文件: mode/hash/stage_number/file
git ls-files --stage

# 查看具体的变化
git diff [<commit=HEAD>] --cached/staged  # <commit=HEAD> -> 暂存区
git diff      # 暂存区 -> 工作区
git diff HEAD # HEAD -> 工作区
git diff commit1 commit2

# GUI工具
git difftool

# 查看历史
git log --oneline --decorate --graph --all --format="%h %an %cn %s"
git log --oneline --stat --graph --patch --format="%h %an %cn %s" [<file>]
git log -p/--patch

2.3 团队协作

当完成一个功能开发或bug修复后,准备与他人协作时,准备推送、

  • 拉取​:在推送前,通过​git pull​先从远程仓库拉取最新的更改并合并到你的当前分支,可及时发现并解决冲突:

    • 如果别人修改了同一文件的相同部分,Git会提示冲突,需要手动编辑文件来解决冲突
    • 通过​git add​标记冲突已解决,​git commit​完成合并提交
  • 推送​:通过​git push​将本地分支及其所有提交推送到远程仓库
  • 评审​:通过创建Pull Request(PR)/Merge Request(MR),在 GitHub/GitLab等平台上,提出将你的功能分支合并到主分支的请求。如果评审通过,功能上线,否则,继续根据反馈开发。

    • PR:基于 Fork + 仓库的模型,请求维护员拉取你的代码变更
    • MR: 基于单一仓库的分支模型,请求将代码变更合并入目标分支

在团队协作开发中,高效整合多方贡献离不开Git的核心机制。具体而言,

  • 通过​三方合并​来集成他人的代码改动
  • 借助​远程仓库​将所有开发者分散的本地仓库连接起来,提供高可用性和数据冗余
  • 并利用​分支与标签​在成员间同步项目状态与重要版本

下文将从这三个方面分别阐述其运作机制。

2.3.1 三方合并

要理解三方合并,首先要明白两方合并(Two-way Merge)的不足。

  • 两方合并:只比较两个文件的最终状态(例如,分支 A 的 file.txt 和分支B 的 file.txt)。如果同一行在两个版本中都发生了变化,合并工具将无法智能地判断应该采用哪个更改,从而导致冲突,必须人工解决。
  • 三方合并:它不只比较两个最终版本,而是会引入第三个版本——两个分支的最新共同祖先(Common Ancestor)。这个共同祖像是“分歧开始的地方”,它为判断“谁修改了什么”提供了至关重要的上下文,故能够​完成自动合并​(当然不是100%的成功率)。
git_three_way_merge.svg

Figure 7: git three way merge

2.3.2 远程仓库

远程仓库指托管在因特网中的版本库,各个开发者通过远程仓库进行团队协作,比如团队某成员从同一个远程仓库clone到本地,将自己开发的代码push到远程仓库;另外一成员拉取远程仓库的最新代码,在其他成员的代码基础上开发新功能。

Git 作为分布式版本控制系统,其核心优势之一在于单个本地仓库可以灵活地关联多个远程仓库。这与只能连接单一中央服务器的集中式版本工具形成鲜明对比。

这种多远程仓库的配置策略在实际开发中非常实用,例如:

  • 将同一个项目同时托管于 GitHub 和 Gitee(码云),即可根据网络状况选择最优平台进行访问和推送;
  • 或者在 GitHub 上维护多个不同贡献者的 Fork 版本,便于直接获取特定成员的提交,方便协作与代码整合。

正所谓“狡兔三窟”,这种多远程配置既提升了容灾性和访问灵活性,也支持了更开放的协作模式。

当你首次使用​git clone​命令克隆一个仓库时,Git 会自动将源仓库地址记录在名为 origin 的默认远程仓库配置中,作为初始的远程参考。

功能 命令
查看远程仓库列表 git remote -v
查看某远程仓库明细 git remote show <repository>
添加 git remote add <repository> <url>
删除 git remote remove <repository>
重命名 git remote rename <old> <new>
远程仓库抓取 git fetch <repository>

gitk中,远程分支以 remotes/<remote_name>/<branch_name> 形式命名。

2.3.3 标签和分支管理

git中每个commit都用一串hash值标识(类似68301504e236508059bfb43d65b5b69f93d839ca),不适合人类读取。

分支和标签,让提交(Commit)有一定的意义,容易让人记住。两者本质上都是一个指向某个提交(Commit)的引用,但分支代表一条​活跃的、持续演进的开发线​,是一个可移动的指针,用来开发、协作;标签用来标记​关键里程碑​,是一个固定的指针,用来发布、标记。

分支、标签,都有本地和远程的概念,本地创建后,如果需要共享给团队,需要推送到远程。对应的删除,也有远程、本地删除。

标签、分支、HEAD的创建,底层实现是创建一个40字节的文件,故能瞬时完成:

  • 新建标签:

    • 创建本地标签: .git/refs/tags/<tag_name>,内容为40字节的sha1值
    • 推送到远程
  • 新建分支:

    • 本地创建分支 .git/refs/heads/<branch_name>,内容为40字节的sha1值
    • 推送到远程
  • HEAD内容: .git/HEAD, 内容为 ref:refs/heads/<current_branch_name>
标签 分支
列出标签/分支 git tag -l/--list [<pattern>] git branch -l/--list [<pattern>] -vv
创建本地标签/分支 git tag <tag_name> [<commit>] git branch <branch_name> [<start-point>]
删除本地标签/分支 git tag -d/--delete <tagname> git branch -d/--delete <branch_name>
推送远程标签/分支 git push origin <tag_name> git push origin <branch_name>
删除远程标签/分支 git push origin --delete <tag_name> git push origin --delete <branch_name>
git_dev_flow_detail.svg

Figure 8: git dev flow detail

2.4 总结

把Git的命令都记住不太现实,也没必要,我们要做的是理解git的开发流程及其背后的目的,比如

  • 整体而言,git是为了提升个人以及团队的开发效率;
  • 推送之前的拉取,是为了合并他人的更新,从团队角度看问题就能理解,否则每个人都推送,没有人合并代码,岂不是乱套了。

日常多实践,需要之时查阅​git --help​或本文。

很多高级功能未介绍,如将多个提交历史的在本地压缩成一条提交、Github的使用、分布式工作中分支如何维护、Git钩子等,大家有兴趣可留言。

6 reference