每一个通用的版本控制系统,无论是CVS、Subversion、Git或是其他,都要面对换行符转换的问题。这是因为作为通用的版本控制系统要面对来自不同操作系统的文件,而不同的操作系统在处理文本文件时,可能使用不同的换行符。
不同的操作系统可能使用不同的换行符
文本文件的每一行结尾用一个或者两个特殊的ASCII字符进行标识,这个标识就是换行符。主要的换行符有三种:LF(Line Feed即换行,C语言等用“\\n”表示,相当于十六进制的0x0A),CR(Carriage Return 即回车,C语言等用“\\r”表示,相当于十六进制的0x0D)和CRLF(即由两个字符“CR+LF”组成,即“\\r\\n”,相当于十六进制的0x0D 0x0A),分别用在不同的操作系统中。(以下内容摘自http://en.wikipedia.org/wiki/Newline。)
实际上,自从苹果的Mac OS从第10版转向Unix内核开始,依据不同的文本文件换行符,主流的操作系统可以划分为两大阵营,一个是微软Windows作为一方,使用CRLF作为换行符,另外一方包括Unix、类Unix(如Linux和Mac OS X等)使用LF作为换行符。分属不同阵营的操作系统之间交换文本文件会因为换行符的不同造成障碍,而对于使用版本控制系统,也同样会遇到换行符的麻烦。
文本文件和二进制文件的判别,是换行符转换的基础
几乎所有的版本库控制系统都采用这样的解决方案:对于文本文件,在版本库中保存时换行符使用LF,当从版本库检出到工作区时,则根据平台的不同或者用户的设置的不同,对文本文件的换行符进行转换(转换为LF、CR或CRLF)。
为什么换行符转换要特意强调文本文件呢?这是因为如果对二进制文件(程序或者数据)当中出现的换行符进行上述转换,会导致二进制文件被破坏。因此判别文件类型是文本文件还是二进制文件,是正确进行文件换行符转换的基础。
有的版本控制系统,如CVS,必须在添加文件时人为的设定文件类型(用-kb参数设定二进制文件),一旦用户忘记对二进制文件进行标记,就会造成二进制文件被破坏。这种破坏有时藏的比较深,例如在Linux上检出文件一切正常,因为版本库中被误判为文本文件的图形文件中所包含字符0x0A在Linux上检出没有改变,但是在Windows上检出会导致图形文件中的0x0A字符被转换为0x0D 0x0A两个字符,造成图片被破坏。
有的版本控制系统可以自动识别文本文件和二进制文件,但是识别算法存在问题。例如Subversion检查文件的前1024字节的内容,如果其中包含NULL字符(0x00),或者超过15%是非ASCII字符,则Subversion认定此文件为二进制文件(参见Subversion源代码subversion/libsvn_subr/io.c中的svn_io_detect_mimetype2函数)。这种算法会将包含大量中文的文本文件当作二进制文件,不进行换行符转换,也不能进行版本间的比较(除非强制执行)。
Git显然比Subversion更了解这个世界上文字的多样性,因此在判别二进制文件上没有多余的判别步骤,只对blob对象的前8000个字符进行检查,如果其中出现NULL字符(0x00)则当作二进制文件,否则为文本文件(参见Git源代码xdiff-interface.c中的buffer_is_binary函数)。Git还允许用户通过属性文件对文件类型进行设置,属性文件设置优先。
Git缺省并不开启文本文件的换行符转换,因为毕竟Git对文件是否是二进制文件所做的猜测存在误判的可能。如果用户通过属性文件或者其他方式显式的对文件类型进行了设置,则Git就会对文本文件开启换行符转换。
下面是一个属性文件的示例,为方便描述标以行号。
1 *.txt text
2 *.vcproj eol=crlf
3 *.sh eol=lf
4 *.jpg -text
5 *.jpeg binary
包含了上面属性文件的版本库,会将.txt、.vcproj、.sh为扩展名的文件视为文本文件,在处理过程中会进行换行符转换,而将.jpg、.jpeg为扩展名的文件视为二进制文件,不进行换行符转换。
依据属性文件进行换行符转换
关于属性文件,会在后面的章节详细介绍,现在可以将其理解为工作区目录下的.gitattributes文件,其文件匹配方法及该文件的作用范围和.gitignore文件非常类似。
像上面的属性文件示例中,第1行设置了扩展名为.txt的文件具有text属性,则所有扩展名为.txt的文件添加到版本库时,在版本库中创建的blob文件的换行符一律转换为LF。而当扩展名为.txt的文件检出到工作区时,则根据平台的不同使用不同的换行符,如在Linux上检出使用LF换行符,在Windows上检出使用CRLF换行符。
示例中的第2行设置扩展名为.vcproj的文件的属性eol的值为crlf,隐含着该文件属于文本文件的含义,当向版本库添加扩展名为.vcproj文件时,在版本库中创建的blob文件的换行符一律转换为LF。而当该类型的文件检出到工作区时,则一律使用CRLF作为换行符,不管是在Windows上检出,还是在Linux上检出。
同理示例中的第3行设置的扩展名为.sh的文件也会进行类似的换行符转换,区别在于该类型文件无论在哪个平台检出,都使用LF作为换行符。
向上面那样逐一为不同类型的文件设置换行符格式显得很麻烦,可以在属性文件中添加下面的设置,为所有文件开启自动文件类型判断。
* text=auto
当为所有文件设置了text=auto的属性后,Git就会在文件检入和检出时对文件是否是二进制进行判断,采用前面提到的方法:如果文件头部8000个字符中出现NULL字符则为二进制文件,否则为文本文件。如果判断文件是文本文件就会启用换行符转换。至于本地检出文件采用什么换行符格式,实际上是由core.eol配置变量进行设置的,不过因为core.eol没有设置时采用缺省值native,才使得工作区文本文件的检出采用操作系统默认的换行符格式。配置变量core.eol除了默认的native外,还可以使用lf和crlf,不过一般较少用到。
使用Git配置变量控制换行符转换
在Git 1.7.4之前,用属性文件的方式来设置文件的换行符转换,只能逐一为版本库进行设置,如果要为本地所有的版本库设定文件换行符转换就非常的麻烦。Git 1.7.4提供了全局可用的属性文件,实现了对换行符转换设定的全局控制,我们会在后面的章节加以介绍。现在介绍另外一个方法,即通过配置变量core.autocrlf来开启文本文件换行符转换的功能。例如执行下面的命令,对配置变量core.autocrlf进行设置:
$ git config --global core.autocrlf true
默认Git不对配置变量core.autocrlf进行设置,因此在也没有通过属性文件指定文件类型的情况下,Git不对文件进行换行符转换。但是将配置变量core.autocrlf设置为下列值时,会开启Git对文件类型的智能判别并对文本文件执行换行符转换。
设置配置变量core.autocrlf为true。
效果就相当于为版本库中所有文件设置了text=auto的属性。即通过Git对文件类型的自动判定,对文本文件进行换行符转换。在版本库的blob文件中使用LF作为换行符,而检出到工作区时无论是什么操作系统都使用CRLF为换行符。注意当设置了core.autocrlf为true时,会忽略core.eol的设置,工作区文件始终使用CRLF作为换行符,这对于Windows下的Git非常适合,但不适用于Linux等操作系统。
设置配置变量core.autocrlf为input。
同样开启文本文件的换行符转换,但只是在文件提交到版本库时,将新增入库的blob文件的换行符转换为LF。当从版本库检出文件到工作区,则不进行文件转换,即版本库中文件若是采用LF换行符,检出仍旧是LF作为换行符。这个设置对Linux等操作系统下的Git非常适合,但不适合于Windows。
配制``core.safecrlf``捕捉异常的换行符转换
无论是用户通过属性文件设定文件的类型,还是通过Git智能判别,都可能错误的将二进制文件识别为文本文件,在转换过程中造成文件的破坏。有一种情况下破坏最为严重,就是误判的文件中包含不一致的换行符(既有CRLF,又有LF),这就会导致保存到版本库中的blob对象无论通过何种转换方式都不能还原回原有的文件。
Git提供了名为core.safecrlf的配置变量,可以用于捕捉这种不可逆的换行符转换,提醒用户注意。将配置变量core.safecrlf设置为true时,如果发现存在不可逆换行符转换时,会报错退出,拒绝执行不可逆的换行符转换。如果将配置变量core.safecrlf设置为warn则允许不可逆的转换,但会发出警告。