- Published on
Git tips -- 同一个文件修改了多处如何分作多个提交
- Authors
- Name
- ttyS3
需求
经常会有这么一种情况,一个文件修改了很多次代码,才发现 -- 咦?忘记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.c
第 1,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