前言
书接上文,我们对 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 文件。
再加入到暂存区之前,我们先查看一下
# 当前状态下,是没有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
之后,多了以下文件
通过对比,笔者理解的 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,也就是把 9eb3d5fe6014cf11a120a26b3f8b3af2f20964bd 写入到 .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。
如果你不想使用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
查看历史记录。
世界上只有两种人,一种懂二进制,一种不懂二进制。
评论