需求

经常会有这么一种情况,一个文件修改了很多次代码,才发现 – 咦?忘记commit了。 而且往往这些修改可能它们本来应该属于不同的提交。

怎么办?总不可能将就一下,直接把一些乱七八糟的修改放在一个commit里吧?

这个时候git add的-p, --patch参数就派上大用场了。

介绍

-p, --patch Interactively choose hunks of patch between the index and the work tree and add them to the index. This gives the user a chance to review the difference before adding modified contents to the index. This effectively runs add –interactive, but bypasses the initial command menu and directly jumps to the patch subcommand. See “Interactive mode” for details.

hunks 是一个GNU diff用语, 见 http://www.gnu.org/software/diffutils/manual/html_node/Hunks.html

When comparing two files, diff finds sequences of lines common to both files, interspersed with groups of differing lines called hunks

因此,简单来说,hunks 就是指两个文件对比时的一组一组的差异行。

当我们进行git add时,默认是把整个文件添加进去了。当按hunks选取时,我们便可以自由地选取任意的hunks组成一个commit.

实战

举个最简单的例子,假设 hello.c 原内容如下:

#include <stdio.h>

typedef void (*greet_func)();

void sayHello();

int main() {
  greet_func greet = sayHello;
  greet();
  return 0;
}

void sayHello() {
  printf("Hello from ttys3.net\n");
}

为了完成后续的操作,你需要新建一git仓库,然后添加如上文件,并完成初次提交。

然后,我们将文件修改,假设它变成了如下内容(此处这些改动并无实际意义,只是为了方便演示做的修改):

hello.c1,3,5,7,9 行都被修改了,如果我想把1, 9(都标了//1)组成一个commit,把3,5,7 (都标了//2) 组成另一个commit, 要如何操作?

#include <stdio.h> //1

typedef void (*greet_func)(); //2

void sayHello(); //2

int main() { //2
  greet_func greet = sayHello;
  greet(); //1
  return 0;
}

void sayHello() {
  printf("Hello from ttys3.net\n");
}

当前状态:已修改,未stage, 用git diff可以看到如下结果:

diff --git a/hello.c b/hello.c
index 0968d02..bf5082a 100644
--- a/hello.c
+++ b/hello.c
@@ -1,12 +1,12 @@
-#include <stdio.h>
+#include <stdio.h> //1
 
-typedef void (*greet_func)();
+typedef void (*greet_func)(); //2
 
-void sayHello();
+void sayHello(); //2
 
-int main() {
+int main() { //2
   greet_func greet = sayHello;
-  greet();
+  greet(); //1
   return 0;
 }

我们执行git add -p hello.c:

由于我们这个修改实在是太小了,直接被Git识别成一个hunk了,怎么办?

这个(1/1) Stage this hunk [y,n,q,a,d,s,e,?]? 提示是什么意思?

执行git add --help然后跳到INTERACTIVE MODE 下的 patch部分,有详细的解释

patch
           This lets you choose one path out of a status like selection. After choosing the path, it presents the diff between the index and the working tree file and asks you if you want to stage the change of each hunk. You can
           select one of the following options and type return:

               y - stage this hunk
               n - do not stage this hunk
               q - quit; do not stage this hunk or any of the remaining ones
               a - stage this hunk and all later hunks in the file
               d - do not stage this hunk or any of the later hunks in the file
               g - select a hunk to go to
               / - search for a hunk matching the given regex
               j - leave this hunk undecided, see next undecided hunk
               J - leave this hunk undecided, see next hunk
               k - leave this hunk undecided, see previous undecided hunk
               K - leave this hunk undecided, see previous hunk
               s - split the current hunk into smaller hunks
               e - manually edit the current hunk
               ? - print help

           After deciding the fate for all hunks, if there is any hunk that was chosen, the index is updated with the selected hunks.

           You can omit having to type return here, by setting the configuration variable interactive.singleKey to true.

默认是按下上述键后还需要按下回车确认的,如果想要直接单键确认,可以修改配置 interactive.singleKey = true

老灯简单解释下:

(所有的操作都是针对hunk的)

    y - 取了
    n - 不取
    q - 我不干了,啥也别add,退出吧
    a - 取了这个和此文件后续所有的
    d - 这个不取了,此文件后续所有的我也不取了
    g - 搜索以跳到某个hunk
    / - 以正则搜索某个hunk
    j - 这个未决, 并跳到下一个未决hunk
    J - 这个未决, 并跳到下一个hunk
    k - 这个未决, 并跳到上一个未决hunk
    K - 这个未决, 并跳到上一个hunk
    s - 这个hunk太大了,拆分成更小的hunks吧
    e - 手动编辑当前hunk
    ? - 显示当前帮助信息(当你不记得这些缩写是什么意思时相当有用)

好了,回到我们之前的操作,很明显,我们是要按s将这个hunk继续拆分。

OK,这次被拆分成5个hunks了, 我们先完成第一个commit:

# 按s再回车确认(默认配置都要回车的,后续省略不写了), 拆成更小的hunks
(1/1) Stage this hunk [y,n,q,a,d,s,e,?]? s
Split into 5 hunks.
@@ -1,2 +1,2 @@
-#include <stdio.h>
+#include <stdio.h> //1

# 这个是我们要取的,按y
(1/5) Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]? y
@@ -2,3 +2,3 @@
 
-typedef void (*greet_func)();
+typedef void (*greet_func)(); //2

# 这个hunk不是我们第一个commit里需要的,按n
(2/5) Stage this hunk [y,n,q,a,d,K,j,J,g,/,e,?]? n
@@ -4,3 +4,3 @@
 
-void sayHello();
+void sayHello(); //2

# 这个hunk不是我们第一个commit里需要的,按n
(3/5) Stage this hunk [y,n,q,a,d,K,j,J,g,/,e,?]? n
@@ -6,3 +6,3 @@
 
-int main() {
+int main() { //2
   greet_func greet = sayHello;
# 这个hunk不是我们第一个commit里需要的,按n
(4/5) Stage this hunk [y,n,q,a,d,K,j,J,g,/,e,?]? n
@@ -8,5 +8,5 @@
   greet_func greet = sayHello;
-  greet();
+  greet(); //1
   return 0;
 }

# 这个是我们要取的,按y
(5/5) Stage this hunk [y,n,q,a,d,K,g,/,e,?]? y

我们git status看看,现在是这样的:

git diff看看,发现剩下的都是我们第二个commit要取的了:

好了,现在我们先git commit把刚才取的这些hunks先commit:

❯ git ci -m 'commit 1'
[master 3f9a5ed] commit 1
 1 file changed, 2 insertions(+), 2 deletions(-)

由于剩下的是另一个提交的,因此就可以直接git add hello.c再commit了:

git add hello.c
❯ git ci -m 'commit 2'
[master ae734d3] commit 2
 1 file changed, 3 insertions(+), 3 deletions(-)

好了,我们验证一下。

先看一下第一个提交的改动 git show @^

再看一下第二个提交的改动git show @

参考文档

man 1 git-add or git add --help

https://git-scm.com/book/en/v2/Git-Tools-Interactive-Staging