【摘】Git-从零单排 02期
前言
书接上文,我们对Git有了一个基本的认知,并且基础工作也做好了。接下来,笔者就用一个实例,对照执行命令前后文件的变化,探索以下命令的运行过程。如果,看官对命令底层原理不感兴趣,只是想知道命令怎么用,就直接看翻到文章最后。
git init
git add
git status
git commit
git log
行文时,系统环境为 macOS Mojave v10.14.5,Git版本为 2.20.1 ,开发工具为 vscode 1.45.1 。
初始化 git init
首先,我们需要有一个工作目录(Working Directory)。工作目录可以是刚新建无文件的文件夹,也可以是一个已经有文件的文件夹。
mkdir git-kael-project
cd git-kael-project
然后,我需要使用 git init
对工作目录进行初始化。 git init
命令之后可以带上文件夹名称,如果文件夹不存在,则在当前路径下新建文件夹,并将其初始化。如果存在就直接对文件夹初始化。
git init
git init folderName
接下来,让我们看一下执行 git init
前后,工作目录的区别。
# 未初始化
ls -al
total 0
drwxr-xr-x 2 apple staff 64 5 29 22:31 .
drwxr-xr-x+ 45 apple staff 1440 5 29 22:31 ..
# 初始化
git init
ls -al
total 0
drwxr-xr-x 3 apple staff 96 5 29 22:32 .
drwxr-xr-x+ 45 apple staff 1440 5 29 22:31 ..
drwxr-xr-x 9 apple staff 288 5 29 22:32 .git # 多了一个 .git 文件夹,这就是我们说的仓库!
对比,我们可以知道, git init
命令就是在工作目录的根目录下,生成一个 .git 文件夹,这个文件就是我们这个项目的 Git 仓库。它包含了几乎所有 Git 存储和操作的东西。 要想备份或复制一个版本库,只需把这个目录拷贝至另一处即可。
我们查看一下, .git 文件夹里到底有什么
ls -al .git/
total 24
drwxr-xr-x 9 apple staff 288 5 29 21:41 .
drwxr-xr-x 3 apple staff 96 5 29 21:41 ..
-rw-r--r-- 1 apple staff 23 5 29 21:41 HEAD # HEAD指针,后文再讲
-rw-r--r-- 1 apple staff 137 5 29 21:41 config # 项目配置,使用 git config --loacl 时,配置的内容就存放在这里
-rw-r--r-- 1 apple staff 73 5 29 21:41 description # 项目描述文件,给GitWeb使用,我们不用关心
drwxr-xr-x 13 apple staff 416 5 29 21:41 hooks # 钩子,存放的是一些 shell 脚本
drwxr-xr-x 3 apple staff 96 5 29 21:41 info # 目录下面的 exclude 文件,是用来存放不想放置在 .gitignore 文件里的忽略模式
drwxr-xr-x 4 apple staff 128 5 29 21:41 objects # 项目所有的 git 对象都被存放在这里,tree 对象、parent 对象、blob 对象
drwxr-xr-x 4 apple staff 128 5 29 21:41 refs # 存放指针的位置,branch 、remote 、tag
# -rw-r--r-- 1 apple staff xx x xx xx:xx index # 暂存区,还未生成
大家可以先对上面的目录结构稍微记忆一下。强调一下,因为还没有执行任何操作,所以现在暂存区还没被创建,也就是 index 文件!
添加到暂存区 git add
接下来,我们新建一个 hello-git.txt 文件。
touch hello-git.txt
再加入到暂存区之前,我们先查看一下
# 当前状态下,是没有index文件的,也就是无暂存区
ls -al .git/
total 24
drwxr-xr-x 11 apple staff 352 5 29 22:47 .
drwxr-xr-x 5 apple staff 160 5 29 22:47 ..
-rw-r--r--@ 1 apple staff 23 5 29 21:41 HEAD
-rw-r--r-- 1 apple staff 137 5 29 21:41 config
-rw-r--r-- 1 apple staff 73 5 29 21:41 description
drwxr-xr-x 13 apple staff 416 5 29 21:41 hooks
drwxr-xr-x 3 apple staff 96 5 29 21:41 info
drwxr-xr-x 5 apple staff 160 5 29 22:37 objects
drwxr-xr-x 4 apple staff 128 5 29 21:41 refs
# 同时.git/objects下,也是没有git对象的
ls -al .git/objects/
total 0
drwxr-xr-x 5 apple staff 160 5 29 22:37 .
drwxr-xr-x 11 apple staff 352 5 29 22:47 ..
drwxr-xr-x 2 apple staff 64 5 29 21:41 info
drwxr-xr-x 2 apple staff 64 5 29 21:41 pack
然后,我们用 git add
命令,把 hello-git.txt 加入到暂存区。之后我们再查看一下
当然,我这里查看的就是执行命令后,会有变化的位置。其它位置是没有变化的,各位看官可自行验证。
# git add 命令,后面可以带文件名(这里可以是一个或多个),也可以带符号点 . ,符号点与 --all 全等。
# git add . 与 git add --all 效果一样
git add hello-git.txt
# 查看 .git/
ls -al .git/
total 48
drwxr-xr-x 11 apple staff 352 5 29 22:47 .
drwxr-xr-x 5 apple staff 160 5 29 22:47 ..
-rw-r--r--@ 1 apple staff 6148 5 29 22:47 .DS_Store
-rw-r--r--@ 1 apple staff 23 5 29 21:41 HEAD
-rw-r--r-- 1 apple staff 137 5 29 21:41 config
-rw-r--r-- 1 apple staff 73 5 29 21:41 description
drwxr-xr-x 13 apple staff 416 5 29 21:41 hooks
-rw-r--r--@ 1 apple staff 32 5 29 22:46 index # 这个暂存区终于出来!
drwxr-xr-x 3 apple staff 96 5 29 21:41 info
drwxr-xr-x 5 apple staff 160 5 29 22:37 objects
drwxr-xr-x 4 apple staff 128 5 29 21:41 refs
# 查看 .git/objects/
ls -al .git/objects/
total 0
drwxr-xr-x 5 apple staff 160 5 29 22:37 .
drwxr-xr-x 11 apple staff 352 5 29 22:47 ..
drwxr-xr-x 3 apple staff 96 5 29 22:37 e6
drwxr-xr-x 2 apple staff 64 5 29 21:41 info
drwxr-xr-x 2 apple staff 64 5 29 21:41 pack
# 继续查看 .git/objects/e6/
ls -al .git/objects/e6/
total 8
drwxr-xr-x 3 apple staff 96 5 29 22:37 .
drwxr-xr-x 5 apple staff 160 5 29 22:37 ..
-r--r--r-- 1 apple staff 15 5 29 22:37 9de29bb2d1d6434b8b29ae775ad8c2e48c5391
到这里,我们可以清楚的看到,当我们执行了 git add hello-git.txt
之后,仓库发生的变化。
- 针对有修改的文件 hello-git.txt 的内容,生成一个 blob 对象,这个对象能完美还原 hello-git.txt 文件当下的内容。
- 针对 blob 对象,外加一个头部信息(header)一起做 SHA-1 校验运算,得的校验和(一个长度为40的字符串)。并使用校验和的前两位做文件夹名,在这个文件夹里,存入使用后38位做文件名的 blob 对象。
- 生成 .git/index 文件,.git/index 文件的文本编码是 ISO88591 ,并且把 blob 对象的校验和、文件名写入到 .git/index 里。我们可以通过
git ls-files --stage
查看暂存区的内容
对于 git add
这个命令,其实它是 git hash-object
和 git update-index
这两个底层命令组合实现的。也就是说,如果你愿意,你是可以使用这两个底层命令来 装逼 操作的。 git add
只是为了简化操作而实现的上层命令。这两个底层命令后续有机会再讲!
查看状态 git status
好了,在看官平时工作当中,可能会出现,离开工位一段时间,回来时就不记得之前在工作目录做了什么(嗯,应该只有笔者才会这样zz)。这个时候,你可以努力回想使用 git status
查看工作目录的状态。
git status
On branch master
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: hello-git.txt
# 打印的信息告诉我们,在 master 分支上,有一个还没有提交的新文件: hello-git.txt。
# 括号里还告诉我们,可以使用 git rm --cached filename 来把文件从暂存区移走。
如果你想,你也可以使用 git ls-files --stage
来 装逼 操作。
git ls-files --stage
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 hello-git.txt
生成快照 git commit
把修改加入到暂存区之后,使用 git commit
命令,生成整个项目的‘全貌快照’,也就是 commitObject 。项目版本历史就是由一个个 commitObject 组成的,Git 可以将项目还原到任意一个 commitObject 。生成一个 commitObject 又叫 完成一次提交。
# 在使用 git add 时,我们可以有选择的把文件加放到暂存区
# 但是在使用 git commit 时,一定是把暂存区里包含的 blob 对象全部一起,生成一个 commitObject。
# git commit --amend 后面会在应用场景里讲,可以先理解为修改前一个 commitObject。
# 虽然表面上来看是这样。但是其实是不对的。commitObject 在大部分情况下,是无法修改的。
git commit -m 'create hello-git.txt'
[master (root-commit) 9eb3d5f] create hello-git.txt
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 hello-git.txt
# 提示信息里,有 分支、commitObject 的校验和、commitMessage、修改的内容统计。
老规则,我们来对比看一下, 输入 git commit
命令前后, .git 里的文件变化。
# 下面信息是在 git commit 命令之前查看的。
ls -al .git/
total 32
drwxr-xr-x 10 apple staff 320 5 30 17:27 .
drwxr-xr-x 5 apple staff 160 5 30 17:27 ..
-rw-r--r-- 1 apple staff 23 5 30 17:26 HEAD
-rw-r--r-- 1 apple staff 137 5 30 17:26 config
-rw-r--r-- 1 apple staff 73 5 30 17:26 description
drwxr-xr-x 13 apple staff 416 5 30 17:26 hooks
-rw-r--r-- 1 apple staff 112 5 30 17:26 index
drwxr-xr-x 3 apple staff 96 5 30 17:26 info
drwxr-xr-x 5 apple staff 160 5 30 17:26 objects
drwxr-xr-x 4 apple staff 128 5 30 17:26 refs
# 下面信息是在 git commit 命令之后查看的。
ls -al .git/
total 56
drwxr-xr-x 13 apple staff 416 5 30 17:22 .
drwxr-xr-x 4 apple staff 128 5 30 17:22 ..
-rw-r--r-- 1 apple staff 21 5 30 17:10 COMMIT_EDITMSG
-rw-r--r--@ 1 apple staff 23 5 29 21:41 HEAD
-rw-r--r-- 1 apple staff 137 5 29 21:41 config
-rw-r--r-- 1 apple staff 73 5 29 21:41 description
drwxr-xr-x 13 apple staff 416 5 29 21:41 hooks
-rw-r--r-- 1 apple staff 145 5 30 17:10 index
drwxr-xr-x 3 apple staff 96 5 29 21:41 info
drwxr-xr-x 5 apple staff 160 5 30 17:22 logs
drwxr-xr-x 8 apple staff 256 5 30 17:10 objects
drwxr-xr-x 4 apple staff 128 5 29 21:41 refs
# 查看logs
ls -al .git/logs/
total 24
drwxr-xr-x 5 apple staff 160 5 30 17:22 .
drwxr-xr-x 13 apple staff 416 5 30 17:22 ..
-rw-r--r--@ 1 apple staff 165 5 30 17:10 HEAD
drwxr-xr-x 4 apple staff 128 5 30 17:22 refs
ls -al .git/logs/refs/heads/
total 8
drwxr-xr-x 3 apple staff 96 5 30 17:10 .
drwxr-xr-x 4 apple staff 128 5 30 17:22 ..
-rw-r--r--@ 1 apple staff 165 5 30 17:10 master
# 查看objects
ls -al .git/objects/
total 16
drwxr-xr-x 8 apple staff 256 5 30 17:10 .
drwxr-xr-x 13 apple staff 416 5 30 17:22 ..
drwxr-xr-x 3 apple staff 96 5 30 17:10 62
drwxr-xr-x 3 apple staff 96 5 30 17:10 9e
drwxr-xr-x 3 apple staff 96 5 29 22:37 e6
drwxr-xr-x 2 apple staff 64 5 29 21:41 info
drwxr-xr-x 2 apple staff 64 5 29 21:41 pack
ls -al .git/objects/9e/
total 8
drwxr-xr-x 3 apple staff 96 5 30 17:10 .
drwxr-xr-x 8 apple staff 256 5 30 17:10 ..
-r--r--r-- 1 apple staff 131 5 30 17:10 b3d5fe6014cf11a120a26b3f8b3af2f20964bd
ls -al .git/objects/62/
total 8
drwxr-xr-x 3 apple staff 96 5 30 17:10 .
drwxr-xr-x 8 apple staff 256 5 30 17:10 ..
-r--r--r-- 1 apple staff 58 5 30 17:10 01f623b63c5736c92895712c6038835be6a7f2
对比可以发现, git commit
之后,多了以下文件
- COMMIT_EDITMSG
- logs/HEAD
- logs/refs/heads/master
- refs/heads/master
- objects/9e/b3d5fe6014cf11a120a26b3f8b3af2f20964bd
- objects/62/01f623b63c5736c92895712c6038835be6a7f2
通过对比,笔者理解的 git commit
命令执行之后,Git 做了如下事情:
- 生成 .git/COMMIT_EDITMSG 文件,把
-m
参数之后的 commitMessage 写入到这个文件。 - 根据项目目录,计算得到一个 treeObject,并把这个对象写入 .git/objects/ 里,对应的就是多出了 objects/62/01f623 。
- 根据暂存区的内容、git 配置内容、执行时间、commitMessage、treeObject等,生成一个 commitObject。并把这个对象写入到 .git/objects/ 里。对应的就是多出了 objects/9e/b3d5fe 。
- 修改 .git/HEAD 文件里的分支指针所指向的 commitObject,也就是把 9e__b3d5fe6014cf11a120a26b3f8b3af2f20964bd_ 写入到 .git/_refs/heads/master 文件里。如果没有 _.git/_refs/heads/master 文件,就生成一个,所以这里会新增 _.git/_refs/heads/masterr 文件。
- 把当前的这个操作写入到对应的指针的 logs 文件里,这里相关的指针只有两个,一个是 HEAD,另一个是 master。如果指针的 logs 文件不存在,就新建对应的文件。所以就新增了,.git/logs/HEAD 和 .git/logs/refs/heads/master。这个文件里的内容就是本地 reflog 记录,并不是后文讲的
git log
命令得到的内容。
这里,笔者再详细的描述一下 commitObject。为了更好的演示,我们在当前的提交基础上,继续作一些操作。
mkdir lib
touch lib/lib.js
echo 'Git 真好用!' > lib/git.txt
git add .
git commit -m 'create lib/git.txt'
[master 75f75b1] create lib/git.txt
2 files changed, 1 insertion(+)
create mode 100644 lib/git.txt
create mode 100644 lib/lib.js
我们新建了一个 lib/ 文件夹,然后在这个文件夹里,新建了一个 lib.js 文件,和一个 git.txt 文件,并在git.txt 文件里写入了 'Git 真好用!'。接着就是 git add .
把刚的这些修改添加到暂存区,最后使用 git commit -m 'create lib/git.txt'
生成一个提交。我们现在得到了一个 75f75b1
的 commitObject。我们来查看一下这个对象的信息。
# 查看 75f75b1 的类型和详情
git cat-file -t 75f75b1
commit # 这是一个 commit 类型的对象
git cat-file -p 75f75b1
tree d1d138a1890c432ef996b8713b69453a8baa339e # 75f75b1 对象的 tree 对象,对应的项目目录
parent 9eb3d5fe6014cf11a120a26b3f8b3af2f20964bd # 75f75b1 对象的 parent 对象,对应着父提交对象
author kael <kael_yp@foxmail.com> 1590841110 +0800 # 75f75b1 对象的作者
committer kael <kael_yp@foxmail.com> 1590841110 +0800 # 75f75b1 对象的提交者
create lib/git.txt # 75f75b1 对象的提交说明 commitMessage
我们继续查看 d1d138a
这个 tree 对象。
# 查看 d1d138a 的类型和详情
git cat-file -t d1d138a
tree # 这是一个 tree 类型的对象
git cat-file -p 1d138a
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 hello-git.txt # blob 类型的对象,hello-git.txt文件
040000 tree 8bc5165a0972ca341226552000d29888be651bc9 lib # tree 类型的对象,lib 文件夹
# 查看 hello-git.txt 文件
git cat-file -p e69de2
# 为空,因为这个文件就是一个空文件。我忘记加内容了。。。哈哈
# 查看 lib 文件夹
git cat-file -p 8bc516
100644 blob cdb8667b7f629f26cae0ea24993bfd2c6411e33c git.txt # blob 类型的对象,git.txt 文件
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 lib.js # blob 类型的对象,lib.js 文件
# 查看 git.txt 文件
git cat-file -p cdb8667
Git 真好用! # 这就是我们之前写入的内容。这就是 Git 哲学里的,通过文件快照记录版本,而不是使用 diff 。
我们继续查看 9eb3d5f
这个 parent 对象。
# 查看 9eb3d5f 的类型和详情
git cat-file -t 9eb3d5f
commit # 这是一个 commit 类型的对象
# 这是一个 parent 类型的对象
git cat-file -p 9eb3d5f
tree 6201f623b63c5736c92895712c6038835be6a7f2 # 9eb3d5f 对象的 tree 对象,对应的项目目录
author kael <kael_yp@foxmail.com> 1590829805 +0800 # 9eb3d5f 对象的作者
committer kael <kael_yp@foxmail.com> 1590829805 +0800 # 9eb3d5f 对象的提交者
create hello-git.txt # 9eb3d5f 对象的提交说明 commitMessage
再看看 6201f62
这个 tree 对象。
git cat-file -p 6201f62
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 hello-git.txt
git cat-file -p e69de29
# hello-git.txt 内容为空
我们简单描述上面查看到的信息。 75f75b1
这个对象里有 tree 和 parent。tree 里有 blob 和 tree。然后就是 parent 里有 tree,没有 parent。
- tree 属性是目录结构,里面可能会有 tree 和 blob
- parent 是父提交对象,这个属性可能有,或没有。其实还有可能会有第二父提交对象。
- blob 的内容记录的文件内容
如果你不想使用git commit
这个命令,那就使用 git write-tree
和 git commit-tree
来 装逼 实现。 git write-tree
命令用来将当前的目录结构,生成一个 treeObject。 git commit-tree
命令用于将目录树对象写入版本历史。有机会,后续再讲这两个命令。
查看历史 git log
git log
这个命令是最常用命令之一,可以查看指定的 指针 的提交历史。
git log
# 第一行是 类型 校验和 指针集,指针集里可能会有 HEAD、branch、tag,使用逗号分开
commit 9eb3d5fe6014cf11a120a26b3f8b3af2f20964bd (HEAD -> master)
# 第二行是 作者信息
Author: kael <kael_yp@foxmail.com>
# 第三行是 时间
Date: Sat May 30 17:10:05 2020 +0800
# 往下就是 commitMessage
create hello-git.txt
使用 git log
命令是不会对 .git 仓库操作的,也就是你不会有 什么时候什么方式使用了 git log
命令 的记录,因为记录这个操作,没有任何意义。
git log
是一个超级厉害的命令。看一个人 Git 用得好不好,看Ta使用 git log
的姿势怎么样就能看出来。下面我就列举一些常用的 git log
使用场景和其它参数。以下选项可以组合使用
# 正常查看历史记录
git log
# 查看简洁的历史记录
git log --oneline
# 查看带每一次提交时修改的历史记录
git log -p # -p 是 --patch 的简写
# 查看一定数量的历史记录
git log -4 # 这里就是显示倒数的4条
# 查看有简略统计信息的历史记录
git log --stat
# 控制历史记录的显示格式。
git log --pretty=format:"%h - %an, %s" # %h 简短校验和;%an 作者;%s 提交说明;
git log --pretty=online # 差不多等于 git log --oneline
# 查看带结合路径的历史记录,大概就是会给 commitObject 间边上线条
git log --graph
# 查看指定时限内的历史记录
git log --since=1.weeks # 一周内的历史记录
git log --since=2020-05-01 # 查看2020年05月01号之后的提交,--since 可以写作 --after
# 查看指定时间之前的提交
git log --until=2020-05-01 # 查看2020年05月01号之前的提交,--until 可以写作 --before
# 查看包含了指定字符的message所在的历史记录
git log --grep=create # 查看包含了 ‘create’ 的提交说明的历史记录
# 这个就牛逼了,查看包含了指定字符修改的历史记录
git log -Svar # 查看修改里包含了 ‘var’ 的历史记录
# 查看指定作者的历史记录
git log --author=kael # 查看 kael 写的历史记录
# 查看指定提交者的历史记录
git log --committer=kael # 查看 kael 提交的历史记录
这个提交历史记录实际是通过 commitObject 的 parent 属性连起来的,直到找到一个没有 parent 属性的 commitObject。可以跟链表对应理解。

附
补充一下,.git/HEAD 文件的相关知识点。这个文件在 git init
时,里面的内容是 ref: refs/heads/master
,是不是很眼熟?没错,这是一个路径【 .git/refs/heads/master 】。当我们提交时,HEAD 会带着移动的分支指针。如果是第一次提交,Git 会用这个文件的内容生成对应的 分支。这也就是为什么,我们的仓库一般都会有一个 master 分支。如果你在 git init
之后(当然,你其实可以在任意时候对这个文件进行修改),把这个文件的内容改成 refs/heads/test
,那在提交时,默认就会生成 test 分支。笔者强烈建议不要改!会被同事打的! 后续会讲的 git checkout
签出命令,改的就是这个文件的内容。
补充一下,.git/logs/里文件的相关知识点。这个文件夹下的文件,它的内容是长这样的:
cat .git/logs/HEAD
# 首先是40个0,本来是这个记录的 parent 的校验和,但是因为这条记录是第一个,没有父对象,所以就用0占位
# 然后是正常的40位的校验和,这是这个记录自身的校验和
# 然后再是提交者,邮箱,时间
# 最后就是操作类型和 message
0000000000000000000000000000000000000000 9eb3d5fe6014cf11a120a26b3f8b3af2f20964bd kael <kael_yp@foxmail.com> 1590829805 +0800 commit (initial): create hello-git.txt
当我们使用 git reflog
命令时,就是把对应的指针的 logs 文件的内容稍微处理一下显示出来。
后语
最后,笔者就 大概的说一下 Git 基本使用流程 祝大家六一快乐。
- 在项目目录下使用
git ini
初始化。 - 在项目内编辑文件,然后使用
git add
把修改的内容添加到暂存区。 - 然后使用
git commit
把暂存区的内容生成快照提交。 - 可以使用
git status
查看当前的状态。 - 可以使用
git log
查看历史记录。
世界上只有两种人,一种懂二进制,一种不懂二进制。
转载自:https://juejin.cn/post/6844904175352954887