reflog 到底记录了什么、记多久?这篇直接读 .git/logs 下的文件,看哪些操作会写、哪些 ref 有,再造出 120 天到 5 天前的各种条目,验证默认的过期规则,最后演示用它撤销 reset --hard。
$ cat .git/logs/HEAD | sed -E 's/^0{40}/<全 0>/; s/[0-9a-f]{40}/<hash>/g; s/ [0-9]{10} / <时间戳> /'
<全 0> <hash> Alice <alice@example.com> <时间戳> +0800 commit (initial): 提交 1
<hash> <hash> Alice <alice@example.com> <时间戳> +0800 commit: 提交 2
<hash> <hash> Alice <alice@example.com> <时间戳> +0800 checkout: moving from main to dev
<hash> <hash> Alice <alice@example.com> <时间戳> +0800 checkout: moving from dev to main
<hash> <hash> Alice <alice@example.com> <时间戳> +0800 reset: moving to HEAD~1reflog 就是 .git/logs/HEAD 和 .git/logs/refs/heads/<分支> 这样的文本文件,一行一次移动:旧 hash、新 hash、操作者、时间戳、时区、说明。HEAD 有一份,每个分支一份。
$ ls .git/logs .git/logs/refs/heads
.git/logs:
HEAD
refs
.git/logs/refs/heads:
dev
main
$ git reflog show main --format='%gd: %gs'
main@{0}: reset: moving to HEAD~1
main@{1}: commit: 提交 2
main@{2}: commit (initial): 提交 1
$ for i in 0 1 2 3; do printf 'HEAD@{%s} → ' $i; git log -1 --format=%s HEAD@{$i}; done
HEAD@{0} → 提交 1
HEAD@{1} → 提交 2
HEAD@{2} → 提交 2
HEAD@{3} → 提交 2
$ { git log -1 --format=%s 'main@{5.minutes.ago}'; git log -1 --format=%s 'main@{1.hour.ago}'; } 2>&1 | sed -E 's/goes back to .*/goes back to <该 reflog 第一条记录的时间>/'
warning: log for 'main' only goes back to <该 reflog 第一条记录的时间>
提交 1
warning: log for 'main' only goes back to <该 reflog 第一条记录的时间>
提交 1HEAD@{n} 是第 n 次移动之前的位置,main@{5.minutes.ago} 按时间取。所要的时间早于该 reflog 的第一条记录时(实验里的仓库刚建好,5 分钟前就已经算“更早”),git 给出最早的一条并警告。
$ git reflog --format='%gd: %gs' | cut -c1-70
HEAD@{0}: rebase (finish): returning to refs/heads/main
HEAD@{1}: rebase (pick): 主线提交
HEAD@{2}: rebase (start): checkout side
HEAD@{3}: commit (amend): 改了说明
HEAD@{4}: merge side: Merge made by the 'ort' strategy.
HEAD@{5}: commit: 主线提交
HEAD@{6}: checkout: moving from side to main
HEAD@{7}: commit: 侧枝提交
HEAD@{8}: checkout: moving from main to side
HEAD@{9}: commit (initial): 提交 1凡是移动 ref 的操作:commit、amend、checkout、merge、rebase 的每一步、reset 等等,说明里写了是哪种操作,方便在找回时辨认。
$ ls .git/logs/refs; ls .git/logs/refs/heads; ls .git/logs/refs/tags 2>&1 | head -1; git config core.logAllRefUpdates || echo "(未设置)" heads main side ls: .git/logs/refs/tags: No such file or directory true $ git -c core.logAllRefUpdates=always tag v2; ls .git/logs/refs/tags v2 $ rm -rf l25bare.git && git init -q --bare l25bare.git && cd l25bare.git && git config core.logAllRefUpdates || echo "(裸仓库:未设置)"; H=$(git hash-object -w -t tree /dev/null); C=$(GIT_AUTHOR_NAME=a GIT_AUTHOR_EMAIL=a@e GIT_COMMITTER_NAME=a GIT_COMMITTER_EMAIL=a@e git commit-tree $H -m x); git update-ref refs/heads/main $C; ls logs 2>&1 | head -2; echo "(以上是裸仓库 update-ref 之后 logs 目录的内容)" (裸仓库:未设置) ls: logs: No such file or directory (以上是裸仓库 update-ref 之后 logs 目录的内容)
分支有,标签默认没有(core.logAllRefUpdates=always 才有),服务器端的裸仓库默认不记录。所以服务器上误删的东西不能指望 reflog。
expire 之前:
$ git reflog --format='%gd %gs' --date=relative | sed -E 's/ago\}/ago}/'
HEAD@{5 days ago} commit: C-5天前
HEAD@{10 days ago} checkout: moving from tmp to main
HEAD@{3 weeks ago} commit: U2-20天前
HEAD@{6 weeks ago} commit: U1-40天前
HEAD@{3 months ago} checkout: moving from main to tmp
HEAD@{3 months ago} commit: B-100天前
HEAD@{4 months ago} commit (initial): A-120天前
$ git reflog expire --all; echo "(已执行 git reflog expire --all,使用默认过期时间)"
(已执行 git reflog expire --all,使用默认过期时间)
expire 之后:
$ git reflog --format='%gd %gs' --date=relative | sed -E 's/ago\}/ago}/'
HEAD@{5 days ago} commit: C-5天前
HEAD@{10 days ago} checkout: moving from tmp to main
HEAD@{3 weeks ago} commit: U2-20天前reflog 不是永久的:可达提交的条目保留 90 天(gc.reflogExpire),不可达的保留 30 天(gc.reflogExpireUnreachable)。实验里 120 天前、100 天前的可达条目和 40 天前的不可达条目被删,20 天前的不可达条目保留。git gc 会执行这个 expire。
$ git reset -q --hard HEAD~2; git log --format=%s; git reflog --format='%gd: %gs' | head -4
提交 1
HEAD@{0}: reset: moving to HEAD~2
HEAD@{1}: commit: 提交 3
HEAD@{2}: commit: 提交 2
HEAD@{3}: commit (initial): 提交 1
$ git reset -q --hard HEAD@{1}; git log --format=%s
提交 3
提交 2
提交 1
$ git reflog expire --expire=now --all; git reflog | wc -l; git reset --hard HEAD@{1} 2>&1 | head -2
0
fatal: log for HEAD is emptyreset --hard 后用 HEAD@{1} 即可回到之前的位置;一旦手工 reflog expire --expire=now,就不能再这样用了,对象虽然还在,只能靠 git fsck 去找。