一个健康的版本库,是可阅读,可追溯的。对于每一次提交,我们需要有清晰的把握。 Git 的暂存区(index),就是我们的得力工具。暂存区,就像是超市里的购物车。 我们在货架(工作区)上挑选商品(文件的修改),并带到收银台结帐(提交)。 有了暂存区,我们可以对一大堆修改进行分门别类,先后有序的提交。

基本命令

   对暂存区的操作,主要使用三个命令 addresetcheckout。 它们的作用分别为:

  • add 将修改加入暂存区
  • reset 将修改从暂存区移出
  • checkout 放弃未加入暂存区的修改(从暂存区同步到工作区)    其中 ressetcheckout 都是多用途的命令,分别还有另一个用途。 reset 还可用于重置当前分支,checkout 还可用于重置 HEAD 并检出文件。 区别在于若命令后跟提交(hash)或指向提交的指针(分支、tag 等), 则是重置分支和 HEAD 的功能; 命令后跟文件,或没有参数,则是本文所说的用途。

精细操作 --patch

   addresetcheckout 三个命令都可以加 --patch 选项,简写为 -p。 遍历每一处修改进行精细处理。 后面可以跟文件名作为参数;也可以不加参数,表示对所有修改进行处理。 三个带 -p 的命令组合使用,我给它起了个俗名,叫“3P 大法”。 是目前处理 git 暂存区的最理想方案。

   执行命令将进入一个交互界面。大致如下:

diff --git a/index.md b/index.md
index 921ba51..1c0d98b 100644
--- a/index.md
+++ b/index.md
@@ -3,5 +3,5 @@
 
 
 layout: home
-list_title: foo
+list_title: bar
 ---
Stage this hunk [y,n,q,a,d,k,K,j,J,g,/,s,e,?]? 

最后一行的内容三个命令个有不同,分别为:

add
Stage this hunk [y,n,q,a,d,k,K,j,J,g,/,s,e,?]? 

reset
Unstage this hunk [y,n,q,a,d,k,K,j,J,g,/,s,e,?]? 

checkout
Discard this hunk from worktree [y,n,q,a,d,k,K,j,J,g,/,s,e,?]? 

说明了各自的用途。

   这里可见,git 展示了我们对 index.md 文件作出的一处修改。 并提供了 y,n,q,a,d,k,K,j,J,g,/,s,e,? 十一个可用的操作。分别表示什么呢? 别的不清楚,但 ? 想必是用来提供帮助。我们输入 ? Enter。 得到提示信息:

add
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
reset
y - unstage this hunk
n - do not unstage this hunk
q - quit; do not unstage this hunk or any of the remaining ones
a - unstage this hunk and all later hunks in the file
d - do not unstage 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
checkout
y - discard this hunk from worktree
n - do not discard this hunk from worktree
q - quit; do not discard this hunk or any of the remaining ones
a - discard this hunk and all later hunks in the file
d - do not discard 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

   初次看简直眼花缭乱,让我们整理一下:

处理命令:
`y` yes       要(加入/移出/丢弃);
`n` no        不要动;
`a` all       此文件的修改,全都要(加入/移出/丢弃);
`d` don't     此文件的修改,全都不要;
`e` edit      编辑,后面详细说明;
`s` split     分割,有时候 git 会把几个修改并在一起,若想分开处理,用此操作;

导航命令(只能在本文件中导航):
`g` goto      跳转到第 n 个修改,如 `g 1`,`g 2`,需要注意序号从 1 开始;
`/` search    搜索,和 vim、less 等软件的搜索差不多,在大文件中有大量修改的时候很方便;
`j` next      下一处没处理的修改;
`J` Next      下一处修改;
`k` previous  上一处没处理的修改;
`K` Previous  上一处修改;

其它命令:
`q` quit      退出;
`?` help      帮助;

   基本使用只需要掌握 ynsq,高级使用就再加上个 e。 程序员一般有 ynse 就够用了。 导航命令则对于字典维护人员,webpack 配置师这样的工种比较有用。

   下面我们详细讲一下这个 e。既然是 edit,自然需要 editor。 和不带 -m 选项的 commit 一样,它会调起编辑器,vim、nano 或是其它,依据你的配置。 内容如下:

# Manual hunk edit mode -- see bottom for a quick guide
@@ -20,4 +20,5 @@
 	><ul
 		><li>foo</li
-		><li>bar</li
+		><li>baz</li
+		><li>qux</li
 	></ul
# ---
# 这里会有一些操作说明,可以作为参考,我就不粘贴了。

   以上展示了一处修改,我们删除了 bar 行,并增加了 baz、qux 两行。 @@ ... @@ 中的内容展示了修改所在的行号和修改前后的行数。 紧接着是内容,在每行的行首第一个字符,表示行的类型,可以是 空格、-+。 空格表示普通行,此行没有修改;- 表示删除的行,而 + 则表示新加的行。 注意,此空格不是缩进。若你使用缩进符,则非常清晰;若使用空格符代替缩进符,则要仔细了,要多一个空格。

   此时,若保存退出编辑器,则表示应用(加入/移出/丢弃)此修改,相当于直接敲 y。 若我们将修改还原,即删除所有新加的行,即把删除的行取消删除,即将- 改为空格, 则等于不应用此修改,相当于直接敲 n。 若我们修改内容,则可以得到更多的效果。要注意不是所有修改都是允许的。

   在 add 时,情况比较容易理解。例如我们修改为如下的样子:

 	><ul
-		><li>foo</li
+		><li>qux</li
 		><li>bar</li
+		><li>Baz</li
 	></ul

表示我不删除 bar 行,而删除 foo 行。 baz 行的内容进行修改。而 qux 行则换个位置插入。 理解起来并不困难。 我们可以总结出规律:在 add 时,空格行和 - 行可以相互转换,但的内容不能修改; 而 + 行则可随意修改,删除,还可以在任意地方插入。

   而 reset 时,情况就和 add 相反了。空格行和 + 行可以相互转换,但的内容不能修改; 而 - 行则可随意修改,删除,还可以在任意地方插入。 这让人十分费解,还是用例子说明吧。还是上面的内容,我们作如下编辑:

 	><ul
+		><li>foo</li
-		><li>Bar</li
+		><li>baz</li
 		><li>qux</li
-		><li>ET</li
 	></ul

我们得以“放弃”,“取消”这样的字眼来分析。 我们将空格行 foo 改为 + 行,表示取消对 foo 行的添加。 可 foo 行并非添加的,而是原来就在,所以就是删除 foo 行啰。 删除的 bar 行改为 Bar 行,表示不放弃对 bar 行的删除,而取消对 Bar 行的删除。 那么 bar 行还是要删,然后添加 Bar 行。取消不存在的删除,等于添加。即 bar 行改为 Bar 行。 最后,放弃对 ET 行的删除,也等于新插入一个 ET 行。 有点绕脑。有一个囫囵吞枣的办法:reset 时,把 +-,把 -+ 就行了。

   checkout 时,由于方向和 reset 一致,因此允许的修改和效果,都和 reset 一致,是反的。 区别只是 reset 改的是暂存区,而 checkout 改的是文件内容。 因此,可以 checkout 来做做实验。躬行所悟,可比纸上所得要深刻百倍。

   需要注意,对于新文件,add -p 是无效的,它会跳过这些文件,而 reset -p 则没有这个问题。 对于新文件的 add,必须整个文件进行操作。

整个文件操作和全局操作

   通常,用 -p 浏览一遍到底提交了些什么总是好的。 但也会有需要整文件操作,甚至全局操作的时候。 只是勿图方便而滥用。

  • git add file 将文件中的全部修改加入暂存区
  • git reset file 将文件中的全部修改从暂存区移出
  • git checkout file 放弃文件中的全部未加入暂存区的修改

  • git add . 将文件中的全部修改加入暂存区
  • git reset 将暂存区中的全部修改移出(清空购物车)
  • git checkout 全部未加入暂存区的修改

未完善的终极武器 add --interactive

   git add 中,还有一个更加新潮的选项:--interactive,简写为 -i。 顾名思义,这也是一个交互界面。样子如下:

           staged     unstaged path
  1:    unchanged        +0/-1 TODO
  2:    unchanged        +1/-1 index.html

*** Commands ***
  1: status       2: update       3: revert       4: add untracked
  5: patch        6: diff         7: quit         8: help
What now> 

上方显示 status 内容,与 git status 得到得主体内容差不多,列表得方式看起来更加清晰。 下方显示可用的操作,一共有 8 个,此外还有个隐藏的 ?,分别为:

1 status         显示主界面,通常用于刷新
2 update         加入暂存区,相当于 add file
3 revert         移出暂存区,相当于 reset file
4 add untracked  新文件加入暂存区
5 patch          相当于 add -p file
6 diff           查看暂存区中的内容
7 quit           退出
8 help           帮助,关于以上操作
?                帮助,关于交互界面

实际使用时,敲数字或首字母即可,无需键入完整命令。

   其中 u r a p d 为核心操作,执行后,会进入到二级界面,我们看到命令提示符由 > 变为 >>。 这时候,我们可以选取要操作的文件。可用序号选择,也可输入文件名(前面一段,没有重复即可)。 目前,文件名还不支持分段匹配、模糊匹配、* ?,也不支持 tab 键输入提示,和上下键输入历史,希望将来能有。 但序号也够用了。在所选的序号或文件名前加 - 则表示取消选择。第一次回车,执行选择,然后可以继续选择。 第二次回车,执行操作。

   add --interactive 给了我们一个很棒的操作方式,我们可流畅地操作,不必一而再再而三地执行 git status。 就操作方式而言,可以说是终极武器。然而这个未来的终极武器还有需多不够完善之处。 它有 add filereset file 以及 add -p file 却缺少了 reset -p filecheckout filecheckout -p file。 可以说只整合了一半的功能。最关键的 add -p filereset -p file,就少了一个。 我们只能先 reset fileadd -p file。若文件中有很多修改,这会相当麻烦。 因此,这个命令还需要观望。目前最佳方案,还是三个 patch,俗称 3P 大法。