Dealing with line endings in Windows with Git and ESLint
Table of contents
- Add an .editorconfig file
If you are on a Windows machine, and you switch between branches with
git checkout branch_name, Git may replace your carefully placed LF (line feed) line endings with CRLF (carriage return and line feed combo).
Line endings in different operating systems
Windows adds two characters to mark the end of lines, when you press
Enter on your keyboard. It adds the carriage return (CR or
\r) and the line feed (LF or
\n), all together CRLF or
\r\n. Linux/Mac, on the other hand, add only one character, the LF or
More specifically, Git replaces the LF line endings that were placed by:
- your ESLint/Prettier formatters.
- Linux/Mac users that committed on the repository you’re working on.
- Git itself, depending on your Git settings.
- or even you, if setup your code editor to add LF (or use one that does that by default).
When you edit/create files on Windows and press
Enter to move to the next line, at the end of the line, you place CRLF.
This behavior can be frustrating if your ESLint configuration wants LF for line endings. In other words, you get a ton of linting errors every time you change a branch.
The solution is to add the following
.gitattributes file at the root of your project.
# Let Git decide what to do, for every file. More on that later. * text=auto # Overwrite the above and declare files that will always have # LF line endings on checkin and checkout. No ESLint/Prettier # issues for those files. *.js text eol=lf *.jsx text eol=lf # If you're using TypeScript, also add .ts, .tsx files *.ts text eol=lf *.tsx text eol=lf # If you're using Prettier, you probably want LF in .json, .md, .css, # and all the other files you're formatting with Prettier. *.json text eol=lf # An alternative is to ditch the above and make everything have LF on # checkin/checkout, but I'm not sure what will happen with binary files. # * text eol=lf # Declare files that are binary so that their eol should not be modified. # Git, with the first line of this file, will probably not modify binary # files, but add the following just to be safe and explicit. *.png binary *.jpg binary
Find out more in a GitHub help article about dealing with line endings in Git.
Add an .editorconfig file
To make sure your code editor adds LF instead of CRLF in the end of lines, when you edit/create files, you can add an .editorconfig file in the root of your project.
root = true [*] indent_style = space indent_size = 2 charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true end_of_line = lf # editorconfig-tools is unable to ignore longs strings or urls max_line_length = null
.editorconfigfile in the GitHub repository of your guide.
When I first posted this article, I showed the solution but I didn’t explain why it worked because I didn’t know. Now, I (kind of) know, so I will try to explain the solution above. I read a lot of documentation and relevant articles, but still wasn’t able to verify what happened in practice. This was due to a misconception, so If you have the same misconception, this post will be useful to you.
The first thing you have to think is what behavior you want from Git and your linters. For example, this a list with what I want:
- I don’t want to switch branches and my linters/Code editor start yelling at me (and have to run a format script afterwards).
.tsxfiles to always have LF line endings.
- I have also setup Prettier to format
.cssfiles, so I want these files to have LF line endings too.
- I don’t want Git to alter the lines in binary files, for example, images.
core.autocrlf Git setting
I didn’t mentioned the
core.autocrlf setting in the solution, only the
.gitattributes file. They both control how Git handles line endings, so I want to talk about
autocrlf a little bit. The reason I didn’t mentioned it, is because the
.gitattributes file, that I showed above, takes precedence over the value of
The most relevant value for the
core.autocrlf setting, a Windows user can set, is
true. For example:
git config --global core.autocrlf true
The autocrlf entry in Git book says that if you set it to
auto-converts CRLF line endings into LF when you add a file to the index, and vice versa when it checks out code onto your filesystem.
So we have to understand what is the index, and what it means to check out code onto your filesystem.
Because the quote above refers to the filesystem, I will introduce another commonly used term in Git, the working directory or working tree. All files inside your repository are your working directory, no matter their state (staged, unstaged, modified, untracked, etc.).
So, for example, if you edit a file and stage it with
git add, that file will still be in your working directory. I had the misconception that when I stage a file (add it to the index), automatically its status changes, and that file is no longer in my working directory, only in my index. As a result, I expected the
core.autocrlf setting to kick in and replace the line endings to LF for the file in my filesystem. That doesn’t happen.
The next term the quote above mentions is the index. Index, or staging area, (let’s say that) is a Git file, stored in
.git/index, that keeps track of the files (and their content) that are currently staged and ready for your next commit. For example, if I create a file:
And use the
git add command:
git add my_file.json
That file is now in the index. You can verify that by using the
git status command:
To see the contents of a file that’s inside the index, run the following command in your terminal (there are more details about the following command at the end of post):
git show :my_file.json | vi -b -
Every branch or commit hash, (let’s say that) is another file in the Git database. The index, the branches and commits, contain instructions on how to reconstruct the repository files on your filesystem, or, in other words, on how to reconstruct your working directory.
The commits and the index are stored inside your
.git folder in an hard to inspect format, meaning that you can’t see their content easily. In contrast, your working directory is saved directly as regular files in your project folder, where it’s easy to view, edit, and delete them.
Let’s go back to the
autocrlf setting now because this is what this section was about. When the
autocrlf setting claims that will replace the CRLF to LF when you add a file to the index, it means that will add LF to the line endings to the file that will save to the index (and, as a result, in your next commit). It won’t change the line endings in the current file, inside your working directory. Those line endings will remain CRLF or LF or whatever you set them.
It promises that will do the opposite (LF to CRLF) when you pull a file from the Git database to the filesystem (something I don’t want as I stated earlier, by the way). The relevant quote is:
and vice versa (LF to CRLF) when it checks out code onto your filesystem.
This happens when you
git checkout to branches or commit hashes. For example,
git checkout my-new-feature (branch) or
git checkout d36d36d (commit hash).
To pull a file from the database to your working directory (“checks out code onto your filesystem”), the file has to not exist inside the branch you are switching to. I’m not sure at the moment if Git will replace line endings if the file is only modified between branches. Also, if you delete a file that’s currently tracked by Git, lets say you delete the
package.json file, with
rm package.json and restore it with
git checkout package.json, Git will also pull that file from the Git database (and will replace LF to CRLF).
This is why the end of line problems usually occur for files you introduced to your project with your latest branch.
.gitattributes file is an alternative to
core.autocrlf when you want to control how Git handles the line endings of your files.
I will now explain the lines in the
.gitattributes file I previously shared. This is the first line:
# Let Git decide what to do, for every file. * text=auto
First of all, let’s talk about the syntax. Each line in the
.gitattributes file contains:
- a path followed by a space. In this case, the path is the wildcard character (
*). With the wildcard character, we refer to every file, so this rule applies to all files.
- space-separated attributes you set for those paths.
The only attribute here is the
text attribute that is set to the value of
auto. This is the passage from the reference of gitattributes for what text=auto means:
Let’s break down the underlined sections from the above image:
If Git decides that the content is text, its line endings are converted to LF on checkin.
Git runs some heuristics to decide if a file is text or binary. If Git decides that the file is a text file, we have the same behavior with
autocrlf when you add the file in the index, that is to covert CRLF line endings to LF.
When the file has been committed with CRLF, no conversion is done.
The second part about CRLF is not so clear to me. It refers to the checkout from the Git database to your filesystem? Or if Git updates the file’s line endings inside the Git database automatically, if you change the
gitattributes settings. Probably the first, but It doesn’t matter, because the next attribute, which is
eol, will overwrite the
text attribute for the files we care most:
# Overwrite "* text=auto" and declare files that will always have # LF line endings on checkin and checkout. No ESLint/Prettier # issues for those files anymore. *.js text eol=lf *.jsx text eol=lf *.ts text eol=lf *.tsx text eol=lf *.json text eol=lf *.md text eol=lf *.mdx text eol=lf *.css text eol=lf
With the rules above, we target the files that ESLint and Prettier check. Let’s now see what the
text attribute without a value does because it’s different than
Setting the text attribute on a path enables end-of-line normalization and marks the path as a text file. End-of-line conversion takes place without guessing the content type. gitattributes reference on setting the text attribute without a value
This what the
eol attribute does with the value
This setting forces Git to normalize line endings to LF on checkin and prevents conversion to CRLF when the file is checked out. gitattributes reference on setting the eol attribute to the lf value
The end result is the following:
- The file will be considered a text file.
- Git will replace the line endings to LF when you add it to the index.
- Git will keep the LF line endings when you bring it to your filesystem from the Git database.
Another interesting quote from the
eol attribute in the
eol: This attribute sets a specific line-ending style to be used in the working directory. It enables end-of-line conversion without any content checks, effectively setting the text attribute.
In other words, these files will always have LF when you add them to your index (checkin) and when you add them to your working directory (checkout).
Now, although Git should already leave the binary files alone because of the first line:
You can be explicit and tell Git which files are binary so that their ending lines should not be modified:
*.png binary *.jpg binary
See also the end-of-line conversion section from the gitattributes reference that has some practical examples and more accurate information.
Inspect the end of lines in the wild
Now I will show some commands that are useful if you want to inspect the ending lines of your files, while you add/remove the files from your working directory/index.
Probably the most useful is the plumbing command
git ls-files with the
The fourth column shows the filename. The first column shows what type of ending line has the copy of the file inside the index. The second column shows the ending line for the file in your working directory. The third column shows the attributes you set up in the
.gitattributes file and their values.
Plumbing vs porcelain commands
A plumbing command, that I mentioned earlier, is a low level command that's supposed to be used by other scripts and not the end-user. The opposite is a porcelain command. Examples of porcelain commands are
git commit, etc.
If you don’t provide a path to
ls-files --eol, it will print a report for every file in your project. You can then pipe the output to
grep to search for those dirty CRLFs. For example:
git ls-files --eol | grep --color "crlf"
Follow this link to make sense of the ls-files output because it’s not always so easy to understand.
See what value you have for the
git config core.autocrlf # Prints: # true
It may be useful to see the origin of this setting with the
git config --show-origin core.autocrlf # Prints: # file:C:/Program Files/Git/etc/gitconfig true
In my case, it’s coming from a default Git config file, not from global settings or the repository settings.
To see the
gitattributes settings for your project, search for the files:
See the contents of a file that’s inside the index
To see the contents of a file that’s inside the index, run the following command in your terminal:
git show :my_file.json | vi -b -
git show command “Shows various types of objects”. If you prefix a file with colon, it will show the file contents from the index. More details about the colon syntax on gitrevisions reference.
You pipe the output of the
git show command to
vi and explicitly ask Vim to read from
stdin with the
- parameter. To exit Vim (hehe), press =>
Vim with the
-b flag will mark CRLF line endings as
^M, for example:
LF line endings are not marked by a character, for example:
Git’s cryptic warnings about line endings
This is a warning I got from Git when I run
git add to stage a file I created with Vim (LF line endings).
At that time, my setup was
core.autocrlf set to
true and no
.gitattributes file. To remind you, with this setup we get the following behavior:
- When I add the file to the index, Git will convert CRLF to LF. This is not applicable here because the file already has LF.
- Git will convert LF to CRLF when I check it out from the Git database (this is applicable).
The second line says that it won’t change my line endings in the working directory, so it will continue to be LF.
I think that the first line that says:
LF will be replaced by CRLF in
Refers to what will happen if I pull that file from the Git database on a checkout. Also, in case you are curious, if I remove the file from the index with
git restore --staged vim.json, as the output from
git status suggests, the file still keeps the LF line endings.
Other things to read
- Reveal animations on scroll with react-spring
- Gatsby background image example
- Extremely fast loading with Gatsby and self-hosted fonts