bigywater.tech
Git · 29

reflog:记录什么、什么时候过期

reflog 到底记录了什么、记多久?这篇直接读 .git/logs 下的文件,看哪些操作会写、哪些 ref 有,再造出 120 天到 5 天前的各种条目,验证默认的过期规则,最后演示用它撤销 reset --hard。

90 / 30 天
git reflog expire 的默认保留期:可达的提交条目 90 天,不可达的 30 天
3 条
造出的 7 条 HEAD reflog 条目在默认 expire 之后剩下的条数
0
git reflog expire --expire=now --all 之后 git reflog 的行数,HEAD@{n} 全部失效

文件格式

实测 · 本机执行输出
  $ 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~1

reflog 就是 .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 第一条记录的时间>
  提交 1

HEAD@{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 等等,说明里写了是哪种操作,方便在找回时辨认。

谁有 reflog

实测 · 本机执行输出
  $ 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 empty

reset --hard 后用 HEAD@{1} 即可回到之前的位置;一旦手工 reflog expire --expire=now,就不能再这样用了,对象虽然还在,只能靠 git fsck 去找。

分步演示

reflog
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / git 2.54.0(Apple Git-157);每个实验在临时目录里新建仓库,HOME 和全局配置指向实验专用的空目录(不读用户自己的 git 配置),提交的作者与时间固定以便复现;需要网络协议的实验用本机的 file:// 或 git daemon 回环端口(9800 以上),不碰任何远程仓库;计时用 time.perf_counter,数字每次运行会有小幅抖动。这一篇的提交使用真实时间;“120 天前、100 天前……”是用 GIT_COMMITTER_DATE 把 reflog 条目的时间写成过去的时间构造的,没有真的等待;相对时间(5 days ago、3 weeks ago)随运行日期变化,取值范围在本文中保持稳定
    • 没有验证:gc.reflogExpire 与 gc.reflogExpireUnreachable 改成其它值;git reflog expire 的 --stale-fix、--rewrite、--updateref;worktree 各自的 reflog;reflog 本身占的空间;core.logAllRefUpdates=false 的行为
    京ICP备2026067502号-1京公网安备11010502063487号