要在Bash下实现 git.multi_gc 功能,首先需要了解gc功能的实质。gc功能实际上由Git中几个不常用的辅助命令和低级命令组合而成。
GC功能涉及的过程
git-pack-refs
git-pack-refs会对$GIT_DIR/refs目录下的heads和tags的打包操作,将结果并保存到$GIT_DIR/packed-refs中。
命令格式:
git pack-refs [--all] [--no-prune]
常用选项:
--all
打包所有的heads和tags,如果不开启该项,则会只打包已经被打包过的heads和所有tags。
git-reflog
git-reflog有3个子命令:expire,show和delete。其中,expire用于删除陈旧的reflog记录。
命令格式:
git reflog expire [--dry-run] [--stale-fix] [--verbose] [--expire=<time>] [--expire-unreachable=<time>] [--all] <refs>…
常用选项:
--expire=<time>
在此期限之前的记录将被删除。
--expire-unreachable=<time>
在此期限之前的且从当前分支顶端不可达的记录将被删除。
--all
作用于所有引用。
git-repack
git repack [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [--window=<n>] [--depth=<n>]
-a
-A
-d
-l
-f
-q
--window=<n> --depth=<n>
git-prune
git prune [-n] [-v] [--expire <expire>] [--] [<head>…]
--expire <time>
git-rerere
git rerere gc
其他需要了解的过程
git-rev-list
--all
git-fsck
问题描述
#!/bin/sh
function do_init() {
mkdir -p ta;
git --git-dir=ta/.git --work-tree=ta init;
echo "OK, first content!" > ta/a.txt;
git --git-dir=ta/.git --work-tree=ta add a.txt;
git --git-dir=ta/.git --work-tree=ta commit -am "initial commit";
mkdir -p ta/p;
echo "OK, second content!" > ta/p/b.txt;
git --git-dir=ta/.git --work-tree=ta add p/b.txt;
git --git-dir=ta/.git --work-tree=ta commit -am "second commit";
# new branch
git --git-dir=ta/.git --work-tree=ta branch new;
echo "Oh, I am wrong!" > ta/a.txt;
git --git-dir=ta/.git --work-tree=ta commit -am "third commit";
# switch
git --git-dir=ta/.git --work-tree=ta checkout new;
echo "No, I am right actually!" > ta/a.txt;
git --git-dir=ta/.git --work-tree=ta commit -am "forth commit";
echo "I am confused now!" > ta/a.txt;
git --git-dir=ta/.git --work-tree=ta commit -am "fifth commit";
# switch back
#git --git-dir=ta/.git --work-tree=ta checkout master;
git clone -s ta/.git tb;
}
function do_log() {
repo=$1;
git --git-dir=${repo}/.git --work-tree=${repo} log --all --graph;
}
function do_mod() {
# make some commits unreferenced
git --git-dir=ta/.git --work-tree=ta reset --hard HEAD^;
}
function do_gc() {
git --git-dir=ta/.git --work-tree=ta fsck; # show if there is dangling commit
git --git-dir=ta/.git --work-tree=ta fsck --no-reflogs; # show if there is unreferenced commit from reflog
# remove unreachable reflog
git --git-dir=ta/.git --work-tree=ta reflog expire --expire=now --all;
git --git-dir=ta/.git --work-tree=ta fsck; # show if there is dangling commit
# remove unreferenced objects
git --git-dir=ta/.git --work-tree=ta prune;
git --git-dir=ta/.git --work-tree=ta fsck; # show if there is dangling commit
}
function do_sta() {
repo=$1;
git --git-dir=${repo}/.git --work-tree=${repo} status;
}
do_init -> do_log ta -> do_mod -> do_gc -> do_sta tb
之后 do_mod 函数中对 ta 仓库进行更改,reset 会将分支的head从 commit 3b7958回复到 commit 0b7ee04,导致 commit 3b7958 编程未被引用状态(悬空状态)。do_gc 中的 reflog expire 和 prune 是 gc 功能的两个关键步骤,将会删除所有悬空对象,从而commit 3b7958被从数据库中删除,并再也无法恢复。
这一过程对 ta 仓库来说是完全合理的,但对从它派生的 tb 仓库来说却是致命的错误,我们调用 "do_sta tb" 显示 tb 仓库的状态,结果是:
fatal: bad object HEAD
原因很显然:对应的 commit 3b7958 早在对 ta 仓库进行更改的过程中被删除了。
同理,对 ta 仓库做 gc 时也会造成类似问题,所以,我们必须考虑好如何解决依赖带来的问题。