在.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
在Git
基础章节中,我们介绍了暂存区的概念,它工作区和本地仓库中间的一种状态,修改的文件或者新增的文件需要先添加到暂存区,作为待提交的任务,然后,使用git commit
命令将暂存区的待提交的任务提交到本地仓库。
在一些介绍Git
的书籍或者博客中,把暂存区的设计看作是Git
最成功的设计之一,那么该怎么理解这个暂存区呢?
Git
中的暂存区是一个名为index
(存放在.git
文件夹)的索引文件,这个索引文件是一个二进制文件,它包含了文件索引的目录树,就像一个虚拟的工作区。在这个工作区中,记录了文件名和文件的状态信息(例如:时间戳和文件长度等),但并没有存储文件的具体内容,而是建立了文件和对象库中对象实体的关联关系。文件的具体内容存放在.git
文件夹中的objects
子文件夹中。
index
文件是一个二进制文件,使用cat
命令或者在文件编辑器中不能查看该文件的内容,但是,Git
提供了查看该文件的相关命令。为了更好的对比工作区、暂存区和本地仓库的目录树的内容,在这里做个一个统一的介绍。
在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表示对应文件的字节大小。
使用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
输出信息中的第一列表示文件的读写属性,第二列表示对象类型(tree
、blob
、commit
、tag
),第三列表示文件在对象库中的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
选项可以把递归过程中遇到的每颗树都显示出来。
查看本地版本库中的目录树时,需要指定一个提交ID
,通常使用HEAD
来代表当前分支的最新提交:
$ git ls-tree -l HEAD
100644 blob 0c80c621b977bc24b75981f2cd16f6746455b4d5 36 weclome.txt
可以看到,工作区、暂存区和本地仓库的目录树的内容是不一样的:
文件名 | 工作区 | 暂存区 | 本地仓库 |
---|---|---|---|
weclome.txt |
45 | 45 | 36 |
hello.txt |
16 | 7 | - |
有了上面目录树内容的对比分析,现在再回忆之前提到了对比工作区、暂存区和本地仓库的命令,是不是更好理解了呢。
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.
在之前的系列文章中,简单的提到过Git
对象库的相关内容,比如:如何查看对象的类型、查看对象的内容以及HEAD
和master
的关系。这篇文章中上述内容做一个简单的回顾,然后在介绍下SHA1
的值是如何生成的。
在第1章介绍了Git
对象有4种:tree
、commit
、blob
和tag
。其中,tree
对象表示文件夹;blob
对象表示文件,存储着文件的内容;commit
表示一个提交对象,存储着提交者的名称和邮箱、父提交对象的ID
、tree
对象的ID
和提交描述信息等;tag
对象表示标签。那么看到一个SHA1
字符串,如何通过它查看对象的类型和其中的内容呢?
git cat-file -t
:查看对象的类型,例如:git cat-file -t 0c80c
git cat-file -p
:查看对象的内容,例如:git cat-file -p 0c80c
为了方便介绍,以下的内容以默认的master
分支为例来介绍。在Git
项目中,依次使用git log -1 HEAD
、git log -1 master
和git 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]>
从上面的输出内容来看,HEAD
、master
和refs/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
SHA1
是一种数据摘要算法,它能够处理从0到两百多万TB
的输入数据,然后输出为固定160比特的数字摘要,即使两个输入数据量很大但差别很小,SHA1
输出的结果也会有限显著的不同。
Git
中的4中对象的ID
是怎么计算的呢?先以提交对象为例:
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]>
$ git cat-file commit HEAD | wc -c
273
commit 273
(
为空字符),然后执行SHA1
哈希算法$ ( printf "commit 273\000"; git cat-file commit HEAD ) | sha1sum
442c5d0fe2c9242db5483a02f350b1af89fc36ff
git rev-parse
命令查看HEAD
对应的ID
$ git rev-parse HEAD
442c5d0fe2c9242db5483a02f350b1af89fc36ff
可以看到第3步和第4步得到的结果是一样的。如果想查看blob
、tree
和tag
对象ID
的生成方法,可以将上面步骤中的commit
替换成对应的对象类型即可。经过上面的步骤,我们可以了解到Git
中生成对象ID
的方法。
在前面的文章中,我们提到过master
分支(对应着refs/heads/master
文件)就像是指向某个提交对象的一个游标,当此分支上有了新的提交时,该游标就会指向新的提交。同时,Git
中提供了git reset
命令支持游标向后移动,可以指向任意一个存在的提交ID
,例如:设置游标指向最新提交的父提交:git reset --hard HEAD^
。
重置命令git reset
命令是Git
中常用命令之一,也是最危险的几个命令之一,常用的用法有:
git reset [-q] [] [--] ...
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^
:彻底撤销最新的更改,工作区、暂存区和引用都会回退到上一次的状态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
的简写,相对于最新提交的一个顺序表示式:
以及提交的描述内容。其中,
的含义是引用refname
之前的第n
改变是的SHA1
值。
那么如何恢复到错误重置之前的状态呢?
ID
git reset --hard master@{n}
或者git reset --hard 恢复到指定提交对应的状态
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 status
、git add
、git 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
分支。
检出命令的常用格式有:
git checkout [-q] [] -- ...
git checkout []
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 .
:会使用暂存区中的所有文件覆盖工作区中的文件,并且不会给使用者任何提示,因此,这条命令极其危险,需要谨慎使用通过上面的3章内容,能够对下图所描述的流程做出描述呢?欢迎在评论区中讨论~~~