Git 优雅处理行结束符

2,396 阅读18分钟

不同的操作系统使用的行结束符是不一样的

  • UNIX/Linux 使用的是 0x0A(LF)
  • 早期的 Mac 系统使用的是 0x0D(CR)
  • 后来的 OS X 在更换内核后与 UNIX 保持一致使用0x0A(LF)
  • DOS/Windows 一直使用 0x0D0A(CRLF)

为此,Git 先后提供了 core.autocrlf.gitattributes 来解决跨平台协作时,行结束符该如何处理的问题

如果不行看过程,只想看最终解决方案,请直接查看 .gitattributes 文件总结 这一节

core.autocrlf

core.autocrlfGit 提供的一个在提交或检出时自动处理行结束符的功能,它有三种设置

git config --global core.autocrlf true

检入存储库时将文件中CRLF行结束符转换为 LF检出存储库时将文件中LF的行结束符转换为 CRLF

git config --global core.autocrlf input

检入存储库时将文件中CRLF行结束符转换为 LF检出存储库时不转换

git config --global core.autocrlf false

检入、检出均不转换

使用总结

windows系统,请设置

git config --global core.autocrlf true

*macOS 、Linux、Unix,请设置

git config --global core.autocrlf input

core.autocrlf 的不足

core.autocrlf 并不是一个完美的解决方案,它有很多不足之处

无法正确处理混合行结束符的文件

以下是不同环境不同配置在向存储库检入、检出包含混合行结束符的文件时的处理结果及预期

Git 存储库中Git 存储库中检出包含CRLFLF的文件后,工作目录中该文件的行结束符预期从工作目录中检入包含CRLFLF的文件到存储库后,存储库中该文件的行结束符预期
Windows (autocrlf 设为 true)CRLFLFCRLFLFCRLF(不符合预期)LFLF
**Windows **(autocrlf 设为 input)CRLFLFCRLFLFCRLF(不符合预期)LFLF
Windows (autocrlf 设为 false)CRLFLFCRLFLFCRLF(不符合预期)CRLFLFLF(不符合预期)
Mac OS、UNIX/Linux (autocrlf 设为 true)CRLFLFCRLFLF(不符合预期)LFLF
Mac OS、UNIX/Linux (autocrlf 设为 input)CRLFLFCRLFLFLF(不符合预期)LFLF
Mac OS、UNIX/Linux (autocrlf 设为 false)CRLFLFCRLFLFLF(不符合预期)CRLFLFLF(不符合预期)

可以看出 core.autocrlf 配置是无法处理包含混合行结束符的文件的

不够智能

在使用总结中提到了 *macOS 、Linux、Unix 平台应将 core.autocrlf 设置为 input ,但假如我不小心设置为 true 会怎样?答案是,从 git 存储库检出文件时,LF 行结束符会被转换成 CRLF

同理,如果 windows 平台错误的将 core.autocrlf 设置为 input,那么从 git 存储库检出文件时,LF 行结束符不会被准换

一刀切

不是所有类型的文件都应该跟随操作系统转换行结束符的。

例如 bash 脚本文件始终应当使用 LF 作为行结束符,如果被转换成 CRLF 行结束符bash 解释器可能无法正常工作,因此任何情况下 core.autocrlf 不应该将 bash 脚本文件的LF 行结束符转换为 CRLF 行结束符.

同样 Windows 批处理 bat 文件最好使用 CRLF 作为行结束符,如果使用 LF 作为行结束符,且代码中包含了中文字符,那么解释器可能无法正常工作

因此是否转换文件的行结束符,不光考虑平台,还要考虑文件类型,这是 core.autocrlf 所欠缺的

可能会造成数据损坏

以上还只是小问题,如果某些二进制文件被 git 识别成文本文件,并进行了行结束符转换,那么这个文件可能被永久损坏

因此 Git manpages 中,有这样一段描述。

CRLF conversion bears a slight chance of corrupting data. autocrlf=true will convert CRLF to LF during commit and LF to CRLF during checkout. A file that contains a mixture of LF and CRLF before the commit cannot be recreated by git. For text files this is the right thing to do: it corrects line endings such that we have only LF line endings in the repository. But for binary files that are accidentally classified as text the conversion can corrupt data.

Git 的行结束符自动切换功能虽然好用,但是是有一定风险的。因为,源码文件之所以允许行结束符的随意切换,是因为源码对行结束符这一数据是不敏感的。对于某些特定的数据,例如二进制文件。如果该文件被意外的识别成了文本文件,并执行了行结束符转换,那么这个文件的数据可能就会被损坏。

无法约束其他协作者

core.autocrlf 是一个本地配置,只能配置自己的 git,如果其他协作者没有配置正确,问题还是没有解决(假设你用的是 mac 并正确配置 core.autocrlfinput,但是某个 windows 平台的协作者没有配置 core.autocrlf ,提交文件中的 CRLF 行结束符没有被转换成 LF,你 pull 后,还是会有CRLF 行结束符的文件 )

只能约束自己,不能约束其他协作者,让 core.autocrlf 在跨平台协作项目中形同虚设

为了解决以上问题,就需要使用 .gitattributes 文件了

.gitattributes 文件

.gitattributes 文件和 .gitignore 文件一样,一般是配置在当前git仓库中并随代码一起推送到远程 git 存储库中,供其他协作者拉取

.gitignore 文件里面写的是要忽略的文件,而 .gitattributes 文件里面写的则是不同类型的文件,行结束符应该如何处理

.gitattributes 文件不光能处理行结束符问题,它能做的事情有很多,但这不是本文的重点,因此不做过多介绍

在学习之前,先看一下 .gitattributes 文件长什么样子。以下是一个 web 类型的项目的 .gitattributes 文件,# 号开头的是注释


# Auto detect
##   Handle line endings automatically for files detected as
##   text and leave all files detected as binary untouched.
##   This will handle all files NOT defined below.
*                 text=auto

# Source code
*.bash            text eol=lf
*.bat             text eol=crlf
*.cmd             text eol=crlf
*.coffee          text
*.css             text
*.htm             text diff=html
*.html            text diff=html
*.inc             text
*.ini             text
*.js              text
*.json            text
*.jsx             text
*.less            text
*.ls              text
*.map             text -diff
*.od              text
*.onlydata        text
*.php             text diff=php
*.pl              text
*.ps1             text
*.py              text diff=python
*.rb              text diff=ruby
*.sass            text
*.scm             text
*.scss            text diff=css
*.sh              text eol=lf
*.sql             text
*.styl            text
*.tag             text
*.ts              text
*.tsx             text
*.xml             text
*.xhtml           text diff=html

# Docker
Dockerfile        text

# Documentation
*.ipynb           text
*.markdown        text
*.md              text
*.mdwn            text
*.mdown           text
*.mkd             text
*.mkdn            text
*.mdtxt           text
*.mdtext          text
*.txt             text
AUTHORS           text
CHANGELOG         text
CHANGES           text
CONTRIBUTING      text
COPYING           text
copyright         text
*COPYRIGHT*       text
INSTALL           text
license           text
LICENSE           text
NEWS              text
readme            text
*README*          text
TODO              text

# Templates
*.dot             text
*.ejs             text
*.haml            text
*.handlebars      text
*.hbs             text
*.hbt             text
*.jade            text
*.latte           text
*.mustache        text
*.njk             text
*.phtml           text
*.tmpl            text
*.tpl             text
*.twig            text
*.vue             text

# Configs
*.cnf             text
*.conf            text
*.config          text
.editorconfig     text
.env              text
.gitattributes    text
.gitconfig        text
.htaccess         text
*.lock            text -diff
package-lock.json text -diff
*.toml            text
*.yaml            text
*.yml             text
browserslist      text
Makefile          text
makefile          text

# Heroku
Procfile          text

# Graphics
*.ai              binary
*.bmp             binary
*.eps             binary
*.gif             binary
*.gifv            binary
*.ico             binary
*.jng             binary
*.jp2             binary
*.jpg             binary
*.jpeg            binary
*.jpx             binary
*.jxr             binary
*.pdf             binary
*.png             binary
*.psb             binary
*.psd             binary
# SVG treated as an asset (binary) by default.
*.svg             text
# If you want to treat it as binary,
# use the following line instead.
# *.svg           binary
*.svgz            binary
*.tif             binary
*.tiff            binary
*.wbmp            binary
*.webp            binary

# Audio
*.kar             binary
*.m4a             binary
*.mid             binary
*.midi            binary
*.mp3             binary
*.ogg             binary
*.ra              binary

# Video
*.3gpp            binary
*.3gp             binary
*.as              binary
*.asf             binary
*.asx             binary
*.fla             binary
*.flv             binary
*.m4v             binary
*.mng             binary
*.mov             binary
*.mp4             binary
*.mpeg            binary
*.mpg             binary
*.ogv             binary
*.swc             binary
*.swf             binary
*.webm            binary

# Archives
*.7z              binary
*.gz              binary
*.jar             binary
*.rar             binary
*.tar             binary
*.zip             binary

# Fonts
*.ttf             binary
*.eot             binary
*.otf             binary
*.woff            binary
*.woff2           binary

# Executables
*.exe             binary
*.pyc             binary

# RC files (like .babelrc or .eslintrc)
*.*rc             text

# Ignore files (like .npmignore or .gitignore)
*.*ignore         text

.gitattributes 文件每一行由文件或路径名属性列表组成,相互之间用空格隔开。

文件名可以使用 * 匹配多个字符,路径名默认不递归子目录,若要递归,则需要在路径名后加 /**

可设置的属性如下

  • text :设置文件是否为文本类型

  • binary :设置文件是否为二进制类型

  • eol : 设置行结束符

  • diff : 比较文件差异设置

  • ...

这些属性的用法也很简单,比如我要设置文件为文本类型,语法为 text=true 。设置文件不为二进制类型 binary=false

有意思的是,git 为属性值为布尔类型的属性提供了简写,当属性值为 true 时可以只写属性名,当属性值为 false 时可以简写为 -属性名(在属性名前加减号)

例如: text 等价于 text=true-text 等价于 text=false

跟大多数配置文件一样,先定义的配置会被后定义的覆盖,例如上面的 .gitattributes 文件,第一句 * text=auto ,意思是匹配所有文件并设置 text=auto。第二句 *.bash text eol=lf 的意思是,匹配 .bash 后缀的文件并设置 text eol=lf 。有了第二句,第一句的覆盖范围就不再是所有文件,而是**.bash** 后缀之外的文件,另外,只有属性相同的才会被覆盖,如果第二句为 *.bash eol=lf ,那么 .bash 后缀的文件的 text 属性的值还是 auto

text 属性

官网对 text 属性解释 虽然很多,但依然存在没有解释到位的地方,经测试,text 属性的作用会受到 匹配的文件在存储库中使用的行结束符 的影响

注意,是受到存储库中的行结束符的影响

如果匹配的文件还不在存储库中(新创建的文件)或者在存储库中的行结束符为 LF,此时 text 属性的转换逻辑如下

  • 检入到存储库时:查看匹配文件的行结束符是否为 CRLF,如果是,则在入库时将行结束符转换为 LF

  • 检出到本地时:分系统

    • 操作系统的行结束符为CRLF (如windows) :将存储库中匹配文件的 LF,在检出到本地时转换为 CRLF (注意转换后,存储库中的行结束符还是 LF,转换的只是工作目录中文件的行结束符

    • *操作系统的行结束符为LF (如Mac OS、Linux、unix) :将存储库中匹配文件的行结束符原样检出不做转换 (也就是说如果某个文件在存储库行结束符CRLF,那么检出后还是 CRLF )

如果匹配的文件在存储库中的行结束符为 CRLF,此时 text 属性的转换逻辑如下

  • 检入到存储库时原样检入不做转换

  • 检出到本地时:检出的逻辑不变,和上面一样

了解了text 属性的转换逻辑之后,在来看text 属性的几个可选值

  • texttext=true-binary : 指定匹配的文件为文本类型,行结束符转换在不猜测文件类型的情况下进行(-binary 很少用,一般就直接写成 text)
  • -texttext=falsebinary,指定匹配的文件为非文本文件,检入、检出时不进行任何转换操作。(-text 很少用,一般就直接写成 binary)
  • text=auto : 让 Git 自己判断文件类型,如果判断为文本类型,就走转换逻辑,否则不走

使用 text 属性时的注意事项

官网对 text 属性解释 有两点没有提到

  1. 没提到 text 属性检出时的转换逻辑

  2. 没提到,如果匹配的文件在存储库中的行结束符为 CRLF,此时匹配的文件从工作目录检入到存储库时,不管文件的行结束符是什么,统统原样检入不做转换

关于第一点,在很长一段时间里,我一直以为 eol 属性是控制检出时行结束符的转换逻辑, 而text 属性只控制检入时行结束符的转换逻辑,直到我做实验时,结果总是和预期不相符,慢慢发现 text 属性也会控制检出时行结束符的转换逻辑,而且不同的平台转换逻辑还不一样。最可气的时,官网对此只字未提。

关于第二点,官网也没有明确说明,但是提了一句

When text=auto conversion is enabled in a cross-platform project using push and pull to a central repository the text files containing CRLFs should be normalized

意思是说,在跨平台的 git 项目中添加了.gitattributes 文件并对某些文件启用 text=auto 后,应该将这些文件中使用了错误行结束符的文件,进行手动行结束符转换,并提交到 git 存储库中。并给出了具体操作

echo "* text=auto" >.gitattributes
git add --renormalize .
git status        # Show files that will be normalized
git commit -m "Introduce end-of-line normalization"

主要看第二行 git add --renormalize . 它的作用如下

Apply the "clean" process freshly to all tracked files to forcibly add them again to the index. This is useful after changing core.autocrlf configuration or the text attribute in order to correct files added with wrong CRLF/LF line endings. This option implies -u. 对所有跟踪的文件重新应用“clean”进程,强制将它们再次添加到索引中。这在更改core.autocrlf 配置或 .gitattributes 文件的 text 属性后,对纠正添加了错误的 CRLF/LF 行结束符的文件非常有用

简单理解就是 根据 git 存储库中的行结束符 以及 core.autocrlf 配置或 .gitattributes 文件中的内容,对工作目录中的文件重新应用行结束符 ,并将变动放入暂存区, 你可以理解为修改行结束符转换配置后的一个刷新操作,把错误的行结束符“手动”改成正确的。

注意,官方示例在 git add --renormalize . 命令后是有一个 . 的, 这其实是git add . 命令与 --renormalize 属性的结合。关于 git add . 请参考 git add . -A -u 的区别

官网为为什么让我们在更改core.autocrlf 配置或 .gitattributes 文件的 text 属性后,执行 git add --renormalize . 命令,不知道大家有没有考虑这个问题,让我们来想一下,如果我们在更改core.autocrlf 配置或 .gitattributes 文件的 text 属性后,不执行 git add --renormalize . 命令会怎么样?

如果不执行,git 版本库可能会含有 CRLF 行结束符的文件,由于没有执行 git add --renormalize . 命令,更改core.autocrlf 配置或 .gitattributes 文件的 text 属性后,这些文件的行结束符依然是CRLF ,因此在检入这些文件的时候,按照 text 属性行结束符转换逻辑,不会转换这些文件的行结束符,这些文件在 git 版本库依然是 CRLF ,虽然这对 windows 平台没有任何影响,但是 *Mac OS、Linux、unix 平台拉取下来之后,根据 text 属性行结束符转换逻辑,依然是 CRLF ,这就把 *Mac OS、Linux、unix 平台的开发人员给坑了。试想一下,你在 Mac 上拉取了一个项目,用 ide 打开后,突然 git 显示一大堆文件发生了变更,你很纳闷,明明什么都没做,其实是现代ide 都很智能,会自动替换不合理的行结束符,结果就导致明明什么都没做,突然 git 显示一大堆文件发生了变更。

近一步理解 text 属性的行结束符转换逻辑

text 属性的行结束符转换逻辑其实是挺复杂了,虽然我尽量用简短的语音描述了出来,但是信息量依然庞大,我准备了几个例子,来帮助大家更好的理解text 属性的行结束符转换逻辑

前提,所有例子的 .gitattributes 文件的内容如下

* text

例1: 有条件如下

  1. a.txt 文件在 git 存储库中的行结束符CRLF
  2. a.txt 文件在 windows 工作目录中的行结束符CRLF
  3. 执行git add --renormalize . 命令

执行结果是什么?答案暂存区会出现以行结束符LF的 a.txt 文件

分析:a.txt 在存储库中的行结束符CRLF,在工作目录中的行结束符CRLF,但是根据.gitattributes 中的设置,a.txt 在存储库中的行结束符应该为LF, 在执行git add --renormalize . 命令后, git 会把 a.txt 的行结束符改为 LF 并添加到暂存区,因此暂存区会出现以行结束符LF的 a.txt 文件

例2: 有条件如下

  1. a.txt 文件在 git 存储库中的行结束符LF
  2. a.txt 文件在 mac 工作目录中的行结束符LF
  3. 手动将 mac 工作目录中 a.txt 文件的行结束符改为 CRLF
  4. 执行 git add a.txt 命令,将 a.txt 添加到暂存区

请问暂存区的内容是? 答案:暂存区什么都没有,并且工作目录中 a.txt 行结束符变回原来的LF

分析:执行 git add a.txt 命令,将 a.txt 添加到暂存区时,由于 .gitattributes 的存在,会触发行结束符转换逻辑,根据转换逻辑,CRLF 会被转换成 LF ,有意思的地方来了,暂存区暂存的是,跟存储库中不同的改动,由于a.txt 文件在 git 存储库中的行结束符也是LF,并且我们只改了a.txt 文件的行结束符,没有该内容,因此暂存区中会认为没有改动,因此暂存区什么都没有。

我明明把一个有改动的文件添加到暂存区,结果暂存区什么都没有,而且原来的改动还还原了,是不是很神奇,但在理解了text 属性的行结束符转换逻辑之后,就能理解是什么情况了

text 属性总结

.gitattributes 文件的 text 属性,可以说是 core.autocrlf 配置的完美替代了,充分解决了 core.autocrlf 配置各种不足

  • 无法正确处理混合行结束符的文件 :经测试 text 属性可以正常处理,行结束符转换逻辑跟上面说的一样

  • 不够智能 : 检出时,text 属性可以根据不同的系统,执行不同的行结束符转换逻辑

  • 可能会造成数据损坏 : text 属性可以定义非文本文件,非文本文件不走行结束符转换逻辑

  • 无法约束其他协作者 : 通过同步**.gitattributes** 文件, 使得所有协作者拥有相同的行结束符转换逻辑

text 属性唯一没有解决的是 core.autocrlf 配置中的一刀切问题, 但这就该我们的 eol 属性上场了

eol 属性

有时候检入、检出时text 属性行结束符准换逻辑可能并不适合我们的要求。

比如 .sh 类型的文件,无论在哪个平台都应该使用 LF 行结束符 ,而 text 属性行结束符准换逻辑会导致在 windows 平台上检出 .sh 类型的文件时,行结束符被改成 CRLF

再比如 .bat 类型的文件,无论在哪个平台都应该使用 CRLF 行结束符 ,而 text 属性行结束符准换逻辑会导致在 windows 平台上检入 .bat 类型的文件时,行结束符被改成 LF

为了避免这个问题,我们需要使用 eol 属性eol 属性可以指定匹配的文件在检入、检出时,使用固定的行结束符,它有两个属性值

  • eol=crlf : 始终以 CRLF 行结束符检入、检出匹配文件
  • eol=lf : 始终以 LF 行结束符检入、检出匹配文件

另外 eol 也支持无需文件类型检查的行结束符转换,因此特别适合与 text 搭配使用,例如上面提到的 .sh.bat 类型的文件,一般这样写

*.sh text eol=lf
*.bat text eol=crlf

eol 属性总结

eol 属性很好的解决了 core.autocrlf 配置中的一刀切问题,补起了 text 属性的最后一个短板。

注意:eol 属性一般只会用在那些行结束符不应随平台系统改变而改变的文件上,绝大多数类型的文件使用 text 属性 就够了

.gitattributes 文件总结

.gitattributes 文件的text 、eol 属性能很好的解决行结束符转换问题,实际使用中,我们只需要在项目中合理配置不同类型文件的 text 、eol 属性即可,如果不想自己写,github上也有现成的模板,提供了一系列针对各种开发环境,已经写好了的 .gitattributes 文件,可以根据自己的需要自行选择

.gitattributes 文件不光只有 text 、eol 属性,还有 diff、working-tree-encoding等,能实现的功能很多,但是跟处理行结束符关系不大,这里就不多结束了,敢兴趣的可以自行前往 Git Documentation:gitattributes 查看。

core.safecrlf

网上有些文章在介绍 git 行结束符转换 时,有时候还会提到 core.safecrlf 配置,主要作用是在进行“不可逆的”行结束符转换 时,发出警告或阻止,但实际上 core.safecrlf 配置对解决 git 行结束符转换 问题作用不大,反而设置为 true 时,还会阻止自动行结束符转换,因此不建议设置,或者设置为 warn 即可

git config --global core.safecrlf warn

参考

Git Documentation:gitattributes

git-config

Configuring Git to handle line endings