Git权威指南

第4章 git初始化

创建版本库及第一次提交

  1. 告诉Git当前用户的姓名和邮件地址,配置的用户名和邮件地址将在版本库提交时用到

    1
    2
    git config --global user.name "[用户名]"
    git config --global user.email "[邮件地址]"
  2. 初始化仓库

    1
    2
    3
    4
    mkdir demo
    cd demo
    git init
    Initialized empty Git repository in D:/workspace/git/demo/.git/

    如果Git版本是1.6.5以上,可以在git init命令后面跟上目录名称,会自动完成目录创建

    1
    2
    git 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
2
3
4
5
git commit -m "initialized."
[master (root-commit) dbda1b1] initialized.
1 file changed, 1 insertion(+)
create mode 100644 welcome.txt

  • 通过-m参数设置提交说明
  • 命令输出显示此次提交在名为master的分支上,且是该分支的第一次提交(root-commit),提交ID是dbda1b1。
  • 此次提交修改了一个文件,包含一行插入(新增)
  • 此次提交创建了新文件welcome.txt

如果之前没有设置用户名和邮箱,则会出现如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Author identity unknown

*** Please tell me who you are.

Run

git config --global user.email "you@example.com"
git config --global user.name "Your Name"

to set your account's default identity.
Omit --global to set the identity only in this repository.

fatal: unable to auto-detect email address (got 'User@8-sri0091.(none)')

思考:为什么工作区根目录有一个.git目录

初始化仓库之后,会在根目录出现名为.git的隐藏目录

Windows用户需要通过如下方式设置后才可以看到

20220516153036140

Linux用户通过以下命令可查看

1
2
3
ls -a
./ ../ .git/

Git以及其他分布式版本控制系统的一个共同的显著特点是,版本库位于工作区的根目录下。对Git来说就是.git目录,且仅此一处,没有任何其他跟踪文件或目录了。

传统集中式版本控制系统的版本库和工作区是分开的,甚至是在不同的主机上,因此必须建立工作区和版本库的对应。

Git将版本库(.git目录)放在工作区根目录下,那么Git的相关操作一定要在工作区根目录下执行吗?换句话说,当工作区中包含子目录,并在子目录中执行Git命令,如何定位到版本库的?

实际上,当在Git工作区的某个子目录下执行操作的时候,会在工作区目录中依次向上递归查找.git目录,找到的.git目录就是工作区对应的版本库了。

例如在非Git工作区执行git命令时会因为找不到.git目录报错

1
2
3
4
cd /d/workspace/git
git status
fatal: not a git repository (or any of the parent directories): .git

使用strace命令去跟踪执行git status命令时的磁盘访问,可以看到沿目录依次向上递归的过程。

1
strace -e 'trace=file' git status

显示版本库.git目录所在位置

1
2
3
git rev-parse --git-dir
D:/workspace/git/demo/.git

显示工作区根目录

1
2
3
git rev-parse --show-toplevel
D:/workspace/git/demo

相对于工作区根目录的相对目录

1
2
3
git rev-parse --show-prefix
a/b/c/

显示从当前目录后退到工作区根目录的深度

1
2
3
git rev-parse --show-cdup
../../../

思考:git config命令的各参数区别

Git的配置文件分别是版本库级别、全局(用户主目录下,用户级)和系统级别。

--global:全局级别

--system:系统级别

第5章 git暂存区

修改不能直接提交吗

首先进行文件追加

1
echo "Nice to meet you." >> welcome.txt
1
2
3
4
5
6
7
8
9
git diff
diff --git a/welcome.txt b/welcome.txt
index 18832d3..fd3c069 100644
--- a/welcome.txt
+++ b/welcome.txt
@@ -1 +1,2 @@
Hello.
+Nice to meet you.

需要执行git add命令将文件修改添加到暂存区之后才可以进行提交

如果直接执行提交操作

1
2
3
4
5
6
7
8
9
git commit -m "Append a nice line."
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: welcome.txt

no changes added to commit (use "git add" and/or "git commit -a")

理解Git暂存区(stage)

在版本库.git目录下有一个index文件,下面针对这个文件做一个实验

  1. 撤销工作区中对welcome.txt文件尚未提交的更改

    1
    2
    git checkout -- welcome.txt # 撤销工作区中对welcome.txt文件尚未提交的更改
    git status -s # 如果git版本小于1.7.3,执行git diff
  2. 查看.git/index文件

    1
    2
    3
    ls --full-time .git/index
    -rw-r--r-- 1 User 197121 145 2022-05-16 19:17:16.292820100 +0800 .git/index

  3. 再次执行

    1
    2
    3
    4
    git status -s # 再次执行
    ls --full-time .git/index
    -rw-r--r-- 1 User 197121 145 2022-05-16 19:17:16.292820100 +0800 .git/index

    可以看到时间戳不变

  4. 现在修改下welcome.txt的时间戳,但是不改变它的内容

    1
    2
    3
    4
    5
    6
    7
    8
    9
    ls --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目录中

20220516193323223

  • 左侧为工作区,右侧为版本库。在版本库中标记为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
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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
touch new.txt # 创建新文件
git status
On branch master
Untracked files:
(use "git add <file>..." to include in what will be committed)
new.txt

nothing added to commit but untracked files present (use "git add" to track)

git add new.txt # 添加到暂存区
git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: new.txt

echo "new line." >> new.txt # 更新文件
git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: new.txt

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 checkout -- new.txt
git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: new.txt
# 此时工作区中未提交的修改已经被清除了
cat new.txt
# 查看new.txt的内容,没有发现新增的数据
# 重新将文件更改,这里就省略了
git reset HEAD
git status
On branch master
Untracked files:
(use "git add <file>..." to include in what will be committed)
new.txt

nothing added to commit but untracked files present (use "git add" to track)
# 可以看到暂存区未提交的更改被清除,new.txt变成了未追踪状态了
# 工作区中的内容不受影响
cat new.txt
new line.
# 重新恢复下状态,这里省略了
git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: new.txt

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 rm --cached new.txt
error: the following file has staged content different from both the
file and the HEAD:
new.txt
(use -f to force removal)
# 此时三个区域的状态均不同,所以不允许此操作,可以使用-f参数强制执行
git rm --cached -f new.txt
git status
On branch master
Untracked files:
(use "git add <file>..." to include in what will be committed)
new.txt

nothing added to commit but untracked files present (use "git add" to track)
# 此时暂存区中文件被删除了,所以new.txt变成了未追踪状态,工作区中文件不变
# 恢复下状态,这里省略了
git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: new.txt

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 commit -m "add new.txt" # 将新增的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
# new.txt新增数据的更新还在工作区中
git add new.txt
echo "new new line." >> new.txt
git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: new.txt

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 checkout master new.txt # 此操作将清空工作区和暂存区的更改
git status
On branch master
nothing to commit, working tree clean
cat new.txt
# 最终new.txt中没有内容,被替换成了版本库中的版本了

git diff

工作区、暂存区和版本库的目录树浏览

直观地查看暂存区及HEAD中的目录树?

对于HEAD(版本库中当前提交)指向的目录树,可以使用Git底层命令ls-tree查看

1
2
3
4
git ls-tree -l HEAD
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 new.txt
100644 blob fd3c069c1de4f4bc9b15940f490aeb48852f3c42 25 welcome.txt

  • -l参数可以显示文件的大小

git ls-files命令可以显示暂存区目录树

1
2
3
4
git ls-files -s
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 new.txt
100644 9ac7e0760066ad8d06a0d952c4e1260e628551b4 0 welcome.txt

第三列不是文件大小,而是文件编号

git diff

  1. 工作区与暂存区比较

    1
    2
    3
    4
    5
    6
    7
    8
    git diff
    diff --git a/new.txt b/new.txt
    index e69de29..2595e07 100644
    --- a/new.txt
    +++ b/new.txt
    @@ -0,0 +1 @@
    +new line.

  2. 暂存区和HEAD比较

    1
    2
    3
    4
    5
    6
    7
    8
    9
    git diff --cached
    index fd3c069..9ac7e07 100644
    --- a/welcome.txt
    +++ b/welcome.txt
    @@ -1,2 +1,3 @@
    Hello.
    Nice to meet you.
    +Bye bye.

  3. 工作区和HEAD比较

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    git 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
2
3
4
5
6
7
8
9
git log -1 --pretty=raw
commit 86343253f0d44b3a002b11423c02587fd3040e07 # 这些数字实际上是SHA1哈希值,这是本次提交的唯一标识
tree f341012ebeaf74262be6892be96ecce373a68ef3 # 这是本次提交所对应的目录树
parent ca04d27bdf1a442d67c379fd5803f5b85c868bd5 # 这是本次提交的父提交(上一次提交)
author Crayon <614820984@qq.com> 1652703487 +0800
committer Crayon <614820984@qq.com> 1652703487 +0800

add new.txt

研究Git对象ID可以使用git cat-file命令

1
2
git cat-file -t <ID> # 查看ID类型
git cat-file -p <ID> # 查看文件内容

SHA1哈希值是什么

哈希是一种摘要算法(散列算法),可以将任意长度的输入经过散列运算转换为固定长度的输出。

比较著名的摘要算法有MD5和SHA1

Linux下使用sha1sum命令可以生成摘要

1
2
3
printf Git | sha1sum
5819778898df55e3a762f0c5728b457970d72cae *-

提交的SHA1哈希值是如何生成的

  • 查看HEAD对应的提交内容

    1
    2
    3
    4
    5
    6
    7
    8
    git 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
    3
    git 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
    4
    git cat-file blob HEAD:welcome.txt
    Hello.
    Nice to meet you.

  • 文件总共包含25字节的内容

    1
    2
    3
    git 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
2
3
cat .git/refs/heads/master
86343253f0d44b3a002b11423c02587fd3040e07

添加新文件,查看ID

1
2
3
4
5
touch new-commit.txt
git add new-commit.txt
cat .git/refs/heads/master
724d3845eb776ded300fedf37e8642b81c6f8b54

引用refs/heads/master就像一个游标,在有新的提交的时候指向了新的提交

git reset命令可以将“游标”指向任意一个存在的提交ID

重置之后如果没有记住提交ID,通过浏览历史记录是找不到的

用reflog挽救错误的重置

如果没有重置前master分支指向的提交ID,想要重置回原来的提交似乎是一件麻烦的事(去对象库中一个一个地找)。

.git/logs目录下日志文件记录了分支变更情况。默认非裸版本库(带有工作区)都提供分支日志功能,这是因为带有工作区的版本库都有如下设置:

1
2
3
git config core.logallrefupdates
true

查看master分支的日志文件.git/logs/refs/heads/master中的内容

1
2
3
4
5
6
7
8
tail -5 .git/logs/refs/heads/master
86343253f0d44b3a002b11423c02587fd3040e07 33d9b1023f6be2ae49454973d071be2b28e1facb Crayon <614820984@qq.com> 1652790561 +0800 commit: new commit
33d9b1023f6be2ae49454973d071be2b28e1facb 86343253f0d44b3a002b11423c02587fd3040e07 Crayon <614820984@qq.com> 1652790577 +0800 reset: moving to HEAD~1
86343253f0d44b3a002b11423c02587fd3040e07 724d3845eb776ded300fedf37e8642b81c6f8b54 Crayon <614820984@qq.com> 1652790678 +0800 reset: moving to 724d3845eb776ded300fedf37e8642b81c6f8b54
724d3845eb776ded300fedf37e8642b81c6f8b54 86343253f0d44b3a002b11423c02587fd3040e07 Crayon <614820984@qq.com> 1652790709 +0800 reset: moving to HEAD~1
86343253f0d44b3a002b11423c02587fd3040e07 724d3845eb776ded300fedf37e8642b81c6f8b54 Crayon <614820984@qq.com> 1652790722 +0800 reset: moving to 724d3845eb776ded300fedf37e8642b81c6f8b54


第一列表示原指向,第二列表示变更后指向,文件最后是最新的记录

Git提供git reflog命令

1
2
3
4
5
6
7
git reflog show master | head -5 # head -5表示取前五行
724d384 HEAD@{0}: reset: moving to 724d3845eb776ded300fedf37e8642b81c6f8b54
8634325 HEAD@{1}: reset: moving to HEAD~1
724d384 HEAD@{2}: reset: moving to 724d3845eb776ded300fedf37e8642b81c6f8b54
8634325 HEAD@{3}: reset: moving to HEAD~1
33d9b10 HEAD@{4}: commit: new commit

与日志文件不同,这里输出的第一行即为最新版本

git reflog命令在输出中提供了一个方便易记的表达式:<refname>@{<n>}

这个表达式的含义是引用<refname>之前第<n>次改变时的SHA1哈希值

  • 重置master为两次改变之前的值

    1
    git reset --hard master@{2}

    重置后就能成功回到被撤销的版本了

深入了解git reset命令

git reset命令有两种用法

1
2
$ git reset [-q] [<commit>] [--] <paths>...
$ git reset [--soft | --mixed | --hard | --merge | --keep] [-q] [<commit>]

上述两种用法中,其中<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
# 可以看到刚刚的提交被撤销了

第二种用法则会重置引用,而且根据参数的不同可以对工作区或暂存区进行重置

20220518160543182

命令格式:git reset [--soft | --mixed | --hard] [<commit>]

  • --hard参数:会执行上图中的全部动作①、②、③,即:
    1. 替换引用的指向。引用指向新的提交ID。
    2. 替换暂存区。替换后,暂存区的内容和引用指向的目录树一致。
    3. 替换工作区。替换后,工作区的内容变得和暂存区一致,也和HEAD所指向的目录树一致。
  • --soft参数:会执行动作①。即只更改引用的指向,不改变暂存区和工作区。
  • --mixed参数或不加参数(默认--mixed):会执行动作①、②。即更改引用的指向并重置暂存区,但是不改变工作区。

例子:

  • git reset(同git reset HEAD

    用HEAD指向的目录树重置暂存区,工作区不会受到影响。

    相当于将之前用git add命令添加到暂存区的内容撤出

  • git reset --soft HEAD^

    工作区和暂存区不做改变,但是引用回退一次。当对最新提交的提交说明或提交更改不满意的时候,撤销最新的提交以便重新提交。

    git commit --amend用于对最新的提交进行重新提交以修补错误的提交说明或提交文件。

    相当于执行了下面两条命令

    1
    2
    git 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
2
3
4
$ cat .git/HEAD
ref: refs/heads/master
$ git branch -v
* master 8634325 add new.txt

git checkout命令检出该ID的父提交

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ git checkout 8634325^
Note: switching to '8634325^'.

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 <new-branch-name>

Or undo this operation with:

git switch -

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

HEAD is now at ca04d27 Append a nice line.

出现大段的输出,意思就是现在处于“分离头指针”的状态,在这个状态下执行另一个checkout命令检出则会丢弃在此状态下的修改和提交,如果想要保留此状态下的修改和提交,使用-b参数调用checkout命令来创建新的跟踪分支,如:

git checkout -b new_branch_name

查看HEAD的内容可以发现头指针指向了一个具体的提交

1
2
$ cat .git/HEAD
ca04d27bdf1a442d67c379fd5803f5b85c868bd5

使用reflog命令也可以看到头指针被更改了

1
2
$ git reflog -1
ca04d27 (HEAD) HEAD@{0}: checkout: moving from master to 8634325^

reflog的内容是HEAD头指针的变迁记录,而不是master分支

1
2
3
$ git rev-parse HEAD master
ca04d27bdf1a442d67c379fd5803f5b85c868bd5
86343253f0d44b3a002b11423c02587fd3040e07

可以看到HEADmaster指向不同的提交

checkoutreset命令不同,分支(master)的指向没有改变,还是指向的原有提交的ID

试着做一次更改,并加入到暂存区

1
2
3
4
5
6
7
$ touch detached-commit.txt
$ git add detached-commit.txt
$ git status
HEAD detached at ca04d27
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: detached-commit.txt

执行提交并查看头指针指向

1
2
3
4
5
6
7
$  git commit -m "commit in detached HEAD mode"
[detached HEAD ccedb9c] commit in detached HEAD mode
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 detached-commit.txt

$ cat .git/HEAD
ccedb9c816bd52920c9ba001c04067c935363f83

头指针指向最新的提交

查看日志也可以发现新的提交建立在之前的提交基础之上

1
2
3
4
5
$ git log --graph --oneline
* ccedb9c (HEAD) commit in detached HEAD mode
* ca04d27 Append a nice line.
* 6e314b7 empty commit
* dbda1b1 initialized.

记下新的提交ID(ccedb9c)

master分支名作为参数执行checkout命令将分支切换到master

1
2
3
4
5
6
7
8
9
10
11
12
13
$ git checkout master
Warning: you are leaving 1 commit behind, not connected to
any of your branches:

ccedb9c commit in detached HEAD mode

If you want to keep it by creating a new branch, this may be a good time
to do so with:

git branch <new-branch-name> ccedb9c

Switched to branch 'master'

通过日志已经看不到之前的提交了,但是仍然存在于对象库中(但是因为这个提交没有被任何分支跟踪到,所以不保证这个提交会永久存在)

1
2
3
4
5
$ git log --graph --pretty=oneline
* 86343253f0d44b3a002b11423c02587fd3040e07 (HEAD -> master) add new.txt
* ca04d27bdf1a442d67c379fd5803f5b85c868bd5 Append a nice line.
* 6e314b7f5526662dd54dc3c7e2058010557ccb5b empty commit
* dbda1b1f8b84a477531578bd15917da5686ee8f2 initialized.

挽救分离头指针

刚才分离头指针状态下的提交只能通过提交ID访问到,如果使用reset虽然可以将master分支重置到该提交,但是就会丢到当前master的最新提交(reset命令体现出来的就是分支游标的改变)

这个时候需要使用merge(合并)操作

ccedb9c这个提交合并到当前分支上

1
2
3
4
5
6
$ git merge ccedb9c
Merge made by the 'ort' strategy.
detached-commit.txt | 0
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 detached-commit.txt

查看日志可以看到不一样的分支图

1
2
3
4
5
6
7
8
9
10
$ 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.

查看最新提交的信息,可以看到该提交有两个父提交

1
2
3
4
5
6
7
8
9
$ git cat-file -p HEAD
tree d8102c804d8cbf350b31dc6a38fe384ed5586559
parent 86343253f0d44b3a002b11423c02587fd3040e07
parent ccedb9c816bd52920c9ba001c04067c935363f83
author Crayon <614820984@qq.com> 1655277888 +0800
committer Crayon <614820984@qq.com> 1655277888 +0800

Merge commit 'ccedb9c'

深入了解git checkout命令

三种用法

1
2
3
$ git checkout [-q] [<commit>] [--] <paths>...
$ git checkout [<branch>]
$ git checkout [-m] [[-b|--orphan] <new_branch>] [<start_point>]

第一种用法的--主要是用来避免引用和路径同名发生冲突

  • 第一种用法的<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命令的使用

stash单词释义

git stash用于保存进度与恢复进度

查看保存的进度

1
2
$ git stash list
stash@{0}: WIP on master: 8634325 add new.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 保存当前工作进度,会分别对暂存区和工作区的状态进行保存
$ git stash
# git stash的完整版
# --patch会显示工作区和HEAD的差异,通过对差异文件的编辑决定最终要保存的工作区内容
# -k或--keep-index会在进度保存后将暂存区重置。默认会将暂存区和工作区强制重置
$ git stash [save [--patch] [-k|--[no-]keep-index] [-q|--quiet] [<message>]
# 显示进度列表
$ git stash list
# 恢复进度
# 如果不使用任何参数,会恢复最新保存的进度,并将恢复的工作进度从存储的工作进度列表中清除
# --index会尝试恢复暂存区
# stash是显示进度列表时提供的stash标识,恢复指定进度
$ git stash pop [--index] [<stash>]
# 和pop区别在于恢复后不删除进度
$ git stash apply [--index] [<stash>]
# 删除一个进度,默认删除最新进度
$ git stash drop [<stash>]
# 删除所有进度
$ git stash clear
# 基于进度创建分支
$ git stash branch <branchname> <stash>

探秘git stash