Git 基本原理介绍
编辑:乐乐 | 来自:escapelife.site/posts/da89563c.html
上一篇:男子盗取iCloud账户62万张裸照,还放上了“黄网”?!
执行完成如下命令之后,我们可以得到下图所示的内容,右侧的就是 Git 为我们创建的代码仓库,其中包含了用于版本管理所需要的内容。
# 左边执行
$ mkdir git-demo
$ cd git-demo && git init
$ rm -rf .git/hooks/*.sample
# 右边执行
$ watch -n 1 -d find .
➜ tree .git
.git
├── HEAD
├── config
├── description
├── hooks
├── info
│ └── exclude
├── objects
│ ├── info
│ └── pack
└── refs
├── heads
└── tags
.git/config - 当前代码仓库本地的配置文件
本地配置文件(.git/config)和全局配置文件(~/.gitconfig)
通过执行如下命令,可以将用户配置记录到本地代码仓库的配置文件中去
git config user.name "demo"
git config user.email "demo@demo.com"
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
[user]
name = demo
email = demo@demo.com
blob 类型
commit 类型
tree 类型
# 均无内容
➜ ll .git/objects
total 0
drwxr-xr-x 2 escape staff 64B Nov 23 20:39 info
drwxr-xr-x 2 escape staff 64B Nov 23 20:39 pack
➜ ll .git/objects/info
➜ ll .git/objects/pack
.git/info - 当前仓库的排除等信息
# git ls-files --others --exclude-from=.git/info/exclude
# Lines that start with '#' are comments.
# For a project mostly in C, the following would be a good set of
# exclude patterns (uncomment them if you want to use them):
# *.[oa]
# *~
.git/hooks - 当前代码仓库默认钩子脚本
./.git/hooks/commit-msg.sample
./.git/hooks/pre-rebase.sample
./.git/hooks/pre-commit.sample
./.git/hooks/applypatch-msg.sample
./.git/hooks/fsmonitor-watchman.sample
./.git/hooks/pre-receive.sample
./.git/hooks/prepare-commit-msg.sample
./.git/hooks/post-update.sample
./.git/hooks/pre-merge-commit.sample
./.git/hooks/pre-applypatch.sample
./.git/hooks/pre-push.sample
./.git/hooks/update.sample
.git/HEAD - 当前代码仓库的分支指针
➜ cat .git/HEAD
ref: refs/heads/master
.git/refs - 当前代码仓库的头指针
# 均无内容
➜ ll .git/refs
total 0
drwxr-xr-x 2 escape staff 64B Nov 23 20:39 heads
drwxr-xr-x 2 escape staff 64B Nov 23 20:39 tags
➜ ll .git/refs/heads
➜ ll .git/refs/tags
.git/description - 当前代码仓库的描述信息
➜ cat .git/description
Unnamed repository; edit this file 'description' to name the repository.
add 之后发生了什么
执行完成如下命令之后,我们可以得到下图所示的内容,我们发现右侧新增了一个文件,但是 Git 目录里面的内容丝毫没有变化。这是因为,我们现在执行的修改默认是放在工作区的,而工作区里面的修改不归 Git 目录去管理。
而当我们执行 git status 命令的时候,Git 又可以识别出来现在工作区新增了一个文件,这里怎么做到的呢?—— 详见[理解 blob 对象和 SHA1]部分
而当我们执行 git add 命令让 Git 帮助我们管理文件的时候,发现右侧新增了一个目录和两个文件,分别是 8d 目录、index 和 0e41.. 文件。
# 左边执行
$ echo "hello git" > helle.txt
$ git status
$ git add hello.txt
# 右边执行
$ watch -n 1 -d find .
# 查看 objects 的文件类型
$ git cat-file -t 8d0e41
blob
# 查看 objects 的文件内容
$ git cat-file -p 8d0e41
hello git
# 查看 objects 的文件大小
$ git cat-file -s 8d0e41
10
# 拼装起来
blob 10\0hello git
为了验证我们上述的说法,我们可以添加同样的内容到另一个文件,然后进行提交,来观察 .git 目录的变化。我们发现,右侧的 objects 目录并没有新增目录和文件。这就可以证明,blob 类型的 object 只存储的是文件的内容,如果两个文件的内容一致的话,则只需要存储一个 object 即可。
话说这里 object 为什么没有存储文件名称呢?这里因为 SHA1 的 Hash 算法计算哈希的时候,本身就不包括文件名称,所以取什么名称都是无所谓的。那问题来了,就是文件名的信息都存储到哪里去了呢?—— 详见[理解 blob 对象和 SHA1]部分
# 左边执行
$ echo "hello git" > tmp.txt
$ git add tmp.txt
# 右边执行
$ watch -n 1 -d find .
理解 blob 对象和 SHA1
Hash 算法是把任意长度的输入通过散列算法变化成固定长度的输出,根据算法的不同,生成的长度也有所不同。
Hash 算法:
MD5 - 128bit - 不安全 - 文件校验
SHA1 - 160bit(40位) - 不安全 - Git 存储
SHA256 - 256bit- 安全 - Docker 镜像
SHA512 - 512bit - 安全
➜ echo "hello git" | shasum
d6a96ae3b442218a91512b9e1c57b9578b487a0b -
➜ ls -lh hello.txt
-rw-r--r-- 1 escape staff 10B Nov 23 21:12 hello.txt
➜ echo "blob 10\0hello git" | shasum
8d0e41234f24b6da002d962a26c2495ea16a425f -
# 拼装起来
blob 10\0hello git
这是因为其进行了压缩,存储了一些压缩相关的信息。上例所示的比原始文件大,是因为我们创建的内容实在是太小了。当我们常见一个比较大的文件时,就会看到压缩之后的文件大小远小于原始文件的。
➜ cat .git/objects/8d/0e41234f24b6da002d962a26c2495ea16a425f
xKOR04`HWH,6A%
➜ ls -lh .git/objects/8d/0e41234f24b6da002d962a26c2495ea16a425f
-r--r--r-- 1 escape staff 26B Nov 23 21:36 .git/objects/8d/0e41234f24b6da002d962a26c2495ea16a425f
➜ file .git/objects/8d/0e41234f24b6da002d962a26c2495ea16a425f
.git/objects/8d/0e41234f24b6da002d962a26c2495ea16a425f: VAX COFF executable not stripped - version 16694
import zlib
contents = open('0e41234f24b6da002d962a26c2495ea16a425f', 'rb').read()
zlib.decompress(contents)
之前的章节我们也聊到了,当我们执行 git status 命令的时候,Git 工具怎么知道我们有一个文件没有追踪,以及文件名的信息都存储到哪里去了?
搜索公众号GitHub猿后台回复“UML”,获取一份惊喜礼包。
这一切的答案,都要从工作区和索引区讲起。Git 根据其存储的状态不同,将对应状态的“空间”分为工作区、暂存区(也可称为索引区)和版本区三类。具体示例,可以参考下图。
当我们直接查看 index 文件里面的内容,发现使我们无法理解的乱码,但是通过基本的输出,我们可以看到其文件名称。要想查看 index 文件的内容,可以通过 Git 提供的相关命令进行查看。
# 左边执行
$ echo "file1" > file1.txt
$ git add file1.txt
$ cat .git/index
$ git ls-files # 列出当前暂存区的文件列表信息
$ git ls-files -s # 列出当前暂存区文件的详细信息
# 右边执行
$ watch -n 1 -d tree .git
经过如下操作,会使工作区和暂存区和的内容不一致了,通过命令我们也是可以查看区别的。当我们使用 add 命令将新文件添加到暂存区的时候,会发现这下就一致了。
$ git status
$ echo "file2" > file2.txt
$ git ls-files -s
$ git status
$ git add file2.txt
$ git ls-files -s
$ git status
# 右边执行
$ watch -n 1 -d tree .git
# 左边执行
$ git ls-files -s
$ echo "file.txt" > file1.txt
$ git status
# 右边执行
$ watch -n 1 -d tree .git
# 左边执行
$ git ls-files -s
$ git add file1.txt
$ git ls-files -s
# 右边执行
$ watch -n 1 -d tree .git
Git 仓库中的提交记录保存的是你的目录下所有文件的快照,就像是把整个目录复制,然后再粘贴一样,但比复制粘贴优雅许多!Git 希望提交记录尽可能地轻量,因此在你每次进行提交时,它并不会盲目地复制整个目录。条件允许的情况下,它会将当前版本与仓库中的上一个版本进行对比,并把所有的差异打包到一起作为一个提交记录。Git 还保存了提交的历史记录。这也是为什么大多数提交记录的上面都有父节点的原因。
当我们使用 add 命令将工作区提交到暂存区,而暂存区其实保存的是当前文件的一个状态,其中包括有哪些目录和文件,以及其对应的大小和内容等信息。但是我们最终是需要将其提交到代码仓库(本地)的,而其命令就是 git commit 了。
# 左边执行
$ git commit -m "1st commit"
$ git cat-file -t 6e4a700 # 查看 commit 对象的类型
$ git cat-file -p 6e4a700 # 查看 commit 对象的内容
$ git cat-file -t 64d6ef5 # 查看 tree 对象的类型
$ git cat-file -p 64d6ef5 # 查看 tree 对象的内容
# 右边执行
$ watch -n 1 -d tree .git
而这个 6e4a70 这个 commit 对象,有一个 HEAD 的指向,就是 .git 目录下的 HEAD 文件。其实质就是一个指针,其永远指向我们当前工作的分支,即这里我们工作在 master 分支上。当我们切换分支的时候,这个文件的指向也会随机改变的。
# 左边执行
$ cat .git/refs/heads/master
$ cat .git/HEAD
# 右边执行
$ watch -n 1 -d tree .git
加深理解 commit 提交
当我们再次对 file2.txt 文件的内容进行变更、添加以及提交之后,发现在提交的时候,查看的 commit 对象的内容时,其包含有父节点的 commit 信息。而对于理解的话,可以看看下面的这个提交流程图。
# 左边执行
$ echo "file2.txt" > file2.txt
$ git status
$ git add file2.txt
$ git ls-files -s
$ git cat-file -p 0ac9638
$ git commit -m "2nd commit"
$ git cat-file -p bab53ff
$ git cat-file -p 2f07720
# 右边执行
$ watch -n 1 -d tree .git
而当我们通过 commit 命令提交之后,会发现生成了三个 object 对象,因为 commit 操作不会生成 blob 对象,所以分别是一个 commit 对象和两个 tree 对象。可以发现,tree 对象里面有包含了一个目录的 tree,其里面包含对象文件内容。
下图所示的文件状态,可以体会到 Git 中版本的概念。即 commit 对象指向一个该版本中的文件目录树的根(tree),然后 tree 在指向 blob 对象(文件)和 tree 对象(目录),这样就可以无限的往复下去形成一个完整的版本。
# 左边执行
$ mkdir floder1
$ echo "file3" > floder1/file3.txt
$ git add floder1
$ git ls-files -s
$ git commit -m "3rd commit"
$ git cat-file -p 1711e01
$ git cat-file -p 9ab67f8
# 右边执行
$ watch -n 1 -d tree .git
现在,我们已经基本理解了文件如何在工作区、暂存区以及代码仓库之间进行状态的跟踪和同步。在 Git 的操作中,文件的可能状态有哪些,以及如何进行状态切换的,我们这里一起总结一下!
到底什么是分支?分支切换又是怎么一回事?我们通过查看 Git 的官方文档,就可以得到,分支就是一个有名字的(master/dev)指向 commit 对象的一个指针。
我们在初始化仓库的时候,提供会默认给我们分配一个叫做 master 的分支(在最新的版本默认仓库已经变更为 main 了),而 master 分支就是指向最新的一次提交。为什么需要给分支起名字呢?就是为了方便我们使用和记忆,可以简单理解为 alias 命令的意义一致。
在 Git 中,它有一个非常特殊的 HEAD 文件。而 HEAD 文件是一个指针,其有一个特性就是总会指向当前分支的最新的一个 commit 对象。而这个 HEAD 文件正好,解决了我们上面提出的两个问题。
当我们从 master 切换分支到 dev 的时候,HEAD 文件也会随即切换,即指向 dev 这个指针。设计就是这么美丽,不愧是鬼才,好脑袋。
# 左边执行
$ cat .git/HEAD
$ cat .git/refs/heads/master
$ git cat-file -t 1711e01
# 右边执行
$ glo = git log
这里我们可以看到分支切换之后,HEAD 指向发生变动了。
$ git branch
$ git branch dev
$ ll .git/refs/heads
$ cat .git/refs/heads/master
$ cat .git/refs/heads/dev
$ cat .git/HEAD
$ git checkout dev
$ cat .git/HEAD
# 右边执行
$ glo = git log
搜索公众号技术社区后台回复“壁纸”,获取一份惊喜礼包。
# 左边执行
$ echo "dev" > dev.txt
$ git add dev.txt
$ git commit -m "1st commit from dev branch"
$ git checkout master
$ git branch -d dev
$ git branch -D dev
$ git cat-file -t 861832c
$ git cat-file -p 861832c
$ git cat-file -p 680f6e9
$ git cat-file -p 38f8e88
# 右边执行
$ glo = git log
我们执行 checkout 命令的时候,其不光可以切换分支,而且可以切换到指定的 commit 上面,即 HEAD 文件会指向某个 commit 对象。在 Git 里面,将 HEAD 文件没有指向 master 的这个现象称之为 detached HEAD。
这里不管 HEAD 文件指向的是分支名称也好,是 commit 对象也罢,其实本质都是一样的,因为分支名称也是指向某个 commit 对象的。
# 左边执行
$ git checkout 6e4a700
$ git log
# 右边执行
$ glo = git log
$ git checkout -b tmp
$ git log
实际,真的看不到了吗?大家要记住,在 Git 里面任何的操作,比如分支的删除。它只是删除了指向某个特定 commit 的指针引用而已,而那个 commit 本身并不会被删除,即 dev 分支的那个 commit 提交还是在的。
那我们怎么找到这个 commit 呢?找到之后,我们就可以在上面继续工作,或者找到之前的文件数据等。
第一种方法:
[费劲不太好,下下策]
在 objects 目录下面,自己一个一个看,然后切换过去。
[推荐的操作方式]
使用 Git 提供的 git reflog 专用命令来查找。
该命令的作用就是用于将我们之前的所有操作都记录下来。
# 左边执行
$ git reflog
$ git checkout 9fb7a14
$ git checkout -b dev
# 右边执行
$ glo = git log
就在本节中中,我们使用上节的仓库,修改文件内容之后,看看 diff 命令都输出了哪些内容呢?我们这里一起来看看,研究研究!
$ echo "hello" > file1.txt
$ git diff
$ git cat-file -p 42d9955
$ git cat-file -p ce01362
# 下述命令原理也是一样的
$ git diff --cached
$ git diff HEAD
初始化仓库
$ git init
$ git add README.md
$ git commit -m "first commit"
关联远程仓库
当我们使用上述命令来关联远程服务器仓库的时候,我们本地 .git 目录也是会发生改变的。通过命令查看 .git/config 文件的话,可以看到配置文件中出现了 [remote] 字段。
# 关联远程仓库
$ git remote add origin git@github.com:escapelife/git-demo.git
➜ cat .git/config
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
[remote "origin"]
url = git@github.com:escapelife/git-demo.git
fetch = +refs/heads/*:refs/remotes/origin/*
推送本地分支
当我们执行如下命令,将本地 master 分支推送到远程 origin 仓库的 master 分支。之后,我们登陆 GitHub 就可以看到推送的文件及目录内容了。
推送分支内容的时候,会列举推送的 objects 数量,并将其内容进行压缩,之后推送到我们远程的 GitHub 仓库,并且创建了一个远程的 master 分支(origin 仓库)。
# 推送本地分支
$ git push -u origin master
➜ tree .git
├── logs
│ ├── HEAD
│ └── refs
│ ├── heads
│ │ ├── dev
│ │ ├── master
│ │ └── tmp
│ └── remotes # 新增目录
│ └── origin # 新增目录
│ └── master # 新增文件
└── refs
├── heads
│ ├── dev
│ ├── master
│ └── tmp
├── remotes # 新增目录
│ └── origin # 新增目录
│ └── master # 新增文件
└── tags
远程仓库存储代码
当我们编写完代码之后,将其提交到对应的远程服务器上面,其存储结构和我们地址是一模一样的。如果我们仔细想想的话,不一样的话才见怪了。
Git 本来就是代码的分发平台,无中心节点,即每个节点都是主节点,所以其存储的目录结构都是一直的。这样,不管哪一个节点的内容发生丢失或缺失的话,我们都可以通过其他节点来找到。而 Git 服务器就是一个可以帮助我们,实时都可以找到的节点,而已。
你还有什么想要补充的吗?
免责声明:本文内容来源于网络,文章版权归原作者所有,意在传播相关技术知识&行业趋势,供大家学习交流,若涉及作品版权问题,请联系删除或授权事宜。
技术君个人微信
添加技术君个人微信即送一份惊喜大礼包
→ 技术资料共享
→ 技术交流社群
--END--
往日热文:
Github1.3万星,迅猛发展的JAX对比TensorFlow、PyTorch
PyTorch为何如此受欢迎?创始人Soumith亲述「成长秘籍」
Python程序员深度学习的“四大名著”:
这四本书着实很不错!我们都知道现在机器学习、深度学习的资料太多了,面对海量资源,往往陷入到“无从下手”的困惑出境。而且并非所有的书籍都是优质资源,浪费大量的时间是得不偿失的。给大家推荐这几本好书并做简单介绍。
获得方式: