在使用git的过程中,难免会用到git diff命令,用于比较文件差异。但初学者对这个命令的输出格式几乎都是一脸懵逼,需仔细研究一番。

我读过阮一峰的《读懂diff》,收获颇大,但还是写了本文。一来,阮一峰文章中的举例过于简单和特殊,有些问题没有解释清楚;二来,也是自己的一份总结。

背景

git的diff,源于Unix的diff命;因此,追本溯源我们要从Unix的diff命令说起。

Unix的diff命令由于历史原因,又分为三种输出格式:

  • 常规格式(normal diff)
  • 上下文格式(context diff)
  • 合并格式(unified diff)

本文是系列的第一篇,介绍diff常规输出格式

diff命令的格式

diff用于比较两个文件的差异,如果f1文件看做原文件,f2文件看做改动后的文件,那么直接执行diff f1 f2,就完成了常规模式下对f1和f2的比较。

在开始之前,我们先要意识到:diff对文本,==是按行进行比较==的工具,所以你看到的输出,永远是针对行的描述。

准备初始文件f0

为了研究diff命令的使用方法,我们准备了丰富的案例讲解。每个案例对应与一个改动文件,都与原始文件f0进行diff,便于大家学习。

首先,我们的初始文件f0的内容如下,一共14行:

11
22
33
44
55
66
77
88
99
00
aa
bb
cc
dd

为了便于识别,初始文件每行统一为2个字符;同时,任何改动的行,都会超过两个字符;因此,肉眼会很容易的辨别出改动点。

下面,就开始我们的案例之旅吧!

案例0:文件相同

我们让f0和f0自己比较,显然不会有任何差异行。

  • 命令:diff f0 f0
  • 结果:不出所料,diff秉承了Unix的设计哲学:没消息就是好消息。因此没有给出任何输出。

案例c1:修改一行内容

将第5行修改为hello,形成文件c1:

11
22
33
44
hello
66
77
88
99
00
aa
bb
cc
dd

执行命令:diff f0 c1,输出如下:

5c5
< 55
---
> hello

上述输出内容分内4行:

  1. 第1行是一个==变动提示==,5c5共3个字符,第一个5表示变动的行号;第二个c表示变动方式是修改(change),其他的变动方式还有增加a(add)、删除d(delete);第三个5表示变动后在新文件的行号,由于是修改因此,修改前后行号一样。
  2. 第2行代表“删除操作”,<代表删除,后面紧跟着删除的内容55;也就是说删除了55。
  3. 第3行是三个横线,用于分隔删除和增加的内容。
  4. 第4行是增加的内容,用>表示,>后就是具体增加的内容。

初步总结一下diff输出:

  1. diff先用一个字符串表示==变动提示==,包含:操作的行在旧文件的行号,操作的方式,以新文件的行号。
  2. 操作被分解为“删除”和“新增”两步,分别用<>表示,其后跟上的是具体内容。
  3. 删除和新增两步之间,用---分隔。

案例c2:修改两行内容(相邻)

案例c1只修改了一行,如果我们再多修改一行,会怎样?我们继续把第6行修改为world,形成文件c2:

11
22
33
44
hello
world
77
88
99
00
aa
bb
cc
dd

执行diff f0 c2

5,6c5,6
< 55
< 66
---
> hello
> world

相比案例c1的输出,稍显复杂了:

  1. 变动提示里,c的两边的行号变成了5,6,代表是5到6行。
  2. ---的上下两部分也变成了两行内容。这容易理解,因为删除了两行内容,并新增了两行内容。

总结一下,在连续行修改的情况下:

  1. 第一行操作提示,使用起始行号进行表示范围。
  2. ---上下,用多个<>,标记连续删除和新增的内容。

案例c3:修改两行内容(不相邻)

若干修改的行不相邻,diff会怎么表示?我们把f0的第5行改为hello,第10行为world,修改后形成文件c3:

11
22
33
44
hello
66
77
88
99
world
aa
bb
cc
dd

执行diff f0 c3

5c5
< 55
---
> hello
10c10
< 00
---
> world

结果好像更复杂了,但在案例c1和案例c2的基础,仔细看:拆分看来,其实就是两个案例c1的操作而已,分别以5c510c10开头。

总结:

  1. 当修改内容不连续的时候,diff将修改==拆分为多个片段表示==,每个片段都是一个==完整的连续修改片段==。
  2. 片段的开始符号是“修改提示”,即“原文件行号(或范围)” + 变动方式 + “新文件行号(或范围)”

案例c4:综合情况

将案例c1-c3的情况综合考虑,形成如下的文件c4:

11
22
33
hello
world
!!!!
77
88
99
00
how are you
bb
cc
fine

文件修改了3处(连续的算作一处):4-6行被修改了;11行变成了how are you;14行变成了fine;初步预测一下,diff应该会给出3个修改片段,其中第1个片段是一个连续的范围。

执行命令diff f0 c4,结果不出所料:

4,6c4,6
< 44
< 55
< 66
---
> hello
> world
> !!!!
11c11
< aa
---
> how are you
14c14
< dd
---
> fine

以上都是原行修改的案例,下面我们看一下增加行的案例。

案例a1:增加一行内容

与修改同理,我们从增加一行开始(在第5行下增加一行hello,变成了新文件的第6行),形成文件a1:

11
22
33
44
55
hello
66
77
88
99
00
aa
bb
cc
dd

执行diff f0 a1

5a6
> hello

嚯,内容要比修改的情况简洁多了,但注意两点:

  1. “变动提示”里,除操作方式用a表示增加(add)外;特别注意第3个6表示新文件的行号。由于是新增行,那么在新行自然是偏移到第6行了
  2. 原来出现的<---都不见了,只剩下表示新增内容的>。由于只存在增加内容,无需分隔,---被去除了,可以理解。

总结:

  1. 变动提示5a6,表示在第5行后面新增,并形成新文件的第6行。
  2. 新增操作不用分解,只保留了>行的内容

案例a2:增加两行内容(连续):

文件在第5行后,增加了两行内容,如下:

11
22
33
44
55
hello
world
66
77
88
99
00
aa
bb
cc
dd

执行diff f0 a2,参考案例c2的经验,不出所料,结果如下,就不多解释了:

5a6,7
> hello
> world

案例a3:增加两行内容(不相邻)

新增后的文件a3如下:

11
22
33
44
55
hello
66
77
88
99
00
world
aa
bb
cc
dd

执行diff f0 a3,结果如下:

5a6
> hello
10a12
> world

不连续的时候,diff给出了两个新增片段,符合预期。

但注意第二个片段a后是12,而不是11,因为第一个新增也影响了新文件,所以到第二个新增操作的时候,其实已经偏移了2行。也就是说,变动提示中,新文件的行号受前面所有修改的影响的。

案例a4:综合情况

多修改几处,形成a4:

nihao
11
22
33
44
55
hello
world
66
77
88
99
00
how are you
aa
i am fine
bb
cc
dd

可以看出,首先在开头插入一行nihao,然后再55后连续增加了两行,接着分别不连续的新增了1行,执行diff f0 a4,分为4个片段,结果如下:

0a1
> nihao
5a7,8
> hello
> world
10a14
> how are you
11a16
> i am fine

值得关注的是,在文件头增加的时候,操作提示中用0表示原文件的位置。

案例d1:删除一行

我们把f0文件的第5行删除,形成文件d1:

11
22
33
44
66
77
88
99
00
aa
bb
cc
dd

执行diff f0 d1,结果如下:

5d4
< 55
  1. 和新增类似,这里只有<行,没有--->行。
  2. 变动提示中,==新文件的4表示删除内容近邻的上一行在新文件的位置==。

案例d2:删除两行(连续)

f0删除5,6两行后,形成文件d2:

11
22
33
44
77
88
99
00
aa
bb
cc
dd

执行diff f0 d2,结果如下:

5,6d4
< 55
< 66

同理,不难理解。

案例d3:删除两行(不连续)

f0删除5和10两行后,形成d3:

11
22
33
44
66
77
88
99
aa
bb
cc
dd

执行diff f0 d2,结果如下:

5d4
< 55
10d8
< 00

形成了两个删除片段。

案例d4:综合

删除第1行,删除第5,6行,删除第12行,形成文件d4:

22
33
44
77
88
99
00
aa
cc
dd

执行diff f0 d4

1d0
< 11
5,6d3
< 55
< 66
12d8
< bb

有三个删除片段,结合前边的例子,不难理解。

综合案例acd:增删改

有了上面的基础,我们模拟一下复杂的情况,包含了增删改的混合。与之前不同,我先给出diff的操作结果,看看你能不能反向推测出修改后的文件acd呢?

执行diff f0 acd结果如下:

0a1,2
> today is sunday
> i went to china
2d3
< 22
3a5
> yes it is
6,8c8
< 66
< 77
< 88
---
> hello
11c11,13
< aa
---
> first
> secend
> third

看起来是有点晕,最好准备一个编辑器,自己模拟一下。先将f0的内容拷贝到编辑器内,跟着操作:

  • 0a1,2,说明在文件头增加两行,修改后如下(==为了方便,我在每行前显示了行号==):
    1 today is sunday
    2 i went to china
    3 11
    4 22
    5 33
    6 44
    7 55
    8 66
    9 77
     10 88
     11 99
     12 00
     13 aa
     14 bb
     15 cc
     16 dd
    
  • 2d3,即将第2行删掉,注意是原文件的第2行(根据<后看,也可以确认就是内容22的行):
    1 today is sunday
    2 i went to china
    3 11
    4 33
    5 44
    6 55
    7 66
    8 77
    9 88
     10 99
     11 00
     12 aa
     13 bb
     14 cc
     15 dd
    
  • 3a5,第3行增加一行,形成新文件的第5行,内容是yes it is:
      1 today is sunday
      2 i went to china
      3 11
      4 33
      5 yes it is
      6 44
      7 55
      8 66
      9 77
     10 88
     11 99
     12 00
     13 aa
     14 bb
     15 cc
     16 dd
    
  • 6,8c8,这个和之前的有些区别,修改操作,但修改了原文件三行,新文件只有一行。不过只要理解修改就是删除+新增,变不难了。其实,就是将原文件的第6-8行删除,在替换为hello即可,于是形成:
    1 today is sunday
    2 i went to china
    3 11
    4 33
    5 yes it is
    6 44
    7 55
    8 hello
    9 99
     10 00
     11 aa
     12 bb
     13 cc
     14 dd
    
  • 最后一个操作:11c11,13,和上一个操作类似,修改后的行比修改前多。分析一下不过是删除了原文件11行,并在原位置插入了3行内容。最终修改成的文件就是:
    1 today is sunday
    2 i went to china
    3 11
    4 33
    5 yes it is
    6 44
    7 55
    8 hello
    9 99
     10 00
     11 first
     12 secend
     13 third
     14 bb
     15 cc
     16 dd
    

    到此,diff的常规模式(normal)的输出情况,通过几个栗子几乎覆盖了。尤其是最后一个例子,反推回去如果能搞懂,就没什么问题了。

总结

最后,对diff命令的常规模式总结如下:

  1. diff可分辨变动的粒度是==一行==。
  2. 任何变动,在diff看来,都可以分解为删除行和增加行。
  3. 对与连续的变动,diff会用一个==变动片段==表示。其中变动片段分为4个部分: a. 第一部分,即第一行,是==变动提示==,用“原文件行号(或范围)” + 变动模式 + “新文件行号(或范围)”表示,比如3c3。其中变动模式,分为a(增加)、c(修改)和d(删除)。 b. 第二部分和第四部分用---(即第三部分)分隔。 c. 第二部分表示删除的内容,每一行用<开头,紧随的是具体删除的内容。 d. 第四部分表示增加的内容,每一行用>开头,紧随的是具体增加的内容。 e. 当变动模式是a和d的时候,第二和第部分,只存在一个,同时---也会被省略。
  4. 如果一个变动不连续,则会被拆解为多个变动片段表示。每个片段,都遵循第3条同样的规则。

-------------------------

本文采用 知识共享署名 4.0 国际许可协议(CC-BY 4.0)进行许可。转载请注明来源:https://imshuai.com/unix-diff-examples-series1-normal 欢迎指正或在下方评论。