bigywater.tech
Git · 70

notes

git notes 让你给已有的提交加说明而不改变提交的 hash。它存在哪里、怎么显示、改写历史之后会怎样、为什么 clone 不带笔记?这篇逐项实测,并看笔记很多时 tree 怎么扇出。

hash 不变
加笔记后提交本身的 hash 不变,笔记存在 refs/notes/commits
50 → 100
笔记数从 50 增加到 100 时,tree 从“文件名=hash”变成“两位十六进制目录扇出”
refs/notes/*
push 与 fetch 默认不包含的命名空间,要显式 refspec

不改变提交

实测 · 本机执行输出
  $ git notes add -m "这次提交通过了人工评审,评审人:小王" HEAD
  HEAD 的 hash 没变: True
  $ git log -2 --format='%h %s%n%N' | sed -E 's/^[0-9a-f]{7} /<sha7> /'
  <sha7> 提交二
  这次提交通过了人工评审,评审人:小王
  
  <sha7> 提交一
  $ git for-each-ref --format='%(refname)' refs/notes; git notes list | sed -E 's/[0-9a-f]{40}/<sha40>/g; s/\b[0-9a-f]{7}\b/<sha7>/g'
  refs/notes/commits
  <sha40> <sha40>

给提交加笔记后提交本身的 hash 不变,git log 在提交后面多显示一段 Notes:(%N 占位符)。笔记存在另一个 ref refs/notes/commits 里。

笔记的存储

实测 · 本机执行输出
  $ git cat-file -p refs/notes/commits^{tree} | sed -E 's/[0-9a-f]{40}/<sha40>/g; s/\b[0-9a-f]{7}\b/<sha7>/g'; echo "--- 文件名就是被注解的提交的完整 hash;内容:"; git cat-file -p refs/notes/commits:$(git rev-parse HEAD)
  100644 blob <sha40>	<sha40>
  --- 文件名就是被注解的提交的完整 hash;内容:
  这次提交通过了人工评审,评审人:小王
  文件名等于 HEAD 的完整 hash: True
  $ git log --format="%s" refs/notes/commits
  Notes added by 'git notes add'
  再改一次(append / 编辑)就是在这个“分支”上再加一个提交:
  $ git notes append -m "补充:已合并到主线" HEAD; git log --format="%s" refs/notes/commits | tr "\n" "|"; echo; git notes show HEAD
  Notes added by 'git notes append'|Notes added by 'git notes add'|
  这次提交通过了人工评审,评审人:小王
  
  补充:已合并到主线

refs/notes/commits 是一个普通分支:它的 tree 把“被注解的提交的完整 hash”当文件名,笔记内容是文件内容;每次修改在这个分支上加一个提交,所以笔记本身也有历史。

显示

实测 · 本机执行输出
  $ git notes --ref=build add -m "构建 #1234 成功" HEAD; git for-each-ref --format="%(refname)" refs/notes | tr "\n" " "; echo
  refs/notes/build refs/notes/commits
  $ git log -1 --format='%s%n%N' | head -4; echo '--- 加上 --notes=build:'; git log -1 --notes=build --format='%s' ; git log -1 --notes=build | tail -3
  提交二
  这次提交通过了人工评审,评审人:小王
  
  补充:已合并到主线
  --- 加上 --notes=build:
  提交二
  
  Notes (build):
      构建 #1234 成功
  $ git config notes.displayRef "refs/notes/*"; echo "--- 设置 notes.displayRef=refs/notes/* 之后:"; git log -1 | tail -5
  --- 设置 notes.displayRef=refs/notes/* 之后:
  
      补充:已合并到主线
  
  Notes (build):
      构建 #1234 成功

git log 默认显示 refs/notes/commits 的笔记(%N 占位符);用 --ref 可以建别的命名空间,--notes=<名> 或 notes.displayRef 控制显示哪些。

改写历史

实测 · 本机执行输出
  $ git log -1 --format="%s  笔记: [%N]"
  T1  笔记: []
  笔记仍然挂在旧提交上: 1 条笔记,对应的提交现在已不在 topic 分支上
  $ git log -1 --format="%s  笔记: [%N]"; git notes list | wc -l | sed "s/^ */现在 notes 里的条数: /"
  T1  笔记: [T1 的笔记
  ]
  现在 notes 里的条数: 2

笔记按提交 hash 挂着,rebase / amend 得到新 hash 后默认看不到旧笔记;设置 notes.rewriteRef 才会在改写时把笔记复制到新提交。

扇出

实测 · 本机执行输出
    10 条笔记:顶层 tree 有  10 个条目,类型:hash 作文件名;第一个条目:2cc2efbbe309
    50 条笔记:顶层 tree 有  50 个条目,类型:hash 作文件名;第一个条目:0b92f10531fd
   100 条笔记:顶层 tree 有  84 个条目,类型:两位十六进制的目录(扇出);第一个条目:06
   150 条笔记:顶层 tree 有 116 个条目,类型:两位十六进制的目录(扇出);第一个条目:02
   200 条笔记:顶层 tree 有 145 个条目,类型:两位十六进制的目录(扇出);第一个条目:02
   300 条笔记:顶层 tree 有 181 个条目,类型:两位十六进制的目录(扇出);第一个条目:01

笔记不多时 tree 条目就是完整 hash;笔记增多(本例 50 到 100 条之间)git 自动改用两位十六进制的子目录来扇出,和 .git/objects 的做法相同。

同步

实测 · 本机执行输出
  $ git ls-remote /work/l66remote.git | sed -E "s/^[0-9a-f]{40}/<sha40>/"
  <sha40>	HEAD
  <sha40>	refs/heads/main
  $ git notes list | wc -l | sed "s/^ */clone 之后本地的笔记数: /"
  clone 之后本地的笔记数: 0
  $ git fetch -q origin "refs/notes/*:refs/notes/*"; git notes list | wc -l | sed "s#^ *#显式 fetch 之后本地的笔记数: #"; git log -1 --format="%s: %N" | head -2
  显式 fetch 之后本地的笔记数: 1
  提交二: 这次提交通过了人工评审,评审人:小王

refs/notes/* 不在默认的 fetch / push refspec 里,clone 也不带笔记。要同步需要显式地推送、抓取 refs/notes/*:refs/notes/*。

分步演示

git notes
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / git 2.54.0(Apple Git-157);每个实验在临时目录里新建仓库,HOME 和全局配置指向实验专用的空目录(不读用户自己的 git 配置),提交的作者与时间固定以便复现;需要网络协议的实验用本机的 file:// 或 git daemon 回环端口(9800 以上),不碰任何远程仓库;计时用 time.perf_counter,数字每次运行会有小幅抖动。“扇出临界点”只测了 10、50、100、150、200、300 几个点,精确的阈值没有确定;笔记的批量添加用 for 循环调用 git notes add;远程是本机的裸仓库
    • 没有验证:git notes merge 解决不同人对同一提交的笔记冲突;notes.rewriteMode(overwrite / concatenate / cat_sort_uniq / ignore);git notes prune;GitHub 等托管平台对 refs/notes 的支持;笔记很多时对 git log 性能的影响
    京ICP备2026067502号-1京公网安备11010502063487号