
有这么一个段子...
老杨一推代码, 所有的开发同事便都看着他笑, 有的叫道, “ 老杨, 你又把代码合丢了!” 他不回答, 对产品说, “ 追加两个需求, 只做性能优化。” 便排出一台MacBook Pro。 他们又故意的高声嚷道, “ 你推错分支了!” 老杨睁大眼睛说, “ 你怎么这样凭空污人清白……”, “ 什么清白? 我前天亲眼见你写Bug把流水线搞挂, 又改崩生产代码“。 老杨便涨红了脸, 额上的青筋条条绽出, 争辩道, “ 流水线报错不算错, 合代码挂掉的事能叫Bug吗?” 接连便是难懂的话, 什么“ Merge大法好, GitFlow卍解”, 什么“ 拉代码不需要Rebase” 之类, 引得众人都哄笑起来: 店内外充满了快活的空气。
用 git 的, 谁没合丢过代码呢? (反正我是合丢过...)
好, 痛定思痛, 我决定从原理上理解Git, 所以决定写下这篇文章(才不会说是因为buddy叫我准备一场针对 git 的 session).
PS: git官网上有看都看不完的长篇大论, 甚至有人专门为理解git的一种workflow出书...这些都说明"看这篇文章就想把git搞透彻是不可能的"(因为写文章的人都没有把git搞透彻(再次苦笑)).
So: 这篇文章不会是一个体系完善的Git教程, 只会针对性地从原理上理解一些我们常用的Git命令, 最终目标就是能够在基本的多人合作的场景下正常工作, 同时对于复杂的场景能有用于检索资料的先验知识.
目录
- 目录
- Git的那些概念
- Git的本质是什么?
- Git中存储的对象
- 1. blob对象
- 2. tree对象
- 3. commit对象
- ref(引用)
- Git的那些常用操作
- file-level
- commit/branch-level
- repo-level
- Git的那些冲突
- 出现冲突的可能情形
- 规避冲突的优选操作
- 其他的奇技淫巧
- 别名
- 子模块(submodule)
Git的那些概念
Git的本质是什么?
Git的本质其实是一个内容寻址(content-addressable)文件系统, 它的核心部分是一个简单的键值对数据库。我们可以向该数据库写入值(object), 它会返回一个键(object的引用地址, SHA-1字符串), 通过该键可以在任意时刻再次检索值. 我们所操作的版本控制, 其实就是不断地向这个文件系统写入操作日志.
那么这些被写入的操作日志是什么呢?
Git中存储的对象
1. blob对象
对应的值是单个文件内容的快照, 这个快照不包括文件名。

2. tree对象
以树的形式记录的目录结构和文件的索引, 每个普通结点(有子级的结点)都是一个子级的tree
对象的包裹体, 每个叶子结点(无子级的结点)都是一个blob
对象的包裹体, 这些包裹体会附带文件(夹)的名称等元数据。

3. commit对象
对应的值包含一个数据集(通常称为Comments
, 这个数据集包含父级commit
的地址(SHA1值, 通常也被称作commitid)、作者以及提交message等信息)以及一个当前commit
对应的变更的tree
, tree
的内容为当次提交的变动快照(没有发生变动的文件不会被加入快照)。

总结一下, 三者关系:

所以, 当存在多个提交时,
#bash
git init
echo "version 1" > test.txt
git add .
git commit -m "frist commit"
echo "version 2" > test.txt
echo "new file" > new.txt
git add .
git commit -m "second commit"
mkdir bak
echo "version 1" > bak/text.txt
git add .
git commit -m "third commit"
最终的整体结构应该是这样:

ref(引用)
- branch: 指向某一系列提交之首的引用
- HEAD: 指向目前工作基点提交的引用
- tag:
- lightweight tag: 指向任意提交的引用
- annotated tag: 指向一个标签对象的引用(注:标签对象其实和上面提到的三种一样也是对象, 不过非常罕见, 结构类似commit, 不过内含数据不是指向变动快照的
tree
, 而是指向commit
)
- remote branch: 指向某服务器端某一系列提交之首的引用
Git的那些常用操作
file-level

- add @path: 标记文件的stage状态
- reset @path: 取消文件的stage状态
- --hard: 取消文件的stage状态并恢复其到unmodified状态
- checkout @path: 等价于reset @path --hard
commit/branch-level
以下一些操作可以到learngitbranching上进行交互式演示, 接下来在讲解相关概念的时候会在这里做相关的命令演示, 小伙伴们可以在上面操作一下
注:
- 以下的所有commitid都可以换成branch(或者说ref)
- 以下的所有commitid都会是简称(例如c1等)
git clone # 初始化模拟仓库
- checkout @commitid=null: 转移HEAD到指定提交
git checkout c0 # 将HEAD移动到c0(master保持不变, 此时HEAD为detached HEAD, 直接指向了commit而非某个ref)
- commit: 提交
- --amend: 与前一个提交合并提交(改写)
git commit #生成一个新提交
git commit --amend #改写基点提交, 当前worktree内容融合基点提交的内容重新生成一个提交
- branch: 创建一个分支
git branch branch1 # 基于当前生成一个新分支
git checkout branch1 # 移动HEAD到这个新分支(branch1)
- reset @commitid=null: 重置HEAD及其所指向的ref到指定提交
- --hard: 抛弃reset过程中的所有文件变更()
git reset master # 重置HEAD及其所指向的ref到master所在提交
# 重置HEAD及其所指向的ref到"c2'"提交
git reset c2'
PS: 关于reset和check的区别

- revert @commitid: 提交一次与某次提交的内容完全相反的提交
# 生成一次和c2'相反的提交(抵消/还原c2'提交并向前移动HEAD, 新版本的git还支持批量revert(git revert start..end)
git revert c2'
- cherry-pick @commitid: 将某个commit的变动叠加到HEAD所指向的commit上(会创建一个和之前的commit内容一样的提交, 不过两者具有不同的sha1值)
git cherry-pick c1 # 将c1提交的变动快照同步到当前HEAD位置, 新版本的git还支持批量cherry-pick(git cherry-pick start..end)
- tag: 标记具有某种特殊意义的提交(里程碑)
git tag OnMerge # 给当前提交取名"OnMerge", 可以很方便地回滚到特定版本
- merge @commitid: 将某一次提交的内容整合到HEAD(生成一次merge提交叠加在HEAD之上并移动HEAD)
git checkout master # 模拟其他人在master上进行了两次提交
git commit # 模拟一次提交c3
git commit # 再模拟一次提交c4
git checkout branch1 # 切回HEAD到branch1
git merge c3 # 单独合并c3的变动到当前HEAD
git reset c1'
git merge master # 合并master上所有的变动到当前HEAD
- rebase @commitid: 将某一次提交的内容整合到HEAD(把所有不在commitid对应提交所在提交链上的提交叠加在该提交上)
- -i 交互式, 可以选择哪些提交要被rebase到指定位置后
git checkout master # 模拟其他人在master上进行了两次提交
git commit # 模拟一次提交c7
git commit # 再模拟一次提交c8
git checkout branch1 # 切回HEAD到branch1
git rebase c7 # 把当前分支的变更叠加到c7上
git rebase OnMerge # 撤销一下
git rebase master -i # 把当前分支的变更叠加到master上(效果等效于merge, 不过rebase假定自己的编辑都是基于master的)
git checkout master # 如果经常保持rebase, 那么当branch1需要被merge回master时, 会非常容易
git merge branch1 # 冲突已经在频繁的rebase中被解决的, 这里的merge都会是"快速前进"
# 可以说是多分支开发中的"单分支开发"
repo-level
- fetch: 同步remote repo的状态
- remote: 管理remote repo及相关分支, 几乎只需要单次配置, 不做赘述.
- push: 将本地的commit推送到服务器(默认不会推送tag)
- push --tag: 连tag一起推送
Git操作实体的总体结构

Git的那些冲突
出现冲突的可能情形
- 他人和自己编辑了对同一文件的编辑内容存在交叉行
- 他人删除了自己编辑的文件
- 他人和自己新增了同名的文件
规避冲突的优选操作
- 代码稳定后尽快推送(尤其针对公共内容的改动)
- 勤获取代码(Build前自动获取最新代码)
其他的奇技淫巧
别名
git config --global alias.co checkout
子模块(submodule)
# 在现有的git仓库内执行
git submodule add -b master https://example.com/another-repo.git another-repo # 将another-repo添加为当前git仓库的子模块, 并存放到another-repo路径下
git submodule update --remote # 从远端更新当前仓库的子模块
或着针对已经存在的子模块
# 在现有的git仓库内执行
git submodule add another-repo # 将another-repo目录添加为当前git仓库的子模块(前提是another-repo需要是一个git仓库), 不指定分支的时候, git会默认将当前HEAD所在的commit作为子模块(即子模块处于非track状态)
git push --recurse-submodules=<check|on-demand> # push之后子仓库状态即可同步, 其他团队成员可以通过git submodule update --remote来更新本地子模块, check9默认)为循环检测子模块, on-demand为仅提交当前仓库的一级子模块