深入理解Git(1)

1 .git文件夹目录结构

.git文件夹中执行tree命令,查看文件列表。

$ tree
├── branches   
├── COMMIT_EDITMSG  # 存储最新的提交信息
├── config      # 存储本地仓库的Git配置信息
├── description # 仓库的描述信息,主要是Git托管系统使用
├── HEAD        # 一个指针,指向正在工作中的本地分支的指针,内容为映射到refs的引用
├── hooks       # Git执行特定操作的后出发的一些shell脚本
│   ├── applypatch-msg.sample
│   ├── commit-msg.sample
│   ├── fsmonitor-watchman.sample
│   ├── post-update.sample
│   ├── pre-applypatch.sample
│   ├── pre-commit.sample
│   ├── pre-merge-commit.sample
│   ├── prepare-commit-msg.sample
│   ├── pre-push.sample
│   ├── pre-rebase.sample
│   ├── pre-receive.sample
│   └── update.sample
├── index      # 代表暂存区
├── info       # 存放全局性的排除文件,和.gitignore文件互补,只有一个exclude文件
│   └── exclude
├── logs       # 存储所有的更新的引用记录
│   ├── HEAD
│   └── refs
│       ├── heads
│       │   └── master
│       └── remotes
│           └── origin
│               └── HEAD
├── objects    # 对象目录,存储内容,对象有:commit、blob、tree、tag
│   ├── info
│   └── pack
│       ├── pack-459a02c6ba0c6dd954a874752a7d1641d3f6899c.idx
│       └── pack-459a02c6ba0c6dd954a874752a7d1641d3f6899c.pack
└── refs       # 对象的引用
    ├── heads
    │   └── master
    ├── remotes
    │   └── origin
    │       └── HEAD
    └── tags

2 深入理解Git的暂存区

Git基础章节中,我们介绍了暂存区的概念,它工作区和本地仓库中间的一种状态,修改的文件或者新增的文件需要先添加到暂存区,作为待提交的任务,然后,使用git commit命令将暂存区的待提交的任务提交到本地仓库。

在一些介绍Git的书籍或者博客中,把暂存区的设计看作是Git最成功的设计之一,那么该怎么理解这个暂存区呢?

Git中的暂存区是一个名为index(存放在.git文件夹)的索引文件,这个索引文件是一个二进制文件,它包含了文件索引的目录树,就像一个虚拟的工作区。在这个工作区中,记录了文件名和文件的状态信息(例如:时间戳和文件长度等),但并没有存储文件的具体内容,而是建立了文件和对象库中对象实体的关联关系。文件的具体内容存放在.git文件夹中的objects子文件夹中。

2.1 查看工作区、暂存区和本地仓库的目录树

index文件是一个二进制文件,使用cat命令或者在文件编辑器中不能查看该文件的内容,但是,Git提供了查看该文件的相关命令。为了更好的对比工作区、暂存区和本地仓库的目录树的内容,在这里做个一个统一的介绍。

  1. 查看工作区的目录树

Window系统中可通过文件夹的方式查看,在Linux系统也可以使用shell命令查看:

$ find . -path ./.git -prune -o -type f -printf "%-20p\t%s\n"
./weclome.txt       	45
./a/b/c/hello.txt   	16

其中,45和16表示对应文件的字节大小。

  1. 查看暂存区中的目录树

使用git ls-files -s命令查看暂存区的目录树:

$ git ls-files -s
100644 18832d35117ef2f013c4009f5b2128dfaeff354f 0	a/b/c/hello.txt
100644 9a94974a88cbb47f0744da627a052c0a166d21b6 0	weclome.txt

其中,输出信息中第一列表示文件的读写属性(rw-r--r--),第二列表示该文件在对象库中的ID,第三项表示暂存区的编号(从0开始),第四项表示文件的路径。

也可使用git ls-tree命令查看暂存区的目录树,但是需要先执行git write-tree命令,将暂存区中的目录树写入Git对象库中:

$ git write-tree
f267b7db7ed8833a18a0c79a998ffbb97dcb373f
$ git ls-tree -l f267
040000 tree 53583ee687fbb2e913d18d508aefd512465b2092       -	a
100644 blob 9a94974a88cbb47f0744da627a052c0a166d21b6      45	weclome.txt

git write-tree的输出就是写入Git对象库中的目录树ID,然后ls-tree命令使用该ID查看对应目录树的内容。

git ls-tree输出信息中的第一列表示文件的读写属性,第二列表示对象类型(treeblobcommittag),第三列表示文件在对象库中的ID,第四项表示文件的大小,第五项表示文件或者文件夹的路径。和git ls-files的输出对比来看,git ls-tree不能直接显示子文件夹中的文件,需要使用递归来查看。

$ git write-tree | xargs git ls-tree -l -r -t
040000 tree 53583ee687fbb2e913d18d508aefd512465b2092       -	a
040000 tree 514d729095b7bc203cf336723af710d41b84867b       -	a/b
040000 tree deaec688e84302d4a0b98a1b78a434be1b22ca02       -	a/b/c
100644 blob 18832d35117ef2f013c4009f5b2128dfaeff354f       7	a/b/c/hello.txt
100644 blob 9a94974a88cbb47f0744da627a052c0a166d21b6      45	weclome.txt

其中,-r选项可以递归显示目录的内容,-t选项可以把递归过程中遇到的每颗树都显示出来。

  1. 查看本地版本库中的目录树

查看本地版本库中的目录树时,需要指定一个提交ID,通常使用HEAD来代表当前分支的最新提交:

$ git ls-tree -l HEAD
100644 blob 0c80c621b977bc24b75981f2cd16f6746455b4d5      36	weclome.txt

可以看到,工作区、暂存区和本地仓库的目录树的内容是不一样的:

文件名 工作区 暂存区 本地仓库
weclome.txt 45 45 36
hello.txt 16 7 -

2.2 git diff对比文件差异

有了上面目录树内容的对比分析,现在再回忆之前提到了对比工作区、暂存区和本地仓库的命令,是不是更好理解了呢。

  • 对比工作区与暂存区的文件差异:git diff
$ git diff
diff --git a/a/b/c/hello.txt b/a/b/c/hello.txt
index 18832d3..e8577ea 100644
--- a/a/b/c/hello.txt
+++ b/a/b/c/hello.txt
@@ -1 +1,2 @@
 Hello.
+Bye-Bye.
  • 对比工作区与本地仓库的文件差异:git diff HEAD或者git diff master,其中master为默认的分支名称
$ git diff HEAD
diff --git a/a/b/c/hello.txt b/a/b/c/hello.txt
new file mode 100644
index 0000000..e8577ea
--- /dev/null
+++ b/a/b/c/hello.txt
@@ -0,0 +1,2 @@
+Hello.
+Bye-Bye.
diff --git a/weclome.txt b/weclome.txt
index 0c80c62..9a94974 100644
--- a/weclome.txt
+++ b/weclome.txt
@@ -1,3 +1,4 @@
 Hello.
 allow-empty 测试
 -s 测试
+Bye-Bye.
  • 对比暂存区和本地仓库的文件差异:git diff --cached或者git diff --cached HEAD
$ git diff --cached
diff --git a/a/b/c/hello.txt b/a/b/c/hello.txt
new file mode 100644
index 0000000..18832d3
--- /dev/null
+++ b/a/b/c/hello.txt
@@ -0,0 +1 @@
+Hello.
diff --git a/weclome.txt b/weclome.txt
index 0c80c62..9a94974 100644
--- a/weclome.txt
+++ b/weclome.txt
@@ -1,3 +1,4 @@
 Hello.
 allow-empty 测试
 -s 测试
+Bye-Bye.

3 深入理解Git对象库

在之前的系列文章中,简单的提到过Git对象库的相关内容,比如:如何查看对象的类型、查看对象的内容以及HEADmaster的关系。这篇文章中上述内容做一个简单的回顾,然后在介绍下SHA1的值是如何生成的。

3.1 查看Git对象的类型和内容

在第1章介绍了Git对象有4种:treecommitblobtag。其中,tree对象表示文件夹;blob对象表示文件,存储着文件的内容;commit表示一个提交对象,存储着提交者的名称和邮箱、父提交对象的IDtree对象的ID和提交描述信息等;tag对象表示标签。那么看到一个SHA1字符串,如何通过它查看对象的类型和其中的内容呢?

  • git cat-file -t :查看对象的类型,例如:git cat-file -t 0c80c
  • git cat-file -p :查看对象的内容,例如:git cat-file -p 0c80c

3.2 理解HEAD和分支的关系

为了方便介绍,以下的内容以默认的master分支为例来介绍。在Git项目中,依次使用git log -1 HEADgit log -1 mastergit log -1 refs/head/master三个命令查看对应的输出:

$ git log -1 HEAD
commit 442c5d0fe2c9242db5483a02f350b1af89fc36ff (HEAD -> master)
Author: jiaoxiangning <[email protected]>
Date:   Mon Feb 15 16:44:15 2021 +0800

    -s测试
    
    Signed-off-by: jiaoxiangning <[email protected]>
$ git log -1 master
commit 442c5d0fe2c9242db5483a02f350b1af89fc36ff (HEAD -> master)
Author: jiaoxiangning <[email protected]>
Date:   Mon Feb 15 16:44:15 2021 +0800

    -s测试
    
    Signed-off-by: jiaoxiangning <[email protected]>
$ git log -l refs/heads/master
commit 442c5d0fe2c9242db5483a02f350b1af89fc36ff (HEAD -> master)
Author: jiaoxiangning <[email protected]>
Date:   Mon Feb 15 16:44:15 2021 +0800

    -s测试
    
    Signed-off-by: jiaoxiangning <[email protected]>

从上面的输出内容来看,HEADmasterrefs/heads/master具有同样的内容指向。使用命令分别查看.git/HEAD.git/refs/heads/master文件的内容:

$ cat .git/HEAD
ref: refs/heads/master
$ cat .git/refs/heads/master
442c5d0fe2c9242db5483a02f350b1af89fc36ff

可以发现,HEAD是对于当前分支(master为例)的引用,而refs/heads/master文件内容则指向了某个提交内容的ID

$ git cat-file -t 442c
commit
$ git cat-file -p 442c
tree 43f4d4c26dadf4b1fe68d559915d0f2ea8c1b1db
parent 1900cf08263383b09616613b9fbb0e71b2162f46
author jiaoxiangning <[email protected]> 1613378655 +0800
committer jiaoxiangning <[email protected]> 1613378655 +0800

-s测试

Signed-off-by: jiaoxiangning <[email protected]>

refs/heads文件夹中保存引用的命名空间,其中heads子目录下的引用又称为分支,分支的表示方式有:正规的长格式表达方法,例如:refs/heads/master;或者去掉前面2级目录,使用master表示。例如:包含example分支和master分支的Git项目:

$ ls .git/refs/heads
example  master
$ cat .git/refs/heads/example
70d588a5d5efca0b2297f9be45afb923b9db643f
$ cat .git/refs/heads/master
310cb8cfab7596edd7185f0957e43dd0e797cee5

3.3 SHA1生成方法

SHA1是一种数据摘要算法,它能够处理从0到两百多万TB的输入数据,然后输出为固定160比特的数字摘要,即使两个输入数据量很大但差别很小,SHA1输出的结果也会有限显著的不同。

Git中的4中对象的ID是怎么计算的呢?先以提交对象为例:

  1. 查看HEAD对应的提交内容
$ git cat-file commit HEAD
tree 43f4d4c26dadf4b1fe68d559915d0f2ea8c1b1db
parent 1900cf08263383b09616613b9fbb0e71b2162f46
author jiaoxiangning <[email protected]> 1613378655 +0800
committer jiaoxiangning <[email protected]> 1613378655 +0800

-s测试

Signed-off-by: jiaoxiangning <[email protected]>
  1. 查看提交信息的数据量
$ git cat-file commit HEAD | wc -c
273
  1. 在提交信息前添加内容commit 273为空字符),然后执行SHA1哈希算法
$ ( printf "commit 273\000"; git cat-file commit HEAD ) | sha1sum
442c5d0fe2c9242db5483a02f350b1af89fc36ff
  1. 使用git rev-parse命令查看HEAD对应的ID
$ git rev-parse HEAD
442c5d0fe2c9242db5483a02f350b1af89fc36ff

可以看到第3步和第4步得到的结果是一样的。如果想查看blobtreetag对象ID的生成方法,可以将上面步骤中的commit替换成对应的对象类型即可。经过上面的步骤,我们可以了解到Git中生成对象ID的方法。

4 深入理解Git重置

4.1 理解git reset命令

在前面的文章中,我们提到过master分支(对应着refs/heads/master文件)就像是指向某个提交对象的一个游标,当此分支上有了新的提交时,该游标就会指向新的提交。同时,Git中提供了git reset命令支持游标向后移动,可以指向任意一个存在的提交ID,例如:设置游标指向最新提交的父提交:git reset --hard HEAD^

重置命令git reset命令是Git中常用命令之一,也是最危险的几个命令之一,常用的用法有:

  1. git reset [-q] [] [--] ...
  2. git reset [--soft | --hard | --mixed | --hard | --merge | --keep] [-q] []

上面中[]表示都是可选项,其中,如果不提供commit_id,那么默认使用HEAD指向的提交的ID

第1种方法需要提供一个文件路径,它不会重置引用,也不会更改工作区,而是用指定提交中的对应的文件替换暂存区中的相应的内容,相当于在没有执行commit的情况下取消git add 这条命令对于暂存区中的修改。

第2种方法则会重置引用,同时,根据不同的设置选项,选择性对暂存区和工作区进行重置:

  • --hard选项,即:git reset --hard 命令,将会重置引用(修改master的指向),并使用新的引用的目录树替换暂存区和工作区中的目录树
  • --soft选项,即:git reset --soft 命令,将会重置引用(修改master的指向),但不改变暂存区和工作区的目录树
  • --mixed选项,即:git reset --mixed 命令,将会重置引用(修改master的指向),并使用新的引用的目录树替换暂存区的目录树,但不会修改工作区的目录树。git reset 模式使用--mixed选项

这里列出几个常用的重置命令并对其作出解释:

  • git reset:使用HEAD指向的目录树重置暂存区,工作区的目录树不会收到影响,并且引用也不会被重置,因为重置引用到HEAD相当于没有重置,这条命令的效果相当于把git add命令提交的内容从暂存区移除
  • git reset --mixed:同上
  • git reset HEAD:同上
  • git reset -- :不重置引用,使用HEAD指向的目录树中的对应文件替换暂存区的文件,相当于是对git add 命令的反向操作
  • git reset HEAD :同上
  • git reset --soft HEAD^:重置引用为HEAD的父提交对象,工作区和暂存区保持不变,如果对于刚刚提交的说明或者内容不满意,可以使用该命令将引用回退;还记之前介绍的git commit --amend命令吗,它也能覆盖上一次的提交,其实该命令相当于使用git reset --soft HEAD^git commit -e -F .git/COMMIT_EDITMSG两条命令,其中最后一条命令是修改上一次提交的描述信息。
  • git reset HEAD^:重置引用为HEAD的父提交对象,并且暂存区也会替换为上一次提交之前,但工作区不会改变
  • git reset --hard HEAD^:彻底撤销最新的更改,工作区、暂存区和引用都会回退到上一次的状态

4.2 使用reflog挽救错误的重置

Git提供了对于错误的重置的挽救机制,那就是通过日志文件(.git/logs)记录分支的变更,可凭借日志文件修复错误的重置。先来看看master分支的日志文件中的内容是什么。

$ git reflog show master
442c5d0 (HEAD -> master) master@{0}: commit: -s测试
1900cf0 master@{1}: commit (amend): amend test
fe76cc5 master@{2}: commit: allow-empty example
4d6636a master@{3}: commit: allow-empty测试
36574f3 master@{4}: commit (initial): initialized

日志文件中记录了master分支指向内容的变迁,最新的指向内容在输出结果的最上方。输出结果中包含了提交ID的简写,相对于最新提交的一个顺序表示式:@{n}以及提交的描述内容。其中,@{n}的含义是引用refname之前的第n改变是的SHA1值。

那么如何恢复到错误重置之前的状态呢?

  1. 使用上述命令查看错误重置之前的提交ID
  2. 使用git reset --hard master@{n}或者git reset --hard 恢复到指定提交对应的状态

5 深入理解Git检出

Git中检出本质上是HEAD的重置。在之前的Git分支的文章中,介绍过了HEAD是描述当前工作分支的最新状态,在切换分支时,HEAD游标(或指针)指向的内容会自动变为目标分支。除了之前介绍过的不同分支间的检出,也支持检出特定的提交,即:git checkout

$ git checkout 442c5
Note: switching to '442c5'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:

  git switch -c 

Or undo this operation with:

  git switch -

Turn off this advice by setting config variable advice.detachedHead to false

HEAD is now at 442c5d0 -s测试

上面的输出内容中,提到了一个状态:detached HEAD,中文名是“分离头指针”。“分离头指针”是指HEAD引用的不是一个分支,而是某个具体的提交对象。

$ cat .git/HEAD
442c5d0fe2c9242db5483a02f350b1af89fc36ff

在“分离头指针”状态中,可以完成git statusgit addgit push命令,但会提示警告。

$ touch bb.txt
$ git status
HEAD detached from 442c5d0
Untracked files:
  (use "git add ..." to include in what will be committed)
	bb.txt

nothing added to commit but untracked files present (use "git add" to track)
$ git add bb.txt
$ git status
HEAD detached from 442c5d0
Changes to be committed:
  (use "git restore --staged ..." to unstage)
	new file:   bb.txt
$ git commit -m "bb.txt"
[detached HEAD b63b504] bb.txt
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 bb.tx

上面的输出结果中,均有detached HEAD警告信息。

如果要将“分离头指针”的内容整合到master指针中,可是使用merge命令,例如:在master分支中使用git merge 442c5命令将内容合并到master分支。

检出命令的常用格式有:

  1. git checkout [-q] [] -- ...
  2. git checkout []
  3. git checkout [-m] [[-b|--orphan] ] []

第1种格式,使用指定版本中的文件覆盖工作区的文件。如果提供commit,会修改HEAD指针的引用,进入“分离头指针”状态;如果提供file_path,不会修改HEAD指针,并使用指定提交中或者只能暂存区中的文件覆盖工作区的文件,不提供commit将会使用暂存区中文件。

第2种格式,在之前的Git分支的文章中经常使用,它会修改HEAD指针的引用,切换到特定的分支进行追踪,如果省略``branch`将会进行工作区状态检查。

第3种格式,主要是创建和切换到新的分支。之前的文章中也经常使用到git checkout -b 命令。这里提一下start_point的使用,在创建新分支时,如果提供了start_point,将会以此为基础创建新分支,如果没有提供,则会在HEAD指向的基础上创建新的分支。

下面列出一些常用的git checkout命令:

  • git checkout master:切换到master分支,HEAD指针指向master分支,并使用master分支对应的目录树更新暂存区和工作区
  • git checkout:汇总显示工作区、暂存区和HEAD的差异,如果没有输出结果,表示三者无差异
  • git checkout HEAD:同上
  • git checkout -- :用暂存区中对应的文件覆盖工作区中文件
  • git checkout branch -- :保证HEAD的指向不变,使用branch对应的提交中的file_name内容替换暂存区和工作中的对应的文件。
  • git checkout -- .或者git checkout .:会使用暂存区中的所有文件覆盖工作区中的文件,并且不会给使用者任何提示,因此,这条命令极其危险,需要谨慎使用

6 总结

通过上面的3章内容,能够对下图所描述的流程做出描述呢?欢迎在评论区中讨论~~~

深入理解Git(1)_第1张图片

你可能感兴趣的:(Git系列文章,git)