Git权威指南
Git权威指南
第4章 git初始化
创建版本库及第一次提交
告诉Git当前用户的姓名和邮件地址,配置的用户名和邮件地址将在版本库提交时用到
1
2git config --global user.name "[用户名]"
git config --global user.email "[邮件地址]"初始化仓库
1
2
3
4mkdir demo
cd demo
git init
Initialized empty Git repository in D:/workspace/git/demo/.git/如果Git版本是1.6.5以上,可以在
git init
命令后面跟上目录名称,会自动完成目录创建1
2git init demo
Initialized empty Git repository in D:/workspace/git/demo/.git/
至此仓库即创建成功
现在在工作区中创建一个文件welcome.txt
1 | echo "Hello." > welcome.txt |
将新建立的文件添加到版本库
1 | git add welcome.txt |
Git和大部分其他版本控制系统一样,都需要执行一次提交操作,对于Git来说就是执行
git commit
命令。在提交过程中需要输入提交说明,这个要求对Git来说是强制性的(不接受空白提交说明),就算在提交时不在命令行提供,Git会自动打开一个编辑器要求输入提交说明的。
使用-m
参数直接给出提交说明
1 | git commit -m "initialized." |
- 通过
-m
参数设置提交说明 - 命令输出显示此次提交在名为master的分支上,且是该分支的第一次提交(root-commit),提交ID是dbda1b1。
- 此次提交修改了一个文件,包含一行插入(新增)
- 此次提交创建了新文件welcome.txt
如果之前没有设置用户名和邮箱,则会出现如下内容:
1 | Author identity unknown |
思考:为什么工作区根目录有一个.git目录
初始化仓库之后,会在根目录出现名为.git
的隐藏目录
Windows用户需要通过如下方式设置后才可以看到
Linux用户通过以下命令可查看
1 | ls -a |
Git以及其他分布式版本控制系统的一个共同的显著特点是,版本库位于工作区的根目录下。对Git来说就是.git
目录,且仅此一处,没有任何其他跟踪文件或目录了。
传统集中式版本控制系统的版本库和工作区是分开的,甚至是在不同的主机上,因此必须建立工作区和版本库的对应。
Git将版本库(.git
目录)放在工作区根目录下,那么Git的相关操作一定要在工作区根目录下执行吗?换句话说,当工作区中包含子目录,并在子目录中执行Git命令,如何定位到版本库的?
实际上,当在Git工作区的某个子目录下执行操作的时候,会在工作区目录中依次向上递归查找.git
目录,找到的.git
目录就是工作区对应的版本库了。
例如在非Git工作区执行git
命令时会因为找不到.git
目录报错
1 | cd /d/workspace/git |
使用strace
命令去跟踪执行git status
命令时的磁盘访问,可以看到沿目录依次向上递归的过程。
1 | strace -e 'trace=file' git status |
显示版本库.git
目录所在位置
1 | git rev-parse --git-dir |
显示工作区根目录
1 | git rev-parse --show-toplevel |
相对于工作区根目录的相对目录
1 | git rev-parse --show-prefix |
显示从当前目录后退到工作区根目录的深度
1 | git rev-parse --show-cdup |
思考:git config命令的各参数区别
Git的配置文件分别是版本库级别、全局(用户主目录下,用户级)和系统级别。
--global
:全局级别
--system
:系统级别
第5章 git暂存区
修改不能直接提交吗
首先进行文件追加
1 | echo "Nice to meet you." >> welcome.txt |
1 | git diff |
需要执行git add
命令将文件修改添加到暂存区之后才可以进行提交
如果直接执行提交操作
1 | git commit -m "Append a nice line." |
理解Git暂存区(stage)
在版本库.git
目录下有一个index文件,下面针对这个文件做一个实验
撤销工作区中对welcome.txt文件尚未提交的更改
1
2git checkout -- welcome.txt # 撤销工作区中对welcome.txt文件尚未提交的更改
git status -s # 如果git版本小于1.7.3,执行git diff查看.git/index文件
1
2
3ls --full-time .git/index
-rw-r--r-- 1 User 197121 145 2022-05-16 19:17:16.292820100 +0800 .git/index再次执行
1
2
3
4git status -s # 再次执行
ls --full-time .git/index
-rw-r--r-- 1 User 197121 145 2022-05-16 19:17:16.292820100 +0800 .git/index可以看到时间戳不变
现在修改下welcome.txt的时间戳,但是不改变它的内容
1
2
3
4
5
6
7
8
9ls --full-time welcome.txt
-rw-r--r-- 1 User 197121 25 2022-05-16 19:18:32.223701400 +0800 welcome.txt
touch welcome.txt
ls --full-time welcome.txt # 可以看到时间戳更新了
-rw-r--r-- 1 User 197121 25 2022-05-16 19:24:35.298879700 +0800 welcome.txt
git status -s
ls --full-time .git/index # 时间戳更新了
-rw-r--r-- 1 User 197121 145 2022-05-16 19:25:20.322562900 +0800 .git/index
实验说明git status
命令是先根据.git/index文件中记录的(用于跟踪工作区文件的)时间戳、长度等信息判断工作区文件是否改变,如果工作区文件的时间戳改变了,说明文件的内容可能被改变了,需要打开文件,读取文件内容进行比较。如果文件内容没变,则将该文件的新时间戳记录到.git/index文件中。因为如果要判断文件是否更改,使用时间戳、文件长度等信息进行比较要比通过文件内容比较要快得多,这也是Git高效的原因之一。
文件.git/index实际就是一个包含文件索引的目录数,记录了文件名和文件的状态信息(时间戳和文件长度等)。文件内容保存在.git/objects目录中
- 左侧为工作区,右侧为版本库。在版本库中标记为index的区域是暂存区,标记为master的是master分支所代表的目录树。
- 此时HEAD实际是指向master分支的一个“游标”,所以图示的命令中出现HEAD的地方可以用master替换。
- 图中的objects为Git对象库,实际位于.git/objects目录下。
- 当对工作区修改(或新增)的文件执行
git add
命令时,暂存区的目录树将被更新,同时工作区修改(或新增)的文件内容会被写入对象库中的一个新的对象中,而该对象的ID被记录在暂存区的文件索引中。 - 当执行
git commit
时,暂存区的目录树会写到版本库(对象库)中,master分支会做相应的更新,即master最新指向的目录树就是提交时原暂存区的目录树。 - 当执行
git reset HEAD
命令时,暂存区的目录树会被重写,会被master分支指向的目录树所替换,但是工作区不受影响。 - 当执行
git rm --cached <file>
命令时,会直接从暂存区删除文件,工作区不做出改变。 - 当执行
git checkout .
或git checkout -- <file>
命令时,会用暂存区全部文件或指定文件替换工作区文件。这个操作很危险,会清除工作区中未添加到暂存区的改动。 - 当执行
git checkout HEAD .
或git checkout HEAD <file>
命令时,会用HEAD指向的master分支中的全部或部分文件替换暂存区和工作区中的文件。这个命令极其危险,因为不但会清楚工作区中未提交的改动,也会清除暂存区中未提交的改动。
栗子:
1 | touch new.txt # 创建新文件 |
git diff
工作区、暂存区和版本库的目录树浏览
直观地查看暂存区及HEAD中的目录树?
对于HEAD(版本库中当前提交)指向的目录树,可以使用Git底层命令ls-tree
查看
1 | git ls-tree -l HEAD |
-l
参数可以显示文件的大小
git ls-files
命令可以显示暂存区目录树
1 | git ls-files -s |
第三列不是文件大小,而是文件编号
git diff
工作区与暂存区比较
1
2
3
4
5
6
7
8git diff
diff --git a/new.txt b/new.txt
index e69de29..2595e07 100644
--- a/new.txt
+++ b/new.txt
@@ -0,0 +1 @@
+new line.暂存区和HEAD比较
1
2
3
4
5
6
7
8
9git diff --cached
index fd3c069..9ac7e07 100644
--- a/welcome.txt
+++ b/welcome.txt
@@ -1,2 +1,3 @@
Hello.
Nice to meet you.
+Bye bye.工作区和HEAD比较
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16git diff HEAD
diff --git a/new.txt b/new.txt
index e69de29..2595e07 100644
--- a/new.txt
+++ b/new.txt
@@ -0,0 +1 @@
+new line.
diff --git a/welcome.txt b/welcome.txt
index fd3c069..9ac7e07 100644
--- a/welcome.txt
+++ b/welcome.txt
@@ -1,2 +1,3 @@
Hello.
Nice to meet you.
+Bye bye.
第6章 git对象
Git对象库
1 | git log -1 --pretty=raw |
研究Git对象ID可以使用git cat-file
命令
1 | git cat-file -t <ID> # 查看ID类型 |
SHA1哈希值是什么
哈希是一种摘要算法(散列算法),可以将任意长度的输入经过散列运算转换为固定长度的输出。
比较著名的摘要算法有MD5和SHA1
Linux下使用sha1sum
命令可以生成摘要
1 | printf Git | sha1sum |
提交的SHA1哈希值是如何生成的
查看HEAD对应的提交内容
1
2
3
4
5
6
7
8git cat-file commit HEAD
tree f341012ebeaf74262be6892be96ecce373a68ef3
parent ca04d27bdf1a442d67c379fd5803f5b85c868bd5
author Crayon <614820984@qq.com> 1652703487 +0800
committer Crayon <614820984@qq.com> 1652703487 +0800
add new.txt提交信息中总共包含个210字符
1
2
3git cat-file commit HEAD | wc -c
210在提交信息的前面加上内容commit 210<null>(<null>为空字符),然后执行SHA1算法
1
2
3
4
5
6(printf "commit 210\000"; git cat-file commit HEAD) | sha1sum
86343253f0d44b3a002b11423c02587fd3040e07 *-
git rev-parse HEAD
86343253f0d44b3a002b11423c02587fd3040e07
文件内容的SHA1哈希值是如何生成的
查看版本库中welcome.txt的内容
1
2
3
4git cat-file blob HEAD:welcome.txt
Hello.
Nice to meet you.文件总共包含25字节的内容
1
2
3git cat-file blob HEAD:welcome.txt | wc -c
25在文件内容的前面加上blob 25<null>,然后执行SHA1算法
1
2
3
4
5(printf "blob 25\000"; git cat-file blob HEAD:welcome.txt) | sha1sum
fd3c069c1de4f4bc9b15940f490aeb48852f3c42 *-
git rev-parse HEAD:welcome.txt
fd3c069c1de4f4bc9b15940f490aeb48852f3c42
HEAD代表版本库中最近一次提交
符号
^
可以用于指代父提交。例如:
HEAD^
代表版本库中的上一次提交,即最近一次提交的父提交HEAD^^
则代表HEAD^
的父提交对于一个提交有很多个父提交,可以在符号
^
后面用数字表示第几次提交。例如:
a573106^2
的含义是提交a573106
的多个父提交中的第二个父提交(日志分支图体现在横向)(不是祖先提交!)例子HEAD^1
相当于HEAD^
符号
~<n>
也可以用于指代祖先提交(日志分支图体现在纵向)。例如:
a573106~5
相当于a573106^^^^^
第7章 git重置
master分支在版本库的引用目录(.git/refs)中体现为一个引用文件.git/refs/heads/master
,其内容就是分支中最新提交的提交ID
1 | cat .git/refs/heads/master |
添加新文件,查看ID
1 | touch new-commit.txt |
引用refs/heads/master
就像一个游标,在有新的提交的时候指向了新的提交
git reset
命令可以将“游标”指向任意一个存在的提交ID
重置之后如果没有记住提交ID,通过浏览历史记录是找不到的
用reflog挽救错误的重置
如果没有重置前master分支指向的提交ID,想要重置回原来的提交似乎是一件麻烦的事(去对象库中一个一个地找)。
.git/logs
目录下日志文件记录了分支变更情况。默认非裸版本库(带有工作区)都提供分支日志功能,这是因为带有工作区的版本库都有如下设置:
1 | git config core.logallrefupdates |
查看master分支的日志文件.git/logs/refs/heads/master
中的内容
1 | tail -5 .git/logs/refs/heads/master |
第一列表示原指向,第二列表示变更后指向,文件最后是最新的记录
Git提供git reflog
命令
1 | git reflog show master | head -5 # head -5表示取前五行 |
与日志文件不同,这里输出的第一行即为最新版本
git reflog
命令在输出中提供了一个方便易记的表达式:<refname>@{<n>}
这个表达式的含义是引用<refname>之前第<n>次改变时的SHA1哈希值
重置master为两次改变之前的值
1
git reset --hard master@{2}
重置后就能成功回到被撤销的版本了
深入了解git reset
命令
git reset
命令有两种用法
1 | $ git reset [-q] [<commit>] [--] <paths>... |
上述两种用法中,其中<commit>都是可选项,可以使用引用或提交ID,如果省略<commit>则相当于使用HEAD的指向作为提交ID。
第一种用法在命令中包含路径<paths>,为了避免和提交ID同名而发生冲突,可以在<paths>前用两个连续的-
作为分隔。
第一种用法不会重置引用,更不会改变工作区,而是用指定的提交状态下的文件替换掉暂存区中的文件。
例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 # 先往new.txt添加新内容
echo "new line." >> new.txt
git add new.txt
git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: new.txt
git reset -- new.txt
Unstaged changes after reset:
M new.txt
git status
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: new.txt
# 可以看到刚刚的提交被撤销了
第二种用法则会重置引用,而且根据参数的不同可以对工作区或暂存区进行重置
命令格式:git reset [--soft | --mixed | --hard] [<commit>]
--hard
参数:会执行上图中的全部动作①、②、③,即:- 替换引用的指向。引用指向新的提交ID。
- 替换暂存区。替换后,暂存区的内容和引用指向的目录树一致。
- 替换工作区。替换后,工作区的内容变得和暂存区一致,也和HEAD所指向的目录树一致。
--soft
参数:会执行动作①。即只更改引用的指向,不改变暂存区和工作区。--mixed
参数或不加参数(默认--mixed
):会执行动作①、②。即更改引用的指向并重置暂存区,但是不改变工作区。
例子:
git reset
(同git reset HEAD
)用HEAD指向的目录树重置暂存区,工作区不会受到影响。
相当于将之前用
git add
命令添加到暂存区的内容撤出git reset --soft HEAD^
工作区和暂存区不做改变,但是引用回退一次。当对最新提交的提交说明或提交更改不满意的时候,撤销最新的提交以便重新提交。
git commit --amend
用于对最新的提交进行重新提交以修补错误的提交说明或提交文件。相当于执行了下面两条命令
1
2git reset --soft HEAD^
git commit -e -F .git/COMMIT_EDITMSG # .git/COMMIT_EDITMSG保存了上次的提交日志git reset HEAD^
同(git reset --mixed HEAD^
)工作区不变,将暂存区和引用回退一次
git reset --hard HEAD^
彻底撤销最近的提交。工作区、暂存区、引用全部回退。
第8章 git检出
重置命令的一个用途就是修改引用的游标指向。
重置命令没有使用任何参数对所要重置的分支名进行设置,这是因为重置命令针对的是头指针HEAD
。
重置命令没有改变头指针HEAD
的内容是因为HEAD指向了一个引用rerf/heads/master
,所以重置命令体现为分支“游标”的变更,HEAD
本身一直指向的是refs/heads/master
,并没有在重置时改变。
HEAD的重置即检出
HEAD
可以理解为“头指针”,是当前工作区的“基础版本”,当执行提交时,将指向最新的提交
查看当前HEAD
的指向
1 | $ cat .git/HEAD |
用git checkout
命令检出该ID的父提交
1 | $ git checkout 8634325^ |
出现大段的输出,意思就是现在处于“分离头指针”的状态,在这个状态下执行另一个checkout
命令检出则会丢弃在此状态下的修改和提交,如果想要保留此状态下的修改和提交,使用-b
参数调用checkout
命令来创建新的跟踪分支,如:
git checkout -b new_branch_name
查看HEAD
的内容可以发现头指针指向了一个具体的提交
1 | $ cat .git/HEAD |
使用reflog
命令也可以看到头指针被更改了
1 | $ git reflog -1 |
reflog
的内容是HEAD
头指针的变迁记录,而不是master
分支
1 | $ git rev-parse HEAD master |
可以看到HEAD
和master
指向不同的提交
checkout
和reset
命令不同,分支(master
)的指向没有改变,还是指向的原有提交的ID
试着做一次更改,并加入到暂存区
1 | $ touch detached-commit.txt |
执行提交并查看头指针指向
1 | $ git commit -m "commit in detached HEAD mode" |
头指针指向最新的提交
查看日志也可以发现新的提交建立在之前的提交基础之上
1 | $ git log --graph --oneline |
记下新的提交ID(ccedb9c)
以master
分支名作为参数执行checkout
命令将分支切换到master
上
1 | $ git checkout master |
通过日志已经看不到之前的提交了,但是仍然存在于对象库中(但是因为这个提交没有被任何分支跟踪到,所以不保证这个提交会永久存在)
1 | $ git log --graph --pretty=oneline |
挽救分离头指针
刚才分离头指针状态下的提交只能通过提交ID访问到,如果使用reset
虽然可以将master
分支重置到该提交,但是就会丢到当前master
的最新提交(reset
命令体现出来的就是分支游标的改变)
这个时候需要使用merge
(合并)操作
将ccedb9c
这个提交合并到当前分支上
1 | $ git merge ccedb9c |
查看日志可以看到不一样的分支图
1 | $ git log --graph --oneline |
查看最新提交的信息,可以看到该提交有两个父提交
1 | $ git cat-file -p HEAD |
深入了解git checkout
命令
三种用法
1 | $ git checkout [-q] [<commit>] [--] <paths>... |
第一种用法的--
主要是用来避免引用和路径同名发生冲突
第一种用法的<commit>是可选项,如果省略则相当于从暂存区进行检出,这和重置命令不同(重置默认值是
HEAD
)重置一般用于重置暂存区(除非使用
--hard
参数,否则不重置工作区),而检出命令主要是覆盖工作区(如果<commit>不省略,也会替换暂存区中的相应文件)第一种用法(包含<paths>的用法)不会改变
HEAD
头指针,主要用于指定版本的文件覆盖工作区中对应的文件。如果省略<commit>,则会用暂存区中的文件覆盖工作区的文件,否则用指定提交中的文件覆盖暂存区和工作区中对应的文件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68# 查看当前版本下的文件内容
$ cat welcome.txt
Hello.
Nice to meet you.
# 追加一行,并添加到暂存区
$ echo 'new line.' >> welcome.txt
$ git add welcome.txt
# 再次追加一行内容
$ echo 'new new line.' >> welcome.txt
# 此时的文件内容
$ cat welcome.txt
Hello.
Nice to meet you.
new line.
new new line.
# 通过使用暂存区文件内容覆盖工作区文件内容实现撤销修改的目的
$ git checkout -- welcome.txt
# 此时的文件内容
$ cat welcome.txt
Hello.
Nice to meet you.
new line.
# 将修改提交
$ git commit -m "append new line to welcome.txt"
# 追加一行并添加到暂存区
$ echo 'new new line.' >> welcome.txt
$ git add welcome.txt
# 使用指定版本的文件内容覆盖当前工作区与暂存区中的文件内容实现单个文件版本回退的效果
$ git checkout 095d003 -- welcome.txt
$ git status
On branch master
nothing to commit, working tree clean
$ cat welcome.txt
Hello.
Nice to meet you.
new line.
# 可以看到暂存区和工作区的内容都被清空
# 若是已经提交的更改的情况
$ echo 'new new line.' >> welcome.txt
$ git add welcome.txt
$ git commit -m "append new new line to welcome.txt"
[master 7402a4e] append new new line to welcome.txt
1 file changed, 1 insertion(+)
$ git checkout 095d003 -- welcome.txt
# 工作区回退
$ cat welcome.txt
Hello.
Nice to meet you.
new line.
# 暂存区生成新的暂存文件信息,随时可以提交修改
$ git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: welcome.txt第二种用法则会改变
HEAD
头指针。<branch>指代切换到哪个分支,如果不指定分支名则会进入“分离头指针”的状态第三种用法主要用来创建和切换到新分支(<new_branch>),新的分支从<start_point>指定的提交开始创建
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21# 通过日志查到祖先提交的ID
$ git log --graph --oneline
* b06ad0b (HEAD -> master) Merge commit 'ccedb9c'
|\
| * ccedb9c commit in detached HEAD mode
* | 8634325 add new.txt
|/
* ca04d27 Append a nice line.
* 6e314b7 empty commit
* dbda1b1 initialized.
# 创建一个从祖先提交开始的分支
$ git checkout -b parent-parent ca04d27
Switched to a new branch 'parent-parent'
# 再次查看日志可以发现分支指向祖先提交
$ git log --graph --oneline
* ca04d27 (HEAD -> parent-parent) Append a nice line.
* 6e314b7 empty commit
* dbda1b1 initialized.一条特别危险的命令:
git checkout -- .
或git checkout .
.
代表通配,也就是指所有文件它会取消所有本地修改,使用暂存区覆盖工作区
第9章 恢复进度
git stash
命令的使用
git stash
用于保存进度与恢复进度
查看保存的进度
1 | $ git stash list |
1 | # 保存当前工作进度,会分别对暂存区和工作区的状态进行保存 |