Git 使用的笔记整理第二篇。

git rm

用于从工作区和索引中删除文件:

# 删除工作区文件,并且将这次删除放入暂存区
$ git rm [file1] [file2] ...

# 停止追踪指定文件,但该文件会保留在工作区
$ git rm --cached [file]

除删除外,还可以将文件重命名后放入暂存区:

# 重命名文件,并且修改放入暂存区
$ git mv [file-original] [file-renamed]

git add

提交所有被追踪的文件到暂存区,即提交所有被删除和修改的文件,不会提交新建的文件:

git add -u/--update

提交所有修改的和新建的文件到暂存区:

git add .

提交所有被删除、被替换、被修改和新增的文件到暂存区:

git add -A/--all

git ls-files

显示工作区和索引中的文件信息:

# 显示缓存了的文件(默认)
git ls-files -c/--cached

# 查看暂存区中的文件(与 git ls-tree HEAD 类似)
git ls-files -s/--stage

# 显示删除了的文件
git ls-files -d/--deleted

# 显示修改了的文件
git ls-files -m/--modified

# 查看合并冲突的文件
git ls-files -u/--unmerged

# 显示其他类型的文件(比如未追踪的)
git ls-files -o/--others

# 显示忽略了的文件(满足忽略模式的)
git ls-files -i/--ignored

git object

Git 是一个内容寻址文件系统,且是在其基础上构建的版本控制系统。Git 用 对象(object) 来实现文件内容、提交(commit)信息等的存储和管理,从而实现版本控制。Git 包含三种类型的对象,即数据对象(blob)、树对象(tree)、提交对象(commit)。

数据对象 只存储文件的内容(不包括文件名、权限等信息)。Git 会根据文件内容计算出一个 hash 值,以 hash 值作为文件索引存储在 Git 文件系统中。git hash-object 命令可用于计算文件内容的 hash 值,并生成的数据对象,数据对象存储在 .git/objects 目录中。

$ mkdir test && cd test && git init .
Initialized empty Git repository in /Users/huayong/temp/test/.git/
$ find .git/objects
.git/objects
.git/objects/pack
.git/objects/info
$ echo 'test content' | git hash-object -w --stdin
d670460b4b4aece5915caf5c68d12f560a9fe3e4
$ find .git/objects -type f
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4

上例中,用 hash-object 命令创建了一个数据对象,并发现其被以文件形式存储到 .git/objects 目录中。其中 hash-object 命令的 -w 表示存储生成的数据对象,否则只返回生成的 hash 值而不存储;--stdin 表示从标准输入中获取文件内容,当然也可以指定一个文件路径代替此选项。该命令输出一个长度为 40 个字符的校验和,校验和的前两个字符用于命名子目录,余下的 38 个字符则用作文件名,存储在 .git/objects 目录下。

通过 git cat-file 命令可以从数据对象中取回内容,常用的参数有 -p 查看对象的内容,-t 查看对象类型,-s 查看对象大小。

$ git cat-file -t d670460b4b4aece5915caf5c68d12f560a9fe3e4
blob
$ git cat-file -s d670460b4b4aece5915caf5c68d12f560a9fe3e4
13
$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content

数据对象只解决了文件内容的存储问题,而文件名等其它信息则需要其他对象来存储。树对象便能解决文件名保存的问题。

树对象 是用于记录文件的目录树结构。Git 以一种类似于 UNIX 文件系统的方式存储内容,其将所有内容以树对象和数据对象的形式存储,其中树对象对应了 UNIX 中的目录项,数据对象则大致上对应了 inodes 或文件内容。 一个树对象包含了一条或多条树对象记录,每条记录含有一个指向数据对象或者子树对象的 SHA-1 指针,以及相应的模式、类型、文件名信息。

Git 根据某一时刻暂存区(即 index 区域)所表示的状态创建并记录一个对应的树对象,如此重复便可依次记录一系列的树对象。底层命令 update-index 可为一个单独文件创建暂存区。

$ echo 'version 1' > test.txt
$ git hash-object -w test.txt
83baae61804e65cc73a7201a7252750c76066a30
$ echo 'version 2' > test.txt
$ git hash-object -w test.txt
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
$ find .git/objects -type f
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a
.git/objects/83/baae61804e65cc73a7201a7252750c76066a3
$ git update-index --add --cacheinfo 100644 \
  83baae61804e65cc73a7201a7252750c76066a30 test.txt

其中的 --add 参数表示新增文件名,如果第一次添加某一文件名(即文件还没被添加到暂存或者索引中过),必须使用此选项;--cacheinfo <mode> <object> <path> 是要添加的数据对象的模式、hash值和路径,<path> 意味着不仅可以为数据对象指定单纯的文件名,也可以使用路径。通过 write-tree 命令将暂存区内容写入一个树对象,否则只会存在于暂存区。当调用 write-tree 命令时,它会根据当前暂存区状态自动创建一个新的树对象。

$ git write-tree
d8329fc1cc938780ffdd9f94e0d364e0ea74f579
$ git cat-file -t d8329fc1cc938780ffdd9f94e0d364e0ea74f579
tree
$ git cat-file -p d8329fc1cc938780ffdd9f94e0d364e0ea74f579
100644 blob 83baae61804e65cc73a7201a7252750c76066a30    test.txt

继续创建新的树对象,其次包括 test.txt 文件的第二个版本,以及一个新的文件:

$ echo 'new file' > new.txt
$ git update-index --cacheinfo 100644 \
  1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt
$ git update-index --add new.txt
$ git write-tree
0155eb4229851634a0f03eb265b69f5a2d56f341
$ git cat-file -p 0155eb4229851634a0f03eb265b69f5a2d56f341
100644 blob fa49b077972391ad58037050f2a75f74e3671e92    new.txt
100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a    test.txt

通过 read-tree 命令可以把树对象读入暂存区,如果指定 --prefix 选项,则可将一个已有的树对象作为子树读入暂存区。

$ git read-tree --prefix=bak d8329fc1cc938780ffdd9f94e0d364e0ea74f579
$ git write-tree
3c4e9cd789d88d8d89c1073707c3585e41b0e614
$ git cat-file -p 3c4e9cd789d88d8d89c1073707c3585e41b0e614
040000 tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579    bak
100644 blob fa49b077972391ad58037050f2a75f74e3671e92    new.txt
100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a    test.txt

如果基于这个新的树对象创建一个工作目录,会发现工作目录的根目录包含两个文件以及一个名为 bak 的子目录,该子目录包含 test.txt 文件的第一个版本。

提交对象 用于保存提交的作者、时间、说明等信息,可以使用 commit-tree 命令创建一个提交对象,为此需要指定一个树对象的 SHA-1 值,以及该提交的父提交对象(如果不是第一次提交的话)。

$ echo 'first commit' | git commit-tree d8329f
fd66914a426e6eba479fcb3d3684e682e9fc7417
$ git cat-file -p fd6691
tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579
author Huayong Kuang <sudohuoty@163.com> 1553439569 +0800
committer Huayong Kuang <sudohuoty@163.com> 1553439569 +0800

继续创建另两个提交对象,它们分别引用各自的上一个提交(作为其父提交对象):

$ echo 'second commit' | git commit-tree 0155eb -p fd6691
44a9e500d7977bdd6e3093d8215eb0e7f9a8b2ae
$ echo 'third commit'  | git commit-tree 3c4e9c -p 44a9e5
e388a196350af938429db2db9f7ad0021096b56d
$ git log --stat e388a1
commit e388a196350af938429db2db9f7ad0021096b56d
Author: Huayong Kuang <sudohuoty@163.com>
Date:   Sun Mar 24 23:08:45 2019 +0800

    third commit

 bak/test.txt | 1 +
 1 file changed, 1 insertion(+)

commit 44a9e500d7977bdd6e3093d8215eb0e7f9a8b2ae
Author: Huayong Kuang <sudohuoty@163.com>
Date:   Sun Mar 24 23:08:17 2019 +0800

    second commit

 new.txt  | 1 +
 test.txt | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

commit fd66914a426e6eba479fcb3d3684e682e9fc7417
Author: Huayong Kuang <sudohuoty@163.com>
Date:   Sun Mar 24 22:59:29 2019 +0800

    first commit

 test.txt | 1 +
 1 file changed, 1 insertion(+)

以上谈论的内容便是每次运行 git add 和 git commit 命令时, Git 所做的实质工作——将被改写的文件保存为数据对象,更新暂存区,记录树对象,最后创建一个指明了顶层树对象和父提交的提交对象。 这三种主要的 Git 对象——数据对象、树对象、提交对象——最初均以单独文件的形式保存在 .git/objects 目录下。下列便是目前示例目录内的所有对象:

$ find .git/objects -type f
.git/objects/3c/4e9cd789d88d8d89c1073707c3585e41b0e614
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4
.git/objects/d8/329fc1cc938780ffdd9f94e0d364e0ea74f579
.git/objects/fd/66914a426e6eba479fcb3d3684e682e9fc7417
.git/objects/e3/88a196350af938429db2db9f7ad0021096b56d
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a
.git/objects/44/a9e500d7977bdd6e3093d8215eb0e7f9a8b2ae
.git/objects/01/55eb4229851634a0f03eb265b69f5a2d56f341
.git/objects/fa/49b077972391ad58037050f2a75f74e3671e92
.git/objects/83/baae61804e65cc73a7201a7252750c76066a3

以上内容参考自:Git-内部原理-Git-对象

git refs

Git 的引用(references,或缩写为 refs),是一个指针,指向某一个对象。引用一般都是一个文件,记录着所指向对象的 SHA-1 值,这些文件保存在 .git/refs 目录中。

$ find .git/refs
.git/refs
.git/refs/heads
.git/refs/tags

引用文件可以被手动编辑,如:

echo "e388a196350af938429db2db9f7ad0021096b56d" > .git/refs/heads/master

这就让 master 分支引用指向了某一个对象。但一般不建议手动编辑,可以用更加安全的命令 update-ref 来完成:

git update-ref refs/heads/master e388a196350af938429db2db9f7ad0021096b56d

所以,分支的本质,就是一个指向某一系列提交之首的指针或引用。实际上,分支(branch), 远程跟踪分支(remote-tracking branch)以及标签(tag)都是对提交对象的引用。

HEAD 引用,是一个符号引用(symbolic reference),指向目前所在的分支。HEAD 引用也是一个文件,即 .git/HEAD。所谓符号引用,意味着它并不像普通引用那样包含一个 SHA-1 值——它是一个指向其他引用的指针。

$ cat .git/HEAD
ref: refs/heads/master
$ git update-ref refs/heads/test 44a9e5
$ git co test
Switched to branch 'test'
$ cat .git/HEAD
ref: refs/heads/test

HEAD 所引用的值会在每次切换分支或者每次向分支提交新内容时改变,当然也可以手动编辑它的内容,但建议用更安全的命令 symbolic-ref 来完成:

$ git symbolic-ref HEAD
refs/heads/master
$ git symbolic-ref HEAD refs/heads/test
$ git symbolic-ref HEAD
refs/heads/test

标签引用 则类似一个永不移动的分支引用--永远指向某一个提交对象,就相当于给某一提交对象起一个别名。前文提到了对象的三种类型,实际上还有一种对象类型,即 标签对象,其包含一个标签创建者信息、一个日期、一段注释信息,以及一个指针。

标签有两种类型,即附注标签和轻量标签。附注标签是一个简单的引用,如:

$ git update-ref refs/tags/v1.0 44a9e50
$ cat .git/refs/tags/v1.0
44a9e500d7977bdd6e3093d8215eb0e7f9a8b2ae

若创建一个附注标签,则会创建一个标签对象,并记录一个引用来指向该标签对象,而不是直接指向提交对象。如(-a 选项指定创建附注标签,-m 选项指定注释信息):

$ git tag -a v1.1 e388a19 -m 'test tag'
$ cat .git/refs/tags/v1.1
f1a6f6dbbed2e56bb3bf4b7f1225aac6ffb172fb
$ git cat-file -p f1a6f6dbbed2e56bb3bf4b7f1225aac6ffb172fb
object e388a196350af938429db2db9f7ad0021096b56d
type commit
tag v1.1
tagger Huayong Kuang <sudohuoty@163.com> 1553613794 +0800

test tag

远程引用 记录 push 操作时每一个分支所对应的值,保存在 .git/refs/remotes 目录下。

$ cat .git/refs/remotes/origin/master
e388a196350af938429db2db9f7ad0021096b56d

以上内容参考自:Git-内部原理-Git-引用

git ls-tree

ls-tree 命令用于列出给定树对象的内容,类似于 unix 的 ls -al 命令。命令使用格式如下:

git ls-tree [<options>] <tree-ish> [<path>...]

其中, path 表示文件和目录的匹配模式,例如显示查看 script 目录下所有的 .sh 文件:

$ $ git ls-tree HEAD scripts/*.sh
100755 blob d5ea88a83859831b8f4337cc2c6a6d1b5c789dd2    scripts/deploy.sh
100755 blob b019942d95619df13889967b580c6ff7e35d3a10    scripts/manager.sh
100755 blob f40d5c8b6f0e48ff9546280b4d60d6fe89908e07    scripts/run.sh

git update-index

Git 的数据索引 index,也叫做 stage 空间,或者 cache 空间。平时所说的用 git add 将修改添加到暂存区,其实应该理解为更新索引,即 update-index。实际上 index 空间是新的 commit 的一个写照(snapshot)。

git update-index 是一个底层的命令,用于更新索引内容。以下是一些比较常用的用法:

# 添加文件到索引中
git update-index --add

# 改变索引中文件的可执行权限
git update-index --chmod=(+|-)x

# 暂时忽略文件的改动
git update-index --assume-unchanged
# 取消对文件改动的忽略
git update-index --no-assume-unchanged

git gc

git gc 命令即垃圾收集 (garbage collect),执行此命令是,会收集所有松散对象并将它们存入 packfile,合并这些 packfile 进一个大的 packfile,然后将不被任何 commit 引用并且已存在一段时间 (数月) 的对象删除。该命令底层会调用 git prune 命令。

加上 --prune=<date> 可以指定清理多久以前的松散对象,如 --prune==now。

参考资料