Dealing with line endings in Windows with Git and ESLint

Last update: 30 March, 2022
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.

See more info about the difference between CRLF and LF on a Stack Overflow issue.

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.

.gitattributes
# 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.

.editorconfig
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
A note from Captain Obvious: If you're using a different style guide, you may have to adjust these settings. Search for an .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:

git status output

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.

For more details, and more accurate information about the above, see the Reset Demystified chapter from git book that talks about the three trees in Git (working directory, index, and 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:

.gitattributes
# 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:

Passage from gitattributes reference that explains what text auto does

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:

.gitattributes
# 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:

.gitattributes
* text=auto

You can be explicit and tell Git which files are binary so that their ending lines should not be modified:

.gitattributes
*.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:

Terminal output after running git ls-files command with the eol flag on JSON files.
Example output from the ls-files command.

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:

VIM window that shows a JSON file, created by notepad, that contains CRLF line endings.
VIM window that shows a JSON file, created by notepad, that contains CRLF line endings.

LF line endings are not marked by a character, for example:

Vim window that shows a JSON file, created by Vim, that contains LF line endings.
Vim window that shows a JSON file, created by Vim, that contains LF line endings.

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).

A warning from Git in the terminal, after I run the git add command to stage a new file

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.

Other things to read

Popular

Previous/Next