Git:内部原理

1. 底层命令与上层命令

上层命令主要涵盖了checkoutbranchremote等约 30 个 Git 的子命令。 然而,由于 Git 最初是一套面向版本控制系统的工具集,而不是一个完整的、用户友好的版本控制系统, 所以它还包含了一部分用于完成底层工作的子命令。 这些命令被设计成能以 UNIX 命令行的风格连接在一起,抑或藉由脚本调用,来完成工作。 这部分命令一般被称作“底层(plumbing)”命令,而那些更友好的命令则被称作“上层(porcelain)”命令。

底层命令得以让你窥探 Git 内部的工作机制,也有助于说明 Git 是如何完成工作的,以及它为何如此运作。 多数底层命令并不面向最终用户:它们更适合作为新工具的组件和自定义脚本的组成部分。

当在一个新目录或已有目录执行git init时,Git 会创建一个.git目录。 这个目录包含了几乎所有 Git 存储和操作的东西。 如若想备份或复制一个版本库,只需把这个目录拷贝至另一处即可。 本章探讨的所有内容,均位于这个目录内。 新初始化的.git目录的典型结构如下:

$ ls -F1
config
description
HEAD
hooks/
info/
objects/
refs/

随着 Git 版本的不同,该目录下可能还会包含其他内容。 不过对于一个全新的git init版本库,这将是你看到的默认结构。description文件仅供 GitWeb 程序使用,我们无需关心。config文件包含项目特有的配置选项。info目录包含一个全局性排除(global exclude)文件, 用以放置那些不希望被记录在.gitignore文件中的忽略模式(ignored patterns)。hooks目录包含客户端或服务端的钩子脚本(hook scripts)。Git 钩子
HEAD文件、(尚待创建的)index文件,和objects目录、refs目录。 它们都是 Git 的核心组成部分。objects目录存储所有数据内容;refs目录存储指向数据(分支、远程仓库和标签等)的提交对象的指针;HEAD文件指向目前被检出的分支;index文件保存暂存区信息。

2. Git 对象

2.1 blob 对象

Git 是一个内容寻址文件系统,意味着Git 的核心部分是一个简单的键值对数据库(key-value data store)。 你可以向 Git 仓库中插入任意类型的内容,它会返回一个唯一的键,通过该键可以在任意时刻再次取回该内容。

可以通过底层命令git hash-object来演示上述效果——该命令可将任意数据保存于.git/objects目录(即对象数据库),并返回指向该数据对象的唯一的键。

git hash-object创建一个新的数据对象并将它手动存入你的新 Git 数据库中:

$ echo 'test content' | git hash-object -w --stdin

在这种最简单的形式中,git hash-object会接受你传给它的东西,而它只会返回可以存储在 Git 仓库中的唯一键。-w选项会指示该命令不要只返回键,还要将该对象写入数据库中。 最后,--stdin选项则指示该命令从标准输入读取内容;若不指定此选项,则须在命令尾部给出待存储文件的路径。
此命令输出一个长度为 40 个字符的校验和。 这是一个 SHA-1 哈希值——一个将待存储的数据外加一个头部信息(header)一起做 SHA-1 校验运算而得的校验和。后文会简要讨论该头部信息。 现在我们可以查看 Git 是如何存储数据的:

 find .git/objects -type f

这就是 Git 存储内容的方式,一个文件对应一条内容, 以该内容加上特定头部信息一起的 SHA-1 校验和为文件命名。 校验和的前两个字符用于命名子目录,余下的 38 个字符则用作文件名。
一旦你将内容存储在了对象数据库中,那么可以通过cat-file命令从 Git 那里取回数据。

cat-file指定-p选项可指示该命令自动判断内容的类型,并为我们显示大致的内容:

$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content

在这个(简单的版本控制)系统中,文件名并没有被保存——我们仅保存了文件的内容。 上述类型的对象我们称之为数据对象(blob object)。 利用git cat-file -t命令,可以让 Git 告诉我们其内部存储的任何对象类型,只要给定该对象的 SHA-1 值。

2.2 树对象

接下来要探讨的 Git 对象类型是树对象(tree object),它能解决文件名保存的问题,也允许我们将多个文件组织到一起。 Git 以一种类似于 UNIX 文件系统的方式存储内容,但作了些许简化。 所有内容均以树对象和数据对象的形式存储,其中树对象对应了 UNIX 中的目录项,数据对象则大致上对应了 inodes 或文件内容。 一个树对象包含了一条或多条树对象记录(tree entry),每条记录含有一个指向数据对象或者子树对象的 SHA-1 指针,以及相应的模式、类型、文件名信息。 例如,某项目当前对应的最新树对象可能是这样的:

$ git cat-file -p master^{tree}
100644 blob a906cb2a4a904a152e80877d4088654daad0c859      README
100644 blob 8f94139338f9404f26296befa88755fc2598c289      Rakefile
040000 tree 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0      lib

master^{tree}语法表示master分支上最新的提交所指向的树对象。 请注意,lib子目录(所对应的那条树对象记录)并不是一个数据对象,而是一个指针,其指向的是另一个树对象:

$ git cat-file -p 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0
100644 blob 47c6340d6459e05787f644c2447d2595f5d3a54b      simplegit.rb

从概念上讲,Git 内部存储的数据有点像这样:图片

通常,Git 根据某一时刻暂存区(即 index 区域,下同)所表示的状态创建并记录一个对应的树对象, 如此重复便可依次记录(某个时间段内)一系列的树对象。 因此,为创建一个树对象,首先需要通过暂存一些文件来创建一个暂存区。 可以通过底层命令git update-index为一个单独文件——我们的 test.txt 文件的首个版本——创建一个暂存区。 利用该命令,可以把test.txt文件的首个版本人为地加入一个新的暂存区。 必须为上述命令指定--add选项,因为此前该文件并不在暂存区中(我们甚至都还没来得及创建一个暂存区呢); 同样必需的还有--cacheinfo选项,因为将要添加的文件位于 Git 数据库中,而不是位于当前目录下。 同时,需要指定文件模式、SHA-1 与文件名:

$ git update-index --add --cacheinfo 100644 \
  83baae61804e65cc73a7201a7252750c76066a30 test.txt

现在,可以通过git write-tree命令将暂存区内容写入一个树对象。 此处无需指定-w选项——如果某个树对象此前并不存在的话,当调用此命令时, 它会根据当前暂存区状态自动创建一个新的树对象:

$ git write-tree

2.3 提交对象

树对象,分别代表我们想要跟踪的不同项目快照。 若想重用这些快照,你必须记住所有三个 SHA-1 哈希值。 并且,你也完全不知道是谁保存了这些快照,在什么时刻保存的,以及为什么保存这些快照。 而以上这些,正是提交对象(commit object)能为你保存的基本信息。

可以通过调用commit-tree命令创建一个提交对象,为此需要指定一个树对象的 SHA-1 值,以及该提交的父提交对象(如果有的话)。 我们从之前创建的第一个树对象开始:

$ echo 'first commit' | git commit-tree d8329f
17d6e19d51cb44b229b966ae2206c4007b49258b
$ git cat-file -p fdf4fc3
tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579
author kingyuluk <kingyuluk@hotmail.com> 1613987837 +0800
committer kingyuluk <kingyuluk@hotmail.com> 1613987837 +0800
first commit

提交对象的格式很简单:它先指定一个顶层树对象,代表当前项目快照; 然后是可能存在的父提交(前面描述的提交对象并不存在任何父提交); 之后是作者/提交者信息(依据你的user.nameuser.email配置来设定,外加一个时间戳); 留空一行,最后是提交注释。

现在,如果对最后一个提交的 SHA-1 值运行git log命令,会出乎意料的发现,你已有一个货真价实的、可由git log查看的 Git 提交历史了:

没有借助任何上层命令,仅凭几个底层操作便完成了一个 Git 提交历史的创建。 这就是每次我们运行git addgit commit命令时,Git 所做的工作实质就是将被改写的文件保存为数据对象, 更新暂存区,记录树对象,最后创建一个指明了顶层树对象和父提交的提交对象。 这三种主要的 Git 对象——数据对象、树对象、提交对象——最初均以单独文件的形式保存在.git/objects目录下。

如果跟踪所有的内部指针,将得到一个类似下面的对象关系图:

图片

2.4 对象存储

所有的 Git 对象均同样方式存储,区别仅在于类型标识——另两种对象类型的头部信息以字符串“commit”或“tree”开头,而不是“blob”。 另外,虽然数据对象的内容几乎可以是任何东西,但提交对象和树对象的内容却有各自固定的格式。

3. Git 引用

如果你对仓库中从一个提交(比如1a410e)开始往前的历史感兴趣,那么可以运行git log 1a410e这样的命令来显示历史,不过你需要记得1a410e是你查看历史的起点提交。 如果我们有一个文件来保存 SHA-1 值,而该文件有一个简单的名字, 然后用这个名字指针来替代原始的 SHA-1 值的话会更加简单。

在 Git 中,这种简单的名字被称为“引用(references,或简写为 refs)”。 你可以在.git/refs目录下找到这类含有 SHA-1 值的文件。 在目前的项目中,这个目录没有包含任何文件,但它包含了一个简单的目录结构:

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

若要创建一个新引用来帮助记忆最新提交所在的位置,从技术上讲我们只需简单地做如下操作:

$ echo 1a410efbd13591db07496601ebc7a059dd55cfe9 > .git/refs/heads/master

现在,你就可以在 Git 命令中使用这个刚创建的新引用来代替 SHA-1 值了:

$ git log --pretty=oneline master
1a410efbd13591db07496601ebc7a059dd55cfe9 third commit
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit

我们不提倡直接编辑引用文件。 如果想更新某个引用,Git 提供了一个更加安全的命令update-ref来完成此事:

$ git update-ref refs/heads/master 1a410efbd13591db07496601ebc7a059dd55cfe9

这基本就是 Git 分支的本质:一个指向某一系列提交之首的指针或引用。 若想在第二个提交上创建一个分支,可以这么做:

$ git update-ref refs/heads/test cac0ca

这个分支将只包含从第二个提交开始往前追溯的记录:

$ git log --pretty=oneline test
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit

至此,我们的 Git 数据库从概念上看起来像这样:

图片

当运行类似于git branch <branch>这样的命令时,Git 实际上会运行update-ref命令, 取得当前所在分支最新提交对应的 SHA-1 值,并将其加入你想要创建的任何新引用中。

3.1 HEAD 引用

现在的问题是,当你执行git branch <branch>时,Git 如何知道最新提交的 SHA-1 值呢? 答案是 HEAD 文件。

HEAD 文件通常是一个符号引用(symbolic reference),指向目前所在的分支。 所谓符号引用,表示它是一个指向其他引用的指针。

然而在某些罕见的情况下,HEAD 文件可能会包含一个 git 对象的 SHA-1 值。 当你在检出一个标签、提交或远程分支,让你的仓库变成“分离 HEAD”状态时,就会出现这种情况。

如果查看 HEAD 文件的内容,通常我们看到类似这样的内容:

$ cat .git/HEAD
ref: refs/heads/master

如果执行git checkout test,Git 会像这样更新 HEAD 文件:

$ cat .git/HEAD
ref: refs/heads/test

当我们执行git commit时,该命令会创建一个提交对象,并用 HEAD 文件中那个引用所指向的 SHA-1 值设置其父提交字段。

也可以手动编辑该文件,然而同样存在一个更安全的命令来完成此事:git symbolic-ref。 可以借助此命令来查看 HEAD 引用对应的值:

$ git symbolic-ref HEAD
refs/heads/master

3.2 标签引用

前面我们刚讨论过 Git 的三种主要的对象类型(数据对象树对象提交对象),然而实际上还有第四种。**标签对象(tag object)**非常类似于一个提交对象——它包含一个标签创建者信息、一个日期、一段注释信息,以及一个指针。 主要的区别在于,标签对象通常指向一个提交对象,而不是一个树对象。 它像是一个永不移动的分支引用——永远指向同一个提交对象,只不过给这个提交对象加上一个更友好的名字罢了。

正如Git 基础中所讨论的那样,存在两种类型的标签:附注标签和轻量标签。 可以像这样创建一个轻量标签:

$ git update-ref refs/tags/v1.0 cac0cab538b970a37ea1e769cbbde608743bc96d

这就是轻量标签的全部内容——一个固定的引用。
然而,一个附注标签则更复杂一些。 若要创建一个附注标签,Git 会创建一个标签对象,并记录一个引用来指向该标签对象,而不是直接指向提交对象。 可以通过创建一个附注标签来验证这个过程(使用-a选项):

$ git tag -a v1.1 1a410efbd13591db07496601ebc7a059dd55cfe9 -m 'test tag'

面是上述过程所建标签对象的 SHA-1 值:

$ cat .git/refs/tags/v1.1
9585191f37f7b0fb9444f35a9bf50de191beadc2

现在对该 SHA-1 值运行git cat-file -p命令:

$ git cat-file -p 9585191f37f7b0fb9444f35a9bf50de191beadc2
object 1a410efbd13591db07496601ebc7a059dd55cfe9
type commit
tag v1.1
tagger Scott Chacon <schacon@gmail.com> Sat May 23 16:48:58 2009 -0700
test tag

我们注意到,object 条目指向我们打了标签的那个提交对象的 SHA-1 值。 另外要注意的是,标签对象并非必须指向某个提交对象;你可以对任意类型的 Git 对象打标签。

3.3 远程引用

我们将看到的第三种引用类型是远程引用(remote reference)。 如果你添加了一个远程版本库并对其执行过推送操作,Git 会记录下最近一次推送操作时每一个分支所对应的值,并保存在refs/remotes目录下。

例如,你可以添加一个叫做origin的远程版本库,然后把master分支推送上去:

$ git remote add origin git@github.com:schacon/simplegit-progit.git
$ git push origin master
Counting objects: 11, done.
Compressing objects: 100% (5/5), done.
Writing objects: 100% (7/7), 716 bytes, done.
Total 7 (delta 2), reused 4 (delta 1)
To git@github.com:schacon/simplegit-progit.git
  a11bef0..ca82a6d  master -> master

此时,如果查看refs/remotes/origin/master文件,可以发现origin远程版本库的master分支所对应的 SHA-1 值,就是最近一次与服务器通信时本地master分支所对应的 SHA-1 值:

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

远程引用和分支(位于refs/heads目录下的引用)之间最主要的区别在于,远程引用是只读的。 虽然可以git checkout到某个远程引用,但是 Git 并不会将 HEAD 引用指向该远程引用。因此,你永远不能通过commit命令来更新远程引用。 Git 将这些远程引用作为记录远程服务器上各分支最后已知位置状态的书签来管理。

4. 包文件

https://git-scm.com/book/zh/v2/Git-内部原理-包文件

当文件发生更改,Git 会用一个全新的对象来存储新的文件内容。

Git 最初向磁盘中存储对象时所使用的格式被称为“松散(loose)”对象格式。 但是,Git 会时不时地将多个这些对象打包成一个称为“包文件(packfile)”的二进制文件,以节省空间和提高效率。 当版本库中有太多的松散对象,或者你手动执行git gc命令,或者你向远程服务器执行推送时,Git 都会这样做。 要看到打包过程,你可以手动执行git gc命令让 Git 对对象进行打包:

 $ git gc

这个时候再查看objects目录,你会发现大部分的对象都不见了,与此同时出现了一对新文件:

*.idx
*.pack

仍保留着的几个对象是未被任何提交记录引用的数据对象, 因为你从没将它们添加至任何提交记录中,所以 Git 认为它们是悬空(dangling)的,不会将它们打包进新生成的包文件中。
剩下的文件是新创建的包文件和一个索引。 包文件包含了刚才从文件系统中移除的所有对象的内容。 索引文件包含了包文件的偏移信息,我们通过索引文件就可以快速定位任意一个指定对象。

Git 打包对象时,会查找命名及大小相近的文件,并只保存文件不同版本之间的差异内容。 你可以查看包文件,观察它是如何节省空间的。git verify-pack这个底层命令可以让你查看已打包的内容:

b042a60ef7dff760008df33cee372b945b6e884e blob   22054 5799 1463
033b4468fa6b2a9547a70d88d1bbe8bf3f9ed0d5 blob   9 20 7262 1 \
  b042a60ef7dff760008df33cee372b945b6e884e

此处,033b4这个数据对象(即repo.rb文件的第一个版本,如果你还记得的话)引用了数据对象b042a,即该文件的第二个版本。 命令输出内容的第三列显示的是各个对象在包文件中的大小,可以看到b042a占用了 22K 空间,而033b4仅占用 9 字节。 同样有趣的地方在于,第二个版本完整保存了文件内容,而原始的版本反而是以差异方式保存的——这是因为大部分情况下需要快速访问文件的最新版本。

5. 引用规范

我们已经使用过一些诸如远程分支到本地引用的简单映射方式,但这种映射可以更复杂。 假设你已经跟着前几节在本地创建了一个小的 Git 仓库,现在想要添加一个远程仓库:

$ git remote add origin https://github.com/schacon/simplegit-progit

运行上述命令会在你仓库中的.git/config文件中添加一个小节, 并在其中指定远程版本库的名称(origin)、URL 和一个用于获取操作的引用规范(refspec)

[remote "origin"]
	url = https://github.com/schacon/simplegit-progit
	fetch = +refs/heads/*:refs/remotes/origin/*

引用规范的格式由一个可选的+号和紧随其后的<src>:<dst>组成, 其中<src>是一个模式(pattern),代表远程版本库中的引用;<dst>是本地跟踪的远程引用的位置。+号告诉 Git 即使在不能快进的情况下也要(强制)更新引用。

默认情况下,引用规范由git remote add origin命令自动生成, Git 获取服务器中refs/heads/下面的所有引用,并将它写入到本地的refs/remotes/origin/中。 所以,如果服务器上有一个master分支,你可以在本地通过下面任意一种方式来访问该分支上的提交记录:

$ git log origin/master
$ git log remotes/origin/master
$ git log refs/remotes/origin/master

上面的三个命令作用相同,因为 Git 会把它们都扩展成refs/remotes/origin/master
如果想让 Git 每次只拉取远程的master分支,而不是所有分支, 可以把(引用规范的)获取那一行修改为只引用该分支:

fetch = +refs/heads/master:refs/remotes/origin/master

这仅是针对该远程版本库的git fetch操作的默认引用规范。 如果有某些只希望被执行一次的操作,我们也可以在命令行指定引用规范。 若要将远程的master分支拉到本地的origin/mymaster分支,可以运行:

$ git fetch origin master:refs/remotes/origin/mymaster

5.1 引用规范推送

像上面这样从远程版本库获取已在命名空间中的引用当然很棒,但 QA 团队最初应该如何将他们的分支放入远程的qa/命名空间呢? 我们可以通过引用规范推送来完成这个任务。

如果 QA 团队想把他们的master分支推送到远程服务器的qa/master分支上,可以运行:

$ git push origin master:refs/heads/qa/master

如果他们希望 Git 每次运行git push origin时都像上面这样推送,可以在他们的配置文件中添加一条push值:

[remote "origin"]
	url = https://github.com/schacon/simplegit-progit
	fetch = +refs/heads/*:refs/remotes/origin/*
	push = refs/heads/master:refs/heads/qa/master

正如刚才所指出的,这会让git push origin默认把本地master分支推送到远程qa/master分支。

6. 维护与数据恢复

6.1 维护

Git 会不定时地自动运行一个叫做 “auto gc” 的命令。 大多数时候,这个命令并不会产生效果。 然而,如果有太多松散对象(不在包文件中的对象)或者太多包文件,Git 会运行一个完整的git gc命令。 “gc” 代表垃圾回收,这个命令会做以下事情:收集所有松散对象并将它们放置到包文件中, 将多个包文件合并为一个大的包文件,移除与任何提交都不相关的陈旧对象。

手动执行自动垃圾回收:

$ git gc --auto

这个命令通常并不会产生效果。 大约需要 7000 个以上的松散对象或超过 50 个的包文件才能让 Git 启动一次真正的 gc 命令。 你可以通过修改gc.autogc.autopacklimit的设置来改动这些数值。
gc将会做的另一件事是打包你的引用到一个单独的文件。 假设你的仓库包含以下分支与标签:

$ find .git/refs -type f
.git/refs/heads/experiment
.git/refs/heads/master
.git/refs/tags/v1.0
.git/refs/tags/v1.1

如果你执行了git gc命令,refs目录中将不会再有这些文件。 为了保证效率 Git 会将它们移动到名为.git/packed-refs的文件中,就像这样:

$ cat .git/packed-refs
# pack-refs with: peeled fully-peeled
cac0cab538b970a37ea1e769cbbde608743bc96d refs/heads/experiment
ab1afef80fac8e34258ff41fc1b867c702daa24b refs/heads/master
cac0cab538b970a37ea1e769cbbde608743bc96d refs/tags/v1.0
9585191f37f7b0fb9444f35a9bf50de191beadc2 refs/tags/v1.1
^1a410efbd13591db07496601ebc7a059dd55cfe9

如果你更新了引用,Git 并不会修改这个文件,而是向refs/heads创建一个新的文件。 为了获得指定引用的正确 SHA-1 值,Git 会首先在refs目录中查找指定的引用,然后再到packed-refs文件中查找。 所以,如果你在refs目录中找不到一个引用,那么它或许在packed-refs文件中。

注意这个文件的最后一行,它会以^开头。 这个符号表示它上一行的标签是附注标签,^所在的那一行是附注标签指向的那个提交。

6.2 数据恢复

在你使用 Git 的时候,你可能会意外丢失一次提交。 通常这是因为你强制删除了正在工作的分支,但是最后却发现你还需要这个分支, 亦或者硬重置了一个分支,放弃了你想要的提交。

git reset --hard 后,在指定提交后的提交已经丢失了——没有分支指向这些提交。你需要找出最后一次提交的 SHA-1 然后增加一个指向它的分支。 窍门就是找到最后一次的提交的 SHA-1。

最方便,也是最常用的方法,是使用一个名叫git reflog的工具。 当你正在工作时,Git 会默默地记录每一次你改变 HEAD 时它的值。 每一次你提交或改变分支,引用日志都会被更新。 引用日志(reflog)也可以通过git update-ref命令更新,我们在Git 引用有提到使用这个命令而不是是直接将 SHA-1 的值写入引用文件中的原因。 你可以在任何时候通过执行git reflog命令来了解你曾经做过什么。为了使显示的信息更加有用,我们可以执行git log -g,这个命令会以标准日志的格式输出引用日志。

可以通过创建一个新的分支指向这个提交来恢复它。 例如,你可以创建一个名为recover-branch的分支指向这个提交(ab1afef):

$ git branch recover-branch ab1afef

如果没有引用日志了,可以使用git fsck实用工具,将会检查数据库的完整性。 如果使用一个--full选项运行它,它会向你显示出所有没有被其他对象指向的对象:

$ git fsck --full
Checking object directories: 100% (256/256), done.
Checking objects: 100% (18/18), done.
dangling blob d670460b4b4aece5915caf5c68d12f560a9fe3e4
dangling commit ab1afef80fac8e34258ff41fc1b867c702daa24b
dangling tree aea790b9a58f6cf6f2804eeac9f0abbe9631e4c9
dangling blob 7108f7ecb345ee9d0084193f147cdad4d2998293

在这个例子中,你可以在 “dangling commit” 后看到你丢失的提交。 现在你可以用和之前相同的方法恢复这个提交,也就是添加一个指向这个提交的分支。

6.3 移除对象

Git 有很多很棒的功能,但是其中一个特性会导致问题,git clone会下载整个项目的历史,包括每一个文件的每一个版本。 如果所有的东西都是源代码那么这很好,因为 Git 被高度优化来有效地存储这种数据。 然而,如果某个人在之前向项目添加了一个大小特别大的文件,即使你将这个文件从项目中移除了,每次克隆还是都要强制的下载这个大文件。 之所以会产生这个问题,是因为这个文件在历史中是存在的,它会永远在那里。

**警告:这个操作对提交历史的修改是破坏性的。**它会从你必须修改或移除一个大文件引用最早的树对象开始重写每一次提交。 如果你在导入仓库后,在任何人开始基于这些提交工作前执行这个操作,那么将不会有任何问题——否则, 你必须通知所有的贡献者他们需要将他们的成果变基到你的新提交上。

移除项目文件:

$ git rm git.tgz
rm 'git.tgz'
$ git commit -m 'oops - removed large tarball'

可以执行count-objects命令来快速的查看占用空间大小:

$ git count-objects -v
count: 7
size: 32
in-pack: 17
packs: 1
size-pack: 4868
prune-packable: 0
garbage: 0
size-garbage: 0

size-pack的数值指的是你的包文件以 KB 为单位计算的大小,所以你大约占用了 5MB 的空间。 在最后一次提交前,使用了不到 2KB ——显然,从之前的提交中移除文件并不能从历史中移除它。 每一次有人克隆这个仓库时,他们将必须克隆所有的 5MB 来获得这个微型项目,只因为你意外地添加了一个大文件。 现在来让我们彻底的移除这个文件。
首先你必须找到它。 在本例中,你已经知道是哪个文件了。 但是假设你不知道;该如何找出哪个文件或哪些文件占用了如此多的空间? 如果你执行git gc命令,所有的对象将被放入一个包文件中,你可以通过运行git verify-pack命令, 然后对输出内容的第三列(即文件大小)进行排序,从而找出这个大文件。 你也可以将这个命令的执行结果通过管道传送给tail命令,因为你只需要找到列在最后的几个大对象。

$ git verify-pack -v .git/objects/pack/pack-29…69.idx \
  | sort -k 3 -n \
  | tail -3
dadf7258d699da2c8d89b09ef6670edb7d5f91b4 commit 229 159 12
033b4468fa6b2a9547a70d88d1bbe8bf3f9ed0d5 blob   22044 5792 4977696
82c99a3e86bb1267b236a4b6eff7868d97489af1 blob   4975916 4976258 1438

可以看到这个大对象出现在返回结果的最底部:占用 5MB 空间。 为了找出具体是哪个文件,可以使用rev-list命令,我们在指定特殊的提交信息格式中曾提到过。 如果你传递--objects参数给rev-list命令,它就会列出所有提交的 SHA-1、数据对象的 SHA-1 和与它们相关联的文件路径。 可以使用以下命令来找出你的数据对象的名字:

git rev-list基本类似git log命令,但它默认只输出 SHA-1 值而已,没有其他信息。

$ git rev-list --objects --all | grep 82c99a3
82c99a3e86bb1267b236a4b6eff7868d97489af1 git.tgz

现在,你只需要从过去所有的树中移除这个文件。 使用以下命令可以轻松地查看哪些提交对这个文件产生改动:

$ git log --oneline --branches -- git.tgz
dadf725 oops - removed large tarball
7b30847 add git tarball

现在,你必须重写7b30847提交之后的所有提交来从 Git 历史中完全移除这个文件。 为了执行这个操作,我们要使用filter-branch命令,这个命令在重写历史中也使用过:

$ git filter-branch --index-filter \
  'git rm --ignore-unmatch --cached git.tgz' -- 7b30847^..
Rewrite 7b30847d080183a1ab7d18fb202473b3096e9f34 (1/2)rm 'git.tgz'
Rewrite dadf7258d699da2c8d89b09ef6670edb7d5f91b4 (2/2)
Ref 'refs/heads/master' was rewritten

--index-filter选项类似于在重写历史中提到的的--tree-filter选项, 不过这个选项并不会让命令将修改在硬盘上检出的文件,而只是修改在暂存区或索引中的文件。

你必须使用git rm --cached命令来移除文件,而不是通过类似rm file的命令——因为你需要从索引中移除它,而不是磁盘中。 还有一个原因是速度—— Git 在运行过滤器时,并不会检出每个修订版本到磁盘中,所以这个过程会非常快。 如果愿意的话,你也可以通过--tree-filter选项来完成同样的任务。git rm命令的--ignore-unmatch选项告诉命令:如果尝试删除的模式不存在时,不提示错误。 最后,使用filter-branch选项来重写自7b30847提交以来的历史,也就是这个问题产生的地方。 否则,这个命令会从最旧的提交开始,这将会花费许多不必要的时间。

你的历史中将不再包含对那个文件的引用。 不过,你的引用日志和你在.git/refs/original通过filter-branch选项添加的新引用中还存有对这个文件的引用,所以你必须移除它们然后重新打包数据库。 在重新打包前需要移除任何包含指向那些旧提交的指针的文件:

$ rm -Rf .git/refs/original
$ rm -Rf .git/logs/
$ git gc

看看省了多少空间。

$ git count-objects -v
count: 11
size: 4904
in-pack: 15
packs: 1
size-pack: 8
prune-packable: 0
garbage: 0
size-garbage: 0

打包的仓库大小下降到了 8K,比 5MB 好很多。 可以从 size 的值看出,这个大文件还在你的松散对象中,并没有消失;但是它不会在推送或接下来的克隆中出现,这才是最重要的。 如果真的想要删除它,可以通过有--expire选项的git prune命令来完全地移除那个对象:

$ git prune --expire now
$ git count-objects -v
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值