git 原理分析与使用进阶
git 是 Linus Torvalds 设计的源代码版本控制系统,以其方便简洁的使用,高效的操作,分布式的架构 已经替换掉了原来的SVN,成为了主流的版本控制系统。随着我对 git 的不断使用,我逐渐对 git 的原理产生了浓厚的兴趣,所以本文会着重分析 git 原理,并指导更加清晰、高效地使用 git。
git 数据类型
基本数据类型
- blob 存储 文件内容
- tree 存储目录下文件的文件名,权限
- commit 存储提交信息
blob 作为文件的基本元素,存储文件,blob一旦生成,不可修改。新添加的文件或者修改文件,会生成新的blob。tree表示文件目录,一个tree下面可以有tree和blob。一个commit对象存储提交信息,用户每次提交,都会形成新的tree,生成新的commit指向这个新tree。
空间维度 x 时间维度
在空间维度上,blob 和 tree 足够表达源代码的目录结构。但是对于版本管理系统来说,我们还需要记录每个提交点的快照(snapshot),所以需要时间维度的抽象数据结构,commit 就是时间维度上的数据结构,commit的之间的引用,也形成了一个时间线上 tree 结构(DAG,有向无环图)。
git objects database
content-addressed filesystem
git 是一个 content-addressed filesystem 基于内容寻址的系统。git的三种基本对象 blob,tree,commit,存储的位置都是基于内容的hash算出来的。
hash 生成方法
1 | header = "<type> " + content.length + "\0" |
- 生成header,
为 blob,tree,commit,三个种字符串,content.length 是内容(文件的内容,tree对象的内容,commit的内容)的字节长度,末尾加上 \0 字符和内容分开 - header + content 是二进制拼接,而不是文本拼接
- sha1 就是常用的hash 算法 SHA-1,生成40位hash值
对象存储
- 存储路径
.git/objects/hash[0, 2]/hash[2, 40]
, 40位 hash 前两位为目录名,后38位为 文件名 - 存储的内容 是把字节内容经过zlib deflate 压缩后的结果
tag 和 branch
- tag 就是commit的别名,tag 指向的 commit 不变。
- branch 本质也是commit的别名,只是这个 commit 会一直跟着开发的提交在变。但是 branch 一直指向最顶端的 commit。
tag 存储方式
运行
1 | git tag firsttag |
会在.git/refs/tags
生成 firsttag文件,文件内容存储的是 commit的hash
branch 存储方式
.git 目录中 .git/HEAD
文件存储的是当前用户工作的 branch
refs/heads/
目录下存储以 branch
名字命名的文本文件,文件内容 存储的是 commit
的 hash。
通过 git show <commit_hash>
就可以查看 commit的 内容。
1 | git show d8a5333173c2bf5de7853b11b24839715275cce2 |
- 用户从 master checkout 到 firstbranch, 会更新
.git/HEAD
文件的内容,写入内容ref: refs/heads/firstbranch
- 用户在 firstbranch branch 做commit 操作,会更新
refs/heads/firstbranch
文件的内容,写进新的 commit hash
所以本质上branch 也是个 commit的别名,只是这个commit会随着开发提交而改变。
git 存储流程
一个commit 的最终分四种状态, 提交分为三个阶段
modified
所有修改完的代码,在你本机源代码的路径下,也就是你的workspace 里,目前和git 还没关系
added
1 | git add xxx |
跑完了 add 命名。这个阶段后,git才开始工作,你修改的文件 会生成 git 的 blob object 存储在 .git/objects
committed
1 | git commit -m "xxx" |
跑完 commit 命令,git生成 tree object,指向当前的 snapshot,生成 commit object 保存提交信息,并且这个commit是指向tree的。
pushed
git push
则是把修改真正的提交到远程仓库。
使用进阶
怎样恢复代码?
- 你代码commit 了吗?没有commit, 又没有手动备份,无法恢复。
- 你已经commit,但是找不到这个commit 了。执行
git reflog
, 按照你操作的时间点排查,应该是哪个commit
1 | git reflog |
怎样回滚代码?
你提交到中心仓库了吗?如果提交了 你应该
git revert <commit-hash>
生成一个新的 reverted-commit,你再push这个commit 到线上你没有提交到中心仓库
1
2
3
4
5
6
7
8
9# 回滚 commit
git reset --hard HEAD~ # HEAD 指你当前branch的最上方一个commit,HEAD~ 是最上方开始的第二个commit,HEAD~~ 是最上方开始的第三个commit,以此类推。--hard 是指 不要commit 信息,也不要文件修改后的内容。整个这一句连起来就是 回滚到 让让前branch 删除 最新的一个提交
git reset --soft HEAD~ # 只清除commit 信息,不回滚文件修改的内容
# 回滚文件
git checkout -- xx/xxx # 当一个文件为commit时,重置一个单独的文件, 回滚当前 branch的HEAD时的内容
# 假设当前 branch 为 master
git checkout master~ xx/xxx # 回滚文件 xx/xxx 到 HEAD 算起的 第二个commit
git checkout master~2 xx/xxx # 回滚文件 xx/xxx 到 HEAD 算起的 第三个 commit
怎样排查是哪一个commit 引入了某个bug?
二分查找法
1 | git bisect start [终点commit] [起点commit] # 执行后 当前branch 被reset到 两个commit的 最中间的那个commit |
怎样暂存当前的提交?
比如当你本地代码没改完,但是需要马上 pull 最新代码时,可以用
1 | git stash # 暂存所有的修改,不commit |
1 | git stash list # 查看 所有暂存的列表, 是一个stack 结构 |
怎样 merge?
假如面临这样一种情况, 你在你本机的 devel branch 开发好了两个 commit ,但是 你想 merge 到 master 时,发现别人也提交了一个commit,你要怎么merge?
直接 merge 不合并要 merge 的 commit,不改变原有 commit history
1 | git checkout master |
megrge with squash,把要 merge 的 commit,合并成一个commit history
1 | git checkout master |
rebase 通过 rebase -i 调整 commit, 重新编辑 commit history
1 | git checkout master |
总结
git的设计是精巧的,很值得学习。一个好的系统不仅是表象上功能强大,更是内部设计的精巧。这种精巧来源于对应用场景深入洞察,对各种数据结构、编程技术的巧妙应用。git 命令是很丰富的,一般我们用的比较多的是 high level 命令,其实 git 还有很多 low level 命令,多去探索,你也会发现很多乐趣。最后分享 Linus 的关于程序设计的一段话。
“Bad programmers worry about the code. Good programmers worry about data structures and their relationships.”
–Linus Torvalds