git原理入门

296 阅读4分钟

内容来源: 阮一峰老师的Git 原理入门

  • Workspace,工作区
  • Index/Stage,暂存区
  • Repository,仓库区(或本地仓库)
  • Remote,远程仓库

1、初始化

$ git init

在项目跟目录下运行如上命令,会创建一个.git的目录,用来保存版本信息,内容如下

$ ls .git

HEAD
config
hooks/
objects/
branches/
description
info/        
refs/

2、保存对象

创建一个test.txt文件,然后把这个文件加入git创库

$ touch test.txt
$ git hash-object -w test.txt

e69de29bb2d1d6434b8b29ae775ad8c2e48c5391

git hash-object命令把test.txt的当前内容压缩成二进制文件,存入 Git。压缩后的二进制文件,称为一个 Git 对象,保存在.git/objects目录。

这个命令还会计算当前内容的 SHA1 哈希值(长度40的字符串),目录名是哈希值的前2个字符,该子目录下面有一个文件,文件名是哈希值的后38个字符。

$ ls -R .git/object

.git/objects/e6:
9de29bb2d1d6434b8b29ae775ad8c2e48c5391

向文件中写入一下内容

$ echo 'hello world' > test.txt

因为文件内容已经改变,需要将它再次保存成 Git 对象。

$ git hash-object -w test.txt

7fd5222177e8ffadda6437dc9cfa0630a2777673

如果想看该文件原始的文本内容,要用git cat-file命令

$ git cat-file -p 7fd5222177e8ffadda6437dc9cfa0630a2777673

hello word

3、更新暂存区

文件保存成二进制对象以后,还需要通知 Git 哪些文件发生了变动。所有变动的文件,Git 都记录在一个区域,叫做"暂存区"(英文叫做 index 或者 stage)。等到变动告一段落,再统一把暂存区里面的文件写入正式的版本历史。

$ git update-index --add --cacheinfo 100644
7fd5222177e8ffadda6437dc9cfa0630a2777673 test.txt 

上面命令向暂存区写入文件名test.txt、二进制对象名(哈希值)和文件权限。 git ls-files命令可以显示暂存区当前的内容。

$ git ls-files --stage

100644 7fd5222177e8ffadda6437dc9cfa0630a2777673  0  test.txt

git status命令会产生更可读的结果

$ git status

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)

        new file:   test.txt

4、git add 命令

上面两步(保存对象和更新暂存区),如果每个文件都做一遍,那是很麻烦的。Git 提供了git add命令简化操作。

$ git add --add
或
$ git add .

上面命令相当于,对当前项目所有变动的文件,执行前面的两步操作。

5、commit的概念

暂存区保留本次变动的文件信息,等到修改了差不多了,就要把这些信息写入历史,这就相当于生成了当前项目的一个快照(snapshot)。

项目的历史就是由不同时点的快照构成。Git 可以将项目恢复到任意一个快照。快照在 Git 里面有一个专门名词,叫做 commit,生成快照又称为完成一次提交。

下文所有提到"快照"的地方,指的就是 commit。

6、完成提交

首先,设置一下用户名和 Email,保存快照的时候,会记录是谁提交的。

$ git config user.name 'xxx'
$ git config user.email 'yyy@qq.com'

前面保存对象的时候,只是保存单个文件,并没有记录文件之间的目录关系(哪个文件在哪里)。接下来,要保存当前的目录结构。 git write-tree命令用来将当前的目录结构,生成一个 Git 对象。

$ git write-tree

1e537aea8ee1f19012f944d9d62470a8f216fd2b

上面代码中,目录结构也是作为二进制对象保存的,也保存在.git/objects目录里面,对象名就是哈希值。

让我们看一下这个文件的内容。

$ git cat-file -p 1e537aea8ee1f19012f944d9d62470a8f216fd2b

100644 blob 7fd5222177e8ffadda6437dc9cfa0630a2777673    test.txt

所谓快照,就是保存当前的目录结构,以及每个文件对应的二进制对象。上一个操作,目录结构已经保存好了,现在需要将这个目录结构与一些元数据一起写入版本历史。

git commit-tree命令将元数据和目录树,一起生成一个 Git 对象

$ echo 'my first commit' | git commit-tree 1e537aea8ee1f19012f944d9d62470a8f216fd2b

1805dc004ca3b073e68efd3118d9da8647b58656

看一下这个对象的内容

$ git cat-file -p 1805dc004ca3b073e68efd3118d9da8647b58656

tree 1e537aea8ee1f19012f944d9d62470a8f216fd2b
author xxx <xxx@qq.com> 1594455305 +0800
committer xxx <xxx@qqcom> 1594455305 +0800

my first commit

上面代码中,输出结果的第一行是本次快照对应的目录树对象(tree),第二行和第三行是作者和提交人信息,最后是提交说明。

git log命令也可以用来查看某个快照信息。

$ git log --stat 1805dc004ca3b073e68efd3118d9da8647b58656

commit 1805dc004ca3b073e68efd3118d9da8647b58656
Author: yongfen1 <yongfen1@staff.sina.com.cn>
Date:   Sat Jul 11 16:15:05 2020 +0800

    my first commit

 test.txt | 1 +
 1 file changed, 1 insertion(+)

7、git commit

Git 提供了git commit命令,简化提交操作。保存进暂存区以后,只要git commit一个命令,就同时提交目录结构和说明,生成快照。

$ git commit -m "my first commit"

git checkout命令用于切换到某个快照。

$ git checkout 1805dc004ca3b073e68efd3118d9da8647b58656

git show命令用于展示某个快照的所有代码变动。

$ git show 1805dc004ca3b073e68efd3118d9da8647b58656

commit 1805dc004ca3b073e68efd3118d9da8647b58656
Author: xxx <xxx@qq.com>
Date:   Sat Jul 11 16:15:05 2020 +0800

    my first commit

diff --git a/test.txt b/test.txt
new file mode 100644
index 0000000..7fd5222
--- /dev/null
+++ b/test.txt
@@ -0,0 +1 @@
+hello word

8、branch概念

到了这一步,还没完。如果这时用git log命令查看整个版本历史,你看不到新生成的快照。

$ git log

上面命令没有任何输出,这是为什么呢?快照明明已经写入历史了。

原来git log命令只显示当前分支的变动,虽然我们前面已经提交了快照,但是还没有记录这个快照属于哪个分支。

所谓分支(branch)就是指向某个快照的指针,分支名就是指针名。哈希值是无法记忆的,分支使得用户可以为快照起别名。而且,分支会自动更新,如果当前分支有新的快照,指针就会自动指向它。比如,master 分支就是有一个叫做 master 指针,它指向的快照就是 master 分支的当前快照。

用户可以对任意快照新建指针。比如,新建一个 fix-typo 分支,就是创建一个叫做 fix-typo 的指针,指向某个快照。所以,Git 新建分支特别容易,成本极低。

Git 有一个特殊指针HEAD, 总是指向当前分支的最近一次快照。另外,Git 还提供简写方式,HEAD^指向 HEAD的前一个快照(父节点),HEAD~6则是HEAD之前的第6个快照。

每一个分支指针都是一个文本文件,保存在.git/refs/heads/目录,该文件的内容就是它所指向的快照的二进制对象名(哈希值)。

9、更新分支

首先,修改一下test.txt

$ echo "hello world again" > test.txt

然后,保存二进制对象

$ git hash-object -w test.txt

7fd5222177e8ffadda6437dc9cfa0630a2777673

接着,将这个对象写入暂存区,并保存目录结构

$ git update-index test.txt
$ git write-tree

1e537aea8ee1f19012f944d9d62470a8f216fd2b

最后,提交目录结构,生成一个快照

$ echo "second commit" | git commit-tree 1e537aea8ee1f19012f944d9d62470a8f216fd2

1805dc004ca3b073e68efd3118d9da8647b58656

现在,我们把本次快照的哈希值,写入.git/refs/heads/master文件,这样就使得master指针指向这个快照。

 echo 1805dc004ca3b073e68efd3118d9da8647b58656 > .git/refs/heads/master

现在,git log就可以看到快照了。

$ git log

commit 1805dc004ca3b073e68efd3118d9da8647b58656 (HEAD -> master)
Author: xxx <xxx@qq.com>
Date:   Sat Jul 11 16:15:05 2020 +0800

    my first commit

10、参考链接

How does git work internally, Shalitha Suranga