Git 是什么

Git 是一个分布式版本控制系统,每个端都存储着仓库所有代码。对数据的存储是记录快照的方式,默认只增加文件,不删除文件。本质上就是一个内容寻址文件系统,核心部分是键值对数据库。

Git 基础命令和效果

先初始化一个项目,看下空仓库下有什么

  1. # 初始化
  2. $ git init learnGit
  3. # 进入项目
  4. $ cd learnGit

会发现 learnGit 目录里面有个 .git 文件,有如下内容:

  1. .git/
  2. |-- hooks # 钩子函数
  3. | |-- applypatch-msg.sample
  4. | |-- commit-msg.sample
  5. | |-- fsmonitor-watchman.sample
  6. | |-- post-update.sample
  7. | |-- pre-applypatch.sample
  8. | |-- pre-commit.sample
  9. | |-- pre-merge-commit.sample
  10. | |-- pre-push.sample
  11. | |-- pre-rebase.sample
  12. | |-- pre-receive.sample
  13. | |-- prepare-commit-msg.sample
  14. | `-- update.sample
  15. |-- info # 仓库信息
  16. | `-- exclude # 文件忽略
  17. |-- objects # 储存二进制文件
  18. | |-- info
  19. | `-- pack # 储存 git gc 打包文件
  20. |-- refs
  21. | |-- heads # 所有分支引用(指针)
  22. | `-- tags # 标签
  23. |-- HEAD # 当前 commit 引用
  24. |-- config # 仓库配置文件
  25. `-- description # 仓库描述文件

创建 test.txt 文件,并且执行 git add 命令看看

  1. # 创建文件
  2. $ vim test.txt
  3. # 添加到缓存空间
  4. $ git add .
  5. # 查看 .git/index 文件
  6. $ git ls-files -s

再执行 git commit 将文件提交到仓库区

  1. # 提交文件
  2. $ git commit -m 'add test.txt'
  3. # 查看 git 提交历史
  4. $ git log
  5. # 查看 objects
  6. $ find .git/objects/
  7. # 查看 Object 文件类型 -t 类型 | -s 长度 | -p 内容
  8. $ git cat-file -t d67046

可以得出以下树形关系
image.png
此时添加一个 test 文件夹,看 git 状态

  1. $ mkdir test
  2. $ git statis
  3. $ git add .

给 test 文件夹加一个 a.txt 文件

  1. $ vim test/a.txt
  2. $ git add .
  3. # 查看此时索引状态
  4. $ git status
  5. # 查看 .git/objests 内容
  6. $ find .git/objests
  7. $ git commit -m 'add a.txt'
  8. # 查看 .git/objests 内容
  9. $ find .git/objests

总结可以得出 git 储存主要通过 3 个对象,分别为 blob、tree、commit。
image.png

底层命令及部分算法原理

blob 对象

可以看到 git/objects 下面多了一个文件夹和文件,使用 git -cat-files 查看内容

  1. # -t 类型 | -s 长度 | -p 内容
  2. $ git cat-file -p d67046
  3. test content

就是我们刚刚输入的内容,再将其修改并写入数据库

  1. $ echo 'version 1' > test.txt
  2. $ git hash-object -w test.txt
  3. 83baae61804e65cc73a7201a7252750c76066a30
  4. # 再次写入修改
  5. $ echo 'version 2' > test.txt
  6. $ git hash-object -w test.txt
  7. 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a

然后查看数据库下所有内容,所有数据都被保存为快照

  1. $ find .git/objects -type f
  2. .git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a
  3. .git/objects/83/baae61804e65cc73a7201a7252750c76066a30
  4. .git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4

删了本地种的 test.txt,可以从数据库中恢复

  1. # 第一个版本
  2. $ git cat-file -p 83baae > test.txt
  3. $ cat test.txt
  4. version 1
  5. # 第二个版本
  6. $ git cat-file -p 1f7a7a > test.txt
  7. $ cat test.txt
  8. version 1

然后使用 git -cat-files 查看文件类型,是二进制数据对象

  1. $ git cat-file -t 1f7a7a
  2. blob

tree 对象

tree 对象的作用是保存文件名和文件目录关系的,有个 demo 项目代码就目录就如下

  1. # master^{tree} 语法表示 master 分支上最新的提交所指向的树对象
  2. $ git cat-file -p master^{tree}
  3. 100644 blob 8178c76d627cade75005b40711b92f4177bc6cfc README.md
  4. 040000 tree 6258f9911fe852aa82e9da1d3ad5f101c81ba199 lib
  5. 100644 blob 30d74d258442c7c65512eafab474568dd706c430 test.txt
  6. # 查看 lib 的 tree对象
  7. $ git cat-file -p 6258f
  8. 100644 blob 1da36b1e0266efb0e1a57d2bced8734d4343a28a utils.js

Git 内部数据存储为一棵树的结构
071d2d1a-598f-11eb-9d8a-8eebac42c613.png

Git 在 commitstash 时会根据暂存区生成一个 tree 对象,因此先使用 git update-index 创建一个暂存区。

  1. # 文件模式:100644 普通文件 | 100755 可执行文件 | 120000 符号链接
  2. $ git update-index --add --cacheinfo 100644 \
  3. 83baae61804e65cc73a7201a7252750c76066a30 test.txt
  4. # .git 会创建 index 文件,并修改,查看其内容
  5. $ git ls-files --stage
  6. 100644 83baae61804e65cc73a7201a7252750c76066a30 0 test.txt
  7. # 将缓存区写入一个树对象
  8. $ git write-tree
  9. d8329fc1cc938780ffdd9f94e0d364e0ea74f579
  10. $ git cat-file -p d8329
  11. 100644 blob 83baae61804e65cc73a7201a7252750c76066a30 test.txt
  12. # 生成一个树对象
  13. $ git cat-file -t d8329
  14. tree

再创建一个新的 tree 对象,使用 test.txt 第二个版本

  1. # 生成暂存区
  2. $ echo 'new file' > new.txt
  3. $ git update-index --add --cacheinfo 100644 \
  4. 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt
  5. $ git update-index --add new.txt
  6. # 生成树对象
  7. $ git write-tree
  8. 0155eb4229851634a0f03eb265b69f5a2d56f341
  9. $ git cat-file -p 0155eb4229851634a0f03eb265b69f5a2d56f341
  10. 100644 blob fa49b077972391ad58037050f2a75f74e3671e92 new.txt
  11. 100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt

还可以将第一个树对象加入第二个树对象中

  1. $ git read-tree --prefix=bak d8329fc1cc938780ffdd9f94e0d364e0ea74f579
  2. $ git write-tree
  3. 3c4e9cd789d88d8d89c1073707c3585e41b0e614
  4. $ git cat-file -p 3c4e9
  5. 040000 tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579 bak
  6. 100644 blob fa49b077972391ad58037050f2a75f74e3671e92 new.txt
  7. 100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt

f7fd208c-598f-11eb-9500-960f8492bebe.png

提交对象

commit 对象的作用是保存 tree 快照、提交作者、提交时间、上一个 commit 等基本信息。根据暂存区生成的 tree 对象和时间、作者等信息生成。信息会保存在 .git/logs/refs/ 里面

  1. $ echo 'first commit' | git commit-tree d8329f
  2. e7ef8525b1d22ef056b0365f784e464d5766b822
  3. $ git cat-file -p e7ef85
  4. tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579
  5. author gigot <gigot@gmail.com> 1610458188 +0800
  6. committer gigot <gigot@gmail.com> 1610458188 +0800
  7. first commit

再将另外两个 tree 对象分别引用到上一次提交

  1. $ echo 'second commit' | git commit-tree 0155eb -p e7ef85
  2. 14e6379ce4addb40350d3dc114c3180a87145b91
  3. $ echo 'third commit' | git commit-tree 3c4e9c -p 14e637
  4. 03cf4d6eb12f10e0eac3476fb36fde8441458078

然后就可以根据 git log 查看提交的历史记录了

  1. $ git log --stat 03cf4d
  2. commit 03cf4d6eb12f10e0eac3476fb36fde8441458078 (tag: v1.1)
  3. Author: gigot <gigot@gmail.com>
  4. Date: Wed Jan 13 09:43:15 2021 +0800
  5. third commit
  6. bak/test.txt | 1 +
  7. 1 file changed, 1 insertion(+)
  8. commit 14e6379ce4addb40350d3dc114c3180a87145b91 (tag: v1.0, test)
  9. Author: gigot <gigot@gmail.com>
  10. Date: Wed Jan 13 09:39:52 2021 +0800
  11. second commit
  12. new.txt | 1 +
  13. test.txt | 2 +-
  14. 2 files changed, 2 insertions(+), 1 deletion(-)
  15. commit e7ef8525b1d22ef056b0365f784e464d5766b822
  16. Author: gigot <gigot@gmail.com>
  17. Date: Tue Jan 12 21:29:48 2021 +0800
  18. first commit
  19. test.txt | 1 +
  20. 1 file changed, 1 insertion(+)

48c622a0-5992-11eb-a20e-a662c738cbbf.png

对象生成原理

  1. 计算 content 长度,构造 header;
  2. header 添加到 content 前面,构造 Git 对象;
  3. 使用 sha1 算法计算 Git 对象的40位 hash 码;
  4. 使用 zlibdeflate 算法压缩Git对象;
  5. 将压缩后的 Git 对象存储到 .git/objects/hash[0, 2]/hash[2, 40] 路径下
  1. # 伪代码
  2. $ comtent = "test" => "test"
  3. $ header = "blob #{content.length}\0" => "blob 4\u0000"
  4. $ store = header + comtent => "blob 16\u0000test"
  5. $ sha1(store)
  6. 9daeafb9864cf43055ae93beb0afd6c7d144bfa4

小结

  • 文件以二进制存数据对象 blob,有 tree 对象组成一颗树
  • 每次提交更新一个快照对象 commit,指定一个 tree 对象
  • commit 之间有依赖关系 patient,形成完整 log

Git 引用

上面最终根据 03cf4d 可以获得所有提交,但我们日常是很难记住这么一个哈希值,所以添加了引用,即指针

  1. # 1. 直接写入 master(不推荐)
  2. $ echo 03cf4d6eb12f10e0eac3476fb36fde8441458078 > .git/refs/heads/master
  3. $ git log --pretty=oneline master
  4. 03cf4d6eb12f10e0eac3476fb36fde8441458078 third commit
  5. 14e6379ce4addb40350d3dc114c3180a87145b91 second commit
  6. e7ef8525b1d22ef056b0365f784e464d5766b822 first commit
  7. # 2. git update-ref 写入 test 分支
  8. $ git update-ref refs/heads/test 14e6379ce4addb40350d3dc114c3180a87145b91
  9. $ git log --pretty=oneline test
  10. 14e6379ce4addb40350d3dc114c3180a87145b91 second commit
  11. e7ef8525b1d22ef056b0365f784e464d5766b822 first commit

HEAD 引用

保存着当前分支引用,文件位于 .git 目录

  1. $ cat .git/HEAD
  2. ref: refs/heads/master

不推荐直接修改 HEAD 文件来改变分支,可以使用 git symbolic-ref 查看修改

  1. $ git symbolic-ref HEAD
  2. refs/heads/master
  3. # 切换 test 分支
  4. $ git symbolic-ref HEAD refs/heads/test

头指针分离即 HEAD 文件中包含一个 sha-1 值。

Tag 引用

保存着标签引用,引用文件保存在 .git/refs/tags 里面

  1. # 打一个 tag:v1.0
  2. $ git update-ref refs/tags/v1.0 14e6379ce4addb40350d3dc114c3180a87145b91
  3. # 带注释 tag:v1.1
  4. $ git tag -a v1.1 03cf4d6eb12f10e0eac3476fb36fde8441458078 -m 'test tag'
  5. $ git cat-file -p v1.1
  6. object 03cf4d6eb12f10e0eac3476fb36fde8441458078r
  7. type commit
  8. tag v1.1
  9. tagger gigot <gigot@gmail.com> 1610627545 +0800
  10. test tag

远程引用

保存远程仓库引用,引用文件保存在 .git/refs/remotes 里面

  1. $ git remote add origin git@github.com:weniu/demo.git

小结

  • Git 指针是指 HEAD 文件
  • 切换分支则修改 HEAD 文件值
  • branch 和 tag 为 refs 里面的引用文件,引用文件保存 commit 对象

    Git 常用功能(课外)

    git merge or git cherry-pick

Git 默认合并策略 Recursive 主要分为 fast-forward 和 no-fast-forward(-Xours | -Xtheirs);其他还有 Resolve、Ours、Octopus、Subtree 策略。

  1. fast-forward,合并的其中一个 commit 为另一个 commit 的子孙级,将分支引用改为最新 commit
    1. release
    2. |
    3. A <-- B <-- C
    4. |
    5. master

release 分支合并到 master

  1. release
  2. |
  3. A <-- B <-- C
  4. |
  5. master
  1. no-fast-forward,合并时分支是并列的关系(或使用 --no-ff),则采用三方合并(Three-Way Merge)。找到双方分支最近的共同祖先节点,然后分别于其对比看是否修改。如果文件内容冲突,则保留冲突内容,需要手动修改。最终生成新的 commit 对象
    1)默认采用递归合并(Recursive 策略) ```bash

    简单合并

    D <— F feature1 / \ A <— B <— C feature2

三方合并

F 与 A 对比

C 与 A 对比

查看公共节点

$ git merge-base feature1 feature2 A

  1. <br /> 假设合并 `text.txt` 文件冲突
  2. ```diff
  3. <<<<<<< HEAD
  4. test2
  5. =======
  6. test1
  7. >>>>>>> feature1


通过 git show 可以查看冲突源文件,高级的可以用 git ls-files -u

  1. # 公共祖先,另存为 common.txt;:1:test.txt 代表文件 sha1 值
  2. $ git show :1:test.txt > test.commom.txt
  3. test
  4. # 当前分支版本
  5. $ git show :2:test.txt > test.ours.txt
  6. test2
  7. # 合并分支版本
  8. $ git show :3:test.txt > test.theirs.txt
  9. test1


想在文件看到全部对比版本,可以使用 git checkout

  1. $ git checkout --conflict=diff3 test.txt
  2. <<<<<<< ours
  3. test2
  4. ||||||| base
  5. test
  6. =======
  7. test1
  8. >>>>>>> theirs


也可以通过 git merge-file 手动执行文件再合并

  1. $ git merge-file -p \
  2. test.ours.txt test.common.txt test.theirs.txt > test.txt


上面都是只有 1 个公共节点的情况,当出现 2 个以上公共节点时候呢
tree4.png
图中 E 和 D 有 2 个公共节点为 B、C,这种现象称为 Criss-Cross 现象。git 会根据 B 和 C 创建一个临时节点 F 作为三方合并的 base 节点,然后进行正常的三方合并生成新的 commit
2)Resolve 策略
与 Recursive 策略基本一样,但遇到多个公共节点的情况,取其中一个作为 merge base 节点。是 Recursive 策略出现前默认策略

  1. $ git merge -s resolve feature


3)Ours 策略
丢弃合并分支的所有代码,仅生成新的 commit 对象,内容与当前分支一致。做的是假合并(fake merge)

  1. $ git merge -s ours feature1 feature2

4)Octopus 策略
合并多个分支时的默认策略,采用的也是三方合并。当出现冲突时,默认取第一个分支与当前分支冲突结果

  1. $ git merge -s octopus feature1 feature2

5)Subtree 策略
改进的递归合并策略,如果tree B 是 tree A 的子树,则调整 B 以匹配 A 的树结构,不进行同级对比。

git rebase

git rebase 合并策略与 git merge 的基本一致,但功能要强大更多。

  1. B --- C feature
  2. /
  3. A --- D master
  4. $ git rebase master
  5. feature
  6. |
  7. A --- D --- B --- C
  8. |
  9. master

如上使用 master 进行变基操作,会先找到公共节点 A,然后 feature 上每一个 A 后的 commit 都做对比,然后缓存下来,将 feature 的提交历史重置为 master 分支历史,然后将缓存内容重新提交生成 commit

git reflog

记录 .git/HEAD 的变更历史,即 .git/logs/HEAD 的内容,在 git update-ref 执行时写入

git fsck —full

找到没有被其他对象指向的对象,即无引用对象,通常 git stash 等丢失找回

  1. $ git fsck --full
  2. Checking object directories: 100% (256/256), done.
  3. Checking objects: 100% (18/18), done.
  4. dangling blob d670460b4b4aece5915caf5c68d12f560a9fe3e4
  5. dangling blob 9daeafb9864cf43055ae93beb0afd6c7d144bfa4
  6. dangling blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391

参考资料