bigywater.tech
Git · 38

git add -p:hunk 是怎么切分与重组的

git add -p 是怎么把一个文件切成一块一块让你选的?hunk 的边界怎么定、s 什么时候能拆、e 编辑之后行数怎么重算、最后暂存区里到底是什么?这篇用脚本喂答案跑真实的 git add -p,一项项对账。

3 个 hunk
4 处改动被切成 3 个 hunk,相隔一行的两处合并在一起
相同
add -p 选中第 2 个 hunk 暂存的 blob,与手工用 git apply --cached 应用同一个 hunk 得到的 blob 完全一致
@@ 头
手工编辑 hunk 之后,git 自动重算 @@ 头里的行数

hunk 怎么切

实测 · 本机执行输出
  $ git diff | grep -E '^@@'
  @@ -1,8 +1,8 @@
  @@ -17,7 +17,7 @@
  @@ -35,6 +35,6 @@
  第 3 行与第 5 行相隔一行,各带 3 行上下文,上下文重叠,于是合成了一个 hunk

git diff 把连续的改动连同上下文(默认 3 行)切成 hunk,两处改动的上下文重叠就合并成一个。所以 4 处改动只切出 3 个 hunk。

y / n

实测 · 本机执行输出
  答案 n、y、y(第一个 hunk 不要,后两个要);add -p 每个 hunk 问一次(提示里的 s 只在可拆分时才出现):
  $ printf 'n\ny\ny\n' | git add -p f.txt 2>&1 | grep -E '^(@@|\()' | sed -E 's/^(\([0-9]\/[0-9]\) Stage this hunk \[[^]]*\]\?) .*/\1/'
  @@ -1,8 +1,8 @@
  (1/3) Stage this hunk [y,n,q,a,d,k,K,j,J,g,/,s,e,p,P,?]?
  (2/3) Stage this hunk [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]?
  (3/3) Stage this hunk [y,n,q,a,d,K,J,g,/,e,p,P,?]?
  暂存区里(git diff --cached)和工作区里剩下的(git diff):
  $ git diff --cached | grep -E '^(@@|[+-][^+-])'
  @@ -17,7 +17,7 @@
  -行 20
  +行 20 改了
  @@ -35,6 +35,6 @@
  -行 38
  +行 38 改了
  $ git diff | grep -E '^(@@|[+-][^+-])'
  @@ -1,8 +1,8 @@
  -行 03
  +行 03 改了
  -行 05
  +行 05 改了

git add -p 对每个 hunk 问一次:y 暂存、n 跳过。暂存区和工作区于是按 hunk 为单位分开,git diff --cached 与 git diff 各看到一部分。

s 拆分

实测 · 本机执行输出
  对第一个 hunk 输入 s:git 回答 Split into 2 hunks.(第 3 行与第 5 行的改动被分开),再对拆出的第一块输 y、第二块输 n、然后 q:
    ,?]? Split into 2 hunks.
  $ git diff --cached | grep -E '^(@@|[+-][^+-])'
  @@ -1,6 +1,6 @@
  -行 03
  +行 03 改了

一个 hunk 里有多处改动、且它们之间隔着未改动的行时,可以用 s 拆成更小的 hunk,分别决定取舍。只有一处改动的 hunk 没有 s 选项。

e 编辑

实测 · 本机执行输出
  $ git diff --cached | grep -E '^(@@|[+-][^+-])'
  @@ -1,8 +1,7 @@
  -行 03
  +行 03 我手工改的
  -行 05
  $ git show :f.txt | sed -n '3p;5p'; echo '--- 工作区:'; sed -n '3p;5p' f.txt
  行 03 我手工改的
  行 06
  --- 工作区:
  行 03 改了
  行 05 改了

e 把这个 hunk 当补丁交给编辑器。改 + 行就是改要暂存的内容;删掉一个 + 行相当于“只暂存对应的删除”;保存后 git 重算 @@ 头里的行数。工作区文件不受影响。

本质

实测 · 本机执行输出
  add -p(n、y、n)暂存的 blob: a5d5288f9632   手工只取第 2 个 hunk 的补丁 git apply --cached: a5d5288f9632   相同: True
  工作区文件没动: True

暂存的内容 = HEAD 版本加上被选中的 hunk,和 git apply --cached 一个补丁得到的 blob 完全相同。

边界

实测 · 本机执行输出
  $ printf "y\n" | git add -p 2>&1 | head -2; git status --short
  No changes.
  ?? new.txt
  $ git add -N new.txt; git status --short; printf "y\n" | git add -p new.txt 2>&1 | grep -E "^(@@|\(1/1\))" | head -2; git diff --cached --stat | tail -1
   A new.txt
  @@ -0,0 +1,6 @@
  (1/1) Stage addition [y,n,q,a,d,e,p,P,?]?
   1 file changed, 6 insertions(+)
  $ printf 'n\ny\nn\n' | git reset -p f.txt >/dev/null 2>&1; git diff --cached | grep -E '^(@@|[+-][^+-])'
  @@ -1,8 +1,8 @@
  -行 03
  +行 03 改了
  -行 05
  +行 05 改了
  @@ -35,6 +35,6 @@
  -行 38
  +行 38 改了

untracked 文件没有 hunk,要先 git add -N(intent-to-add);reset -p、checkout -p、stash -p 用同一套选择机制,方向相反。

分步演示

git add -p
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / git 2.54.0(Apple Git-157);每个实验在临时目录里新建仓库,HOME 和全局配置指向实验专用的空目录(不读用户自己的 git 配置),提交的作者与时间固定以便复现;需要网络协议的实验用本机的 file:// 或 git daemon 回环端口(9800 以上),不碰任何远程仓库;计时用 time.perf_counter,数字每次运行会有小幅抖动。git add -p 不是在终端里,而是用 printf 通过管道喂答案(非 tty 输入);e 的编辑器是我写的一个小 Python 脚本(GIT_EDITOR),机械地改补丁;hunk 的行号来自 git diff 的输出
    • 没有验证:真终端里 interactive.singleKey 的单键模式;g(跳转)、/(搜索)、a/d/k/j 等其它选项;二进制文件与模式变更(chmod)在 add -p 里的处理;diff.context 与 --inter-hunk-context 调整 hunk 边界;add -p 的 patch 模式(git add -i)
    京ICP备2026067502号-1京公网安备11010502063487号