-
1. 起步
-
2. Git 基础
-
3. Git 分支
-
4. 服务器上的 Git
- 4.1 协议
- 4.2 在服务器上部署 Git
- 4.3 生成 SSH 公钥
- 4.4 架设服务器
- 4.5 Git Daemon
- 4.6 Smart HTTP
- 4.7 GitWeb
- 4.8 GitLab
- 4.9 第三方托管服务
- 4.10 小结
-
5. 分布式 Git
-
A1. 附录 A: Git 在其他环境
- A1.1 图形界面
- A1.2 Visual Studio 中的 Git
- A1.3 Visual Studio Code 中的 Git
- A1.4 IntelliJ / PyCharm / WebStorm / PhpStorm / RubyMine 中的 Git
- A1.5 Sublime Text 中的 Git
- A1.6 Bash 中的 Git
- A1.7 Zsh 中的 Git
- A1.8 PowerShell 中的 Git
- A1.9 小结
-
A2. 附录 B: 在应用程序中嵌入 Git
-
A3. 附录 C: Git 命令
10.7 Git 内部原理 - 维护与数据恢复
维护与数据恢复
有时,你可能需要进行一些清理工作——让仓库更紧凑,清理导入的仓库,或者恢复丢失的工作。本节将涵盖其中一些场景。
维护
Git 会偶尔自动运行一个名为“auto gc”的命令。大多数时候,这个命令什么也不做。但是,如果存在过多的松散对象(不在 packfile 中的对象)或过多的 packfile,Git 就会启动一个完整的 git gc
命令。“gc”代表垃圾收集(garbage collect),该命令执行多项操作:它收集所有松散对象并将其放入 packfile 中,它将多个 packfile 合并成一个大的 packfile,并删除那些无法从任何提交中访问且已有几个月历史的对象。
你可以手动运行 auto gc
,如下所示
$ git gc --auto
同样,这通常什么也不做。你必须有大约 7,000 个或更多的松散对象,或者超过 50 个 packfile,Git 才会真正执行 gc
命令。你可以分别通过 gc.auto
和 gc.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
文件中。
请注意文件的最后一行,它以 ^
开头。这意味着正上方是带附注的标签,该行是带附注的标签指向的提交。
数据恢复
在你使用 Git 的过程中,有时你可能会不小心丢失一个提交。通常,这会发生在你强制删除一个包含工作但事后发现需要该分支的分支,或者你硬重置(hard-reset)了一个分支,从而放弃了你想要某些内容的提交。假设这种情况发生了,你如何找回你的提交呢?
这里有一个例子,它将你的测试仓库中的 master
分支硬重置到一个旧的提交,然后恢复丢失的提交。首先,让我们回顾一下你的仓库目前的状态
$ git log --pretty=oneline
ab1afef80fac8e34258ff41fc1b867c702daa24b Modify repo.rb a bit
484a59275031909e19aadb7c92262719cfcdf19a Create repo.rb
1a410efbd13591db07496601ebc7a059dd55cfe9 Third commit
cac0cab538b970a37ea1e769cbbde608743bc96d Second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d First commit
现在,将 master
分支移回中间的提交
$ git reset --hard 1a410efbd13591db07496601ebc7a059dd55cfe9
HEAD is now at 1a410ef Third commit
$ git log --pretty=oneline
1a410efbd13591db07496601ebc7a059dd55cfe9 Third commit
cac0cab538b970a37ea1e769cbbde608743bc96d Second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d First commit
你实际上已经丢失了最上面的两个提交——你没有分支可以访问这些提交。你需要找到最新的提交 SHA-1,然后添加一个指向它的分支。关键在于找到那个最新的提交 SHA-1——你不可能记住它,对吧?
通常,最快的方法是使用一个名为 git reflog
的工具。在你工作时,Git 会在你每次改变 HEAD 时悄悄记录你的 HEAD 是什么。每当你提交或切换分支时,reflog 都会更新。reflog 也会被 git update-ref
命令更新,这也是使用它而不是仅仅将 SHA-1 值写入你的引用文件的另一个原因,正如我们在Git 引用中介绍的。你可以随时通过运行 git reflog
来查看你到过哪里
$ git reflog
1a410ef HEAD@{0}: reset: moving to 1a410ef
ab1afef HEAD@{1}: commit: Modify repo.rb a bit
484a592 HEAD@{2}: commit: Create repo.rb
在这里我们可以看到我们已经检出的两个提交,但是这里没有太多的信息。为了以更有用的方式查看相同的信息,我们可以运行 git log -g
,它将为你提供 reflog 的正常日志输出。
$ git log -g
commit 1a410efbd13591db07496601ebc7a059dd55cfe9
Reflog: HEAD@{0} (Scott Chacon <schacon@gmail.com>)
Reflog message: updating HEAD
Author: Scott Chacon <schacon@gmail.com>
Date: Fri May 22 18:22:37 2009 -0700
Third commit
commit ab1afef80fac8e34258ff41fc1b867c702daa24b
Reflog: HEAD@{1} (Scott Chacon <schacon@gmail.com>)
Reflog message: updating HEAD
Author: Scott Chacon <schacon@gmail.com>
Date: Fri May 22 18:15:24 2009 -0700
Modify repo.rb a bit
看来最底部的提交是你丢失的那个,所以你可以通过在该提交处创建一个新分支来恢复它。例如,你可以在该提交 (ab1afef) 处启动一个名为 recover-branch
的分支
$ git branch recover-branch ab1afef
$ git log --pretty=oneline recover-branch
ab1afef80fac8e34258ff41fc1b867c702daa24b Modify repo.rb a bit
484a59275031909e19aadb7c92262719cfcdf19a Create repo.rb
1a410efbd13591db07496601ebc7a059dd55cfe9 Third commit
cac0cab538b970a37ea1e769cbbde608743bc96d Second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d First commit
酷——现在你有一个名为 recover-branch
的分支,它位于你 master
分支曾经的位置,使得前两个提交再次可访问。接下来,假设你的丢失由于某种原因不在 reflog 中——你可以通过删除 recover-branch
并删除 reflog 来模拟这一点。现在前两个提交无法被任何东西访问
$ git branch -D recover-branch
$ rm -Rf .git/logs/
由于 reflog 数据保存在 .git/logs/
目录中,你实际上没有 reflog。此时,你如何恢复该提交?一种方法是使用 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”之后看到你丢失的提交。你可以用同样的方法恢复它,即添加一个指向该 SHA-1 的分支。
移除对象
Git 有很多优点,但一个可能导致问题的功能是 git clone
会下载项目的整个历史,包括每个文件的每个版本。如果整个项目都是源代码,这没有问题,因为 Git 经过高度优化,可以高效地压缩这些数据。然而,如果项目历史中的某个时刻有人添加了一个巨大的文件,那么每次克隆都将永远被迫下载那个大文件,即使它在下一个提交中就已经从项目中删除了。因为它可以从历史中访问到,所以它将永远存在。
当你将 Subversion 或 Perforce 仓库转换为 Git 时,这可能是一个巨大的问题。因为在这些系统中你不会下载整个历史,所以这种添加带来的后果很少。如果你是从其他系统导入的,或者发现你的仓库比预期大得多,这里将介绍如何查找和删除大对象。
警告:此技术会破坏你的提交历史。它会重写从你需要修改的最小树算起的所有后续提交对象,以删除大文件引用。如果你在导入后立即执行此操作,在任何人开始基于该提交进行工作之前,你是安全的——否则,你必须通知所有贡献者,他们必须将他们的工作rebase到你的新提交上。
为了演示,我们将在你的测试仓库中添加一个大文件,在下一个提交中将其删除,然后找到它,并将其从仓库中永久删除。首先,向你的历史中添加一个大对象
$ curl -L https://linuxkernel.org.cn/pub/software/scm/git/git-2.1.0.tar.gz > git.tgz
$ git add git.tgz
$ git commit -m 'Add git tarball'
[master 7b30847] Add git tarball
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 git.tgz
哎呀——你不想将一个巨大的 tar 包添加到你的项目中。最好把它删掉
$ git rm git.tgz
rm 'git.tgz'
$ git commit -m 'Oops - remove large tarball'
[master dadf725] Oops - remove large tarball
1 file changed, 0 insertions(+), 0 deletions(-)
delete mode 100644 git.tgz
现在,对你的数据库进行 gc
并查看你占用了多少空间
$ git gc
Counting objects: 17, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (13/13), done.
Writing objects: 100% (17/17), done.
Total 17 (delta 1), reused 10 (delta 0)
你可以运行 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
条目是你的 packfile 的大小(以 KB 为单位),所以你使用了将近 5MB。在上次提交之前,你使用的空间接近 2K——显然,从上一个提交中删除文件并没有将其从你的历史记录中移除。每次有人克隆这个仓库时,他们都必须克隆所有 5MB,仅仅为了获取这个小项目,因为你不小心添加了一个大文件。让我们把它删除。
首先你必须找到它。在这种情况下,你已经知道是哪个文件了。但是假设你不知道;你如何识别是哪个或哪些文件占用了这么多空间呢?如果你运行 git gc
,所有对象都在一个 packfile 中;你可以通过运行另一个底层命令 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
命令,你在强制执行特定的提交消息格式中简要使用过它。如果你向 rev-list
传递 --objects
,它会列出所有提交的 SHA-1 以及与它们关联的文件路径的 blob SHA-1。你可以使用它来查找你的 blob 的名称
$ git rev-list --objects --all | grep 82c99a3
82c99a3e86bb1267b236a4b6eff7868d97489af1 git.tgz
现在,你需要将此文件从你历史中的所有树中删除。你可以轻松地查看哪些提交修改了此文件
$ git log --oneline --branches -- git.tgz
dadf725 Oops - remove 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
选项类似,不同之处在于,你不是传递一个修改磁盘上已检出文件的命令,而是每次修改你的暂存区或索引。
你不能像 rm file
那样删除特定文件,而必须使用 git rm --cached
删除它——你必须从索引中删除它,而不是从磁盘中删除。这样做的原因是速度——因为 Git 无需在运行过滤器之前将每个修订版本检出到磁盘,所以这个过程会快得多。如果你愿意,可以使用 --tree-filter
完成相同的任务。git rm
的 --ignore-unmatch
选项告诉它,如果尝试删除的模式不存在,则不要报错。最后,你要求 filter-branch
只从 7b30847
提交开始重写你的历史,因为你知道问题是从那里开始的。否则,它将从头开始,不必要地花费更长的时间。
你的历史中不再包含对该文件的引用。但是,你的 reflog 和 Git 在你执行 filter-branch
时在 .git/refs/original
下添加的一组新引用仍然包含它,所以你必须删除它们,然后重新打包数据库。在重新打包之前,你需要删除任何指向那些旧提交的内容
$ rm -Rf .git/refs/original
$ rm -Rf .git/logs/
$ git gc
Counting objects: 15, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (11/11), done.
Writing objects: 100% (15/15), done.
Total 15 (delta 1), reused 12 (delta 0)
让我们看看你节省了多少空间。
$ 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
count: 0
size: 0
in-pack: 15
packs: 1
size-pack: 8
prune-packable: 0
garbage: 0
size-garbage: 0