Dealing with line endings in Windows with Git and ESLint
Table of contents
Problem
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 \n
.
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.
Solution
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.
To use it in VS Code, install the EditorConfig plugin. The following is the .editorconfig file from Airbnb’s JavaScript style guide, copied from their repository on GitHub. Visit the link to see the updated version.
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
.editorconfig
file in the GitHub repository of your guide.Explanation
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.
Optimal workflow
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).
- In my projects I have setup ESLint to ask for LF line endings. I use TypeScript, JavaScript and React, so I want the
.js
,.jsx
,.ts
, and.tsx
files to always have LF line endings. - I have also setup Prettier to format
.json
,.yml
,.md
,.mdx
, and.css
files, 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.
The 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 core.autocrlf
.
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 true
:
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:
touch my_file.json
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.
HEAD
).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.
The .gitattributes
file
The .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 text=auto
:
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 lf
:
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 gitattributes
reference:
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:
* text=auto
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.
The ls-files
command
Probably the most useful is the plumbing command git ls-files
with the --eol
flag:
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 add
, git status
, 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.
Inspect core.autocrlf
and gitattributes
values
See what value you have for the core.autocrlf
setting:
git config core.autocrlf
# Prints:
# true
It may be useful to see the origin of this setting with the --show-origin
flag:
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:
project_root/.gitattributes
.project_root/.git/info/attributes
.
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 -
The 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 => :q!
.
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
vim.json
.
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.
More links
Other things to read
Popular
- Reveal animations on scroll with react-spring
- Gatsby background image example
- Extremely fast loading with Gatsby and self-hosted fonts