Normalizing Line Endings in Git: CRLF vs. LF
If you’ve ever worked on a project where developers use different operating systems, you know that line endings can be a peculiar source of frustration. This issue of CRLF vs. LF line endings is actually fairly popular—you’ll find tons of questions on StackOverflow about how to configure software like Git to play nicely with different operating systems.
The typical advice is to configure your local Git to handle line ending conversions for you. For the sake of comprehensiveness, we’ll look at how that can be done in this article, but it isn’t ideal if you’re on a large team of developers. If just one person forgets to configure their line endings correctly, you’ll need to re-normalize your line endings and recommit your files every time a change is made.
A better solution is to add a .gitattributes file to your repo so you can enforce line endings consistently in your codebase regardless of what operating systems your developers are using. Before we look at how that’s done, we’ll briefly review the history behind line endings on Windows and Unix so we can understand why this issue exists in the first place.
History can be boring, though, so if you stumbled upon this post after hours of frustrated research, you can skip straight to A simple .gitattributes config and grab the code. However, I do encourage reading the full post to understand how these things work under the hood—you’ll (hopefully) never have to Google line endings again!
What Are Line Endings
Newline (frequently called line ending, end of line (EOL), line feed, or line break) is a control character or sequence of control characters in a character encoding specification (e.g. ASCII table or EBCDIC ) that is used to signify the end of a line of text and the start of a new one.
The concepts of line feed (LF) and carriage return (CR) are closely associated and can be considered either separately or together. In the physical media of typewriters and printers, two axes of motion, "down" and "across", are needed to create a new line on the page. Although the design of a machine (typewriter or printer) must consider them separately, the abstract logic of software can combine them together as one event. This is why a newline in character encoding can be defined as LF and CR combined into one (commonly called CR+LF or CRLF).
OS designers had to choose how to represent the start of a new line in text in computer files. For various historical reasons, in the Unix/Linux world a single LF character was chosen as the newline marker; MS-DOS chose CR+LF, and Windows inherited this. Thus different platforms use different conventions.
Operating system | Abbreviation | Escape Sequence |
---|---|---|
Linux, Unix, Free BSD, Unix like OS | LF | \n |
Microsoft Windows, MS-DOS, Symbian OS, Palm OS | CR LF | \r\n |
When you copy text file from windows to linux, you copy new line as \r\n and this escape seqence is saved to file in linux machine. When you print such file with e.g. cat -v file.txt
, linux represent \n as a new line, but carriage return
(escape seqence \r) still remain in the end of each file.
Further reading: How to remove/delete CTRL-M (^M) characters from text files in Linux and UNIX systems
So, when you copy file from windows to linux/bsd system, ^M (carriage return ) char still remain on the end of every line.
~] cat -v file.txt
1. line number 1^M
2. line number 2^M
3. line number 3^M
4. line number 4^M
Line Endings in Git
As you can probably imagine, the lack of a universal line ending presents a dilemma for software like Git, which relies on very precise character comparisons to determine if a file has changed since the last time it was checked in. If one developer uses Windows and another uses Mac or Linux, and they each save and commit the same files, they may see line ending changes in their Git diffs—a conversion from CRLF to LF or vice versa. This leads to unnecessary noise due to single-character changes and can be quite annoying.
For this reason, Git allows you to configure line endings in one of two ways: by changing your local Git settings or by adding a .gitattributes file to your project. We’ll look at both approaches over the course of the next several sections.
Line Ending Transformations Concern the Index
Before we look at any specifics, I want to clarify one detail: All end-of-line transformations in Git occur when moving files in and out of the index — the temporary staging area that sits between your local files (working tree ) and the repository that later gets pushed to your remote. When you stage files for a commit, they enter the index and may be subject to line ending normalization (depending on your settings). Conversely, when you check out a branch or a set of files, you’re moving files out of the index and into your working tree.
When normalization is enabled, line endings in your local and remote repository will always be set to LF and never CRLF. However, depending on some other settings, Git may silently check out files into the working tree as CRLF. Unlike the original problem described in this article, this will not pollute git status with actual line ending changes—it’s mainly used to ensure that Windows developers can take advantage of CRLF locally while always committing LF to the repo.
When normalization is enabled, line endings in your local and remote repository will always be set to LF and never CRLF.
We’ll learn more about how all of this works in the next few sections.
Configuring Line Endings in Git with core.autocrlf
As I mentioned in the intro, you can tell Git how you’d like it to handle line endings on your system with the core.autocrlf setting. While this isn’t the ideal approach for configuring line endings in a project, it’s still worth taking a brief look at how it works.
You can enable end-of-line normalization in your Git settings with the following command:
git config --global core.autocrlf [true|false|input]
You can also view the current Git setting using this command:
git config --list
By default, core.autocrlf is set to false on a fresh install of Git, meaning Git won’t perform any line ending normalization. Instead, Git will defer to the core.eol setting to decide what line endings should be used; core.eol defaults to native, which means it depends on the OS you’re using. That’s not ideal because it means that CRLF may make its way into your code base from Windows devs.
That leaves us with two options if we decide to configure Git locally: core.autocrlf=true and core.autocrlf=input. The line endings for these options are summarized below.
Setting | Repo (check-in) | Working Tree (checkout) |
---|---|---|
core.autocrlf=true | LF | CRLF |
core.autocrlf=input | LF | original (usually LF, or CRLF if you're viewing a file you created on Windows) |
Both of these options enable automatic line ending normalization for text files, with one minor difference: core.autocrlf=true converts files to CRLF on checkout from the repo to the working tree, while core.autocrlf=input leaves the working tree untouched.
core.autocrlf=true is recommended setting for Windows developers
For this reason, core.autocrlf=true tends to be recommended setting for Windows developers since it guarantees LF in the remote copy of your code while allowing you to use CRLF in your working tree for full compatibility with Windows editors and file formats.
Normalizing Line Endings in Git with .gitattributes
*You certainly could ask all your developers to configure their local Git. But this is tedious, and it can be confusing trying to recall what these options mean since their recommended usage depends on your operating system. If a developer installs a new environment or gets a new laptop, they’ll need to remember to reconfigure Git. And if a Windows developer forgets to read your docs, or someone from another team commits to your repo, then you may start seeing line ending changes again.
Fortunately, there’s a better solution: creating a .gitattributes file at the root of your repo to settle things once and for all. Git uses this config to apply certain attributes to your files whenever you check out or commit them. One popular use case of .gitattributes is to normalize line endings in a project. With this config based approach, you can ensure that your line endings remain consistent in your codebase regardless of what operating systems or local Git settings your developers use since this file takes priority. You can learn more about the supported .gitattributes options in the official git docs .
A Simple .gitattributes Config
The following .gitattributes config normalizes line endings to LF for all text files checked into your repo while leaving local line endings untouched in the working tree:
* text=auto
Add the file to the root of your workspace, commit it, and push it to your repo.
Let’s also understand how it works.
First, the wildcard selector * matches all files that aren’t gitignored. These files become candidates for end-of-line normalization, subject to any attributes you’ve specified. In this case, we’re using the text attribute, which normalizes all line endings to LF when checking files into your repo. However, it does not modify line endings in your working tree. This is essentially the same as setting core.autocrlf=input in your Git settings.
More specifically, the text=auto option tells Git to only normalize line endings to LF for text files while leaving binary files (images, fonts, etc.) untouched. This distinction is important—we don’t want to corrupt binary files by modifying their line endings.
After committing the .gitattributes file, your changes won’t take effect immediately for files checked into Git prior to the addition of .gitattributes. To force an update, you can use the following command since git 2.16 :
git add --renormalize .
git add --renormalize . update line endings in the local repository.
This updates all tracked files in your repo according to the rules defined in your .gitattributes config. If previously committed text files used CRLF in your repo and are converted to LF during the renormalization process, those files will be staged for a commit. You can then check if any files were modified like you would normally:
git status
# output:
On branch main
Your branch is up to date with 'origin/main'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: .gitignore
modified: .vscode/settings.json
modified: README.md
The only thing left to do is to commit those changes (if any) and push them to your repo. In the future, anytime a new file is checked into Git, it’ll use LF for line endings.
Verifying Line Endings in Git for Any File
If you want to verify that the files in your repo are using the correct line endings after all of these steps, you can run the following command:
git ls-files --eol
# output:
i/lf w/crlf attr/text=auto .gitignore
i/lf w/crlf attr/text=auto .vscode/settings.json
i/lf w/crlf attr/text=auto README.md
Or only for a particular file:
git ls-files path/to/file --eol
For text files, you should see something like this:
i/lf w/crlf attr/text=auto README.md
From left to right, those are:
- i - line endings in Git’s index (and, by extension, the repo). Should be lf for text files.
- w - line endings in your working tree. May be either lf or crlf for text files.
- attr - The attribute that applies to the file. In this example, that’s text=auto.
- The file name itself.
For binary files like images, note that you’ll see -text for both the index and working tree line endings. This means that Git correctly isolated those binary files, leaving them untouched:
i/-text w/-text attr/text=auto image.png
Git Line Endings: Working Tree vs. Index
You may see the following message when you stage files containing CRLF line endings locally (e.g., if you’re on Windows and introduced a new file, or if you’re not on Windows and renormalized the line endings for your codebase):
warning: CRLF will be replaced by LF in <file-name>.
The file will have its original line endings in your working directory.
This is working as expected — CRLF will be converted to LF when you commit your changes, meaning that when you push those files to your remote, they’ll use LF. Anyone who later pulls or checks out that code will see LF line endings locally for those files.
But the text attribute doesn’t change line endings for the local copies of your text files (i.e., the ones in Git’s working tree)—it only changes line endings for files in the repo. Hence the second line of the message, which notes that the text files you just renormalized may still continue to use CRLF locally (on your file system) if that’s the line ending with which they were originally created/cloned on your system. Rest assured that text files will never use CRLF in the remote copy of your code.
The eol Attribute: Controlling Line Endings in Git’s Working Tree
Sometimes, you actually want files to be checked out locally on your system with CRLF while still retaining LF in your repo. Usually, this is for Windows-specific files that are very sensitive to line ending changes. Batch scripts are a common example since they need CRLF line endings to run properly. It’s okay to store these files with LF line endings in your repo, so long as they later get checked out with the correct line endings on a Windows machine. You can find a more comprehensive list of files that need CRLF line endings in the following article: .gitattributes Best Practices .
When we configured our local Git settings, we saw that you can achieve this desired behavior with core.autocrlf=true. The .gitattributes equivalent of this is using the eol attribute, which enables The LF normalization for files checked into your repo but also allows you to control which line ending gets applied in Git’s working tree:
- eol=lf - converts to LF on checkout.
- eol=crlf - converts to CRLF on checkout.
In the case of batch scripts, we’d use eol=crlf:
# All files are checked into the repo with LF
* text=auto
# These files are checked out using CRLF locally
*.bat eol=crlf
In this case, batch scripts will have two non-overlapping rules applied to them additively: text=auto and eol=crlf.
This change won’t take effect immediately, so if you run git ls-files --eol after updating your .gitattributes file, you might still see LF line endings in the working tree. To update existing line endings in your working tree so they respect the eol attribute, you’ll need to run the following set of commands per this stackOverflow answer :
git rm --cached -r .
git reset --hard
This commands updating line endings in the working tree to reflect our eol preferences.
You’ll notice that this command differs from git add --renormalize ., which we previously used to update line endings in the local repo. Now, we’re updating line endings in the working tree to reflect our eol preferences. If you now you run git ls-files --eol, you should see i/lf w/crlf for any files matching the specified pattern.
Under the hood, the eol attribute also implies text with no value, so it’s the same as doing *.bat text eol=crlf in this example. Prior to git 2.10.0 , there was a bug where text=auto eol=crlf implied text eol=crlf that is, the auto-text-detection algorithm didn’t work. This has now been fixed, so text=auto can safely be used with eol.
One final note: In the recommended .gitattributes file, we used * text=auto to mark all text files for end-of-line normalization to LF once they’re staged in Git’s index. We could’ve also done * text=auto eol=lf, although these two are not identical. Like I mentioned before, if you only use * text=auto, you may still see some CRLF line endings locally in your working tree; this is okay and is working as expected. If you don’t want this, you can enforce * text=auto eol=lf instead. However, this is usually not necessary because the main concern is about what line endings make it into the index and your repo.
Summary: Git Config vs. .gitattributes
There are some similarities between Git’s local settings and the Git attributes we looked at. The table below lists each Git setting, its corresponding .gitattributes rule, and the line endings for text files in the index and working tree:
Git config | .gitattributes | Index/Repo | Working Tree |
---|---|---|---|
core.autocrlf=true | * text=auto eol=crlf | LF | CRLF |
core.autocrlf=input | * text=auto | LF | original (LF or CRLF) |
Bonus: Create an .editorconfig File
A .gitattributes file is technically all that you need to enforce the line endings in the remote copy of your code. However, as we just saw, you may still see CRLF line endings on Windows locally because .gitattributes doesn’t tell Git to change the working copies of your files.
Again, this doesn’t mean that Git’s normalization process isn’t working; it’s just the expected behavior. However, this can get annoying if you’re also linting your code with ESLint and Prettier, in which case they’ll constantly throw errors and tell you to delete those extra CRs:
Fortunately, we can take things a step further with an .editorconfig file; this is an editor agnostic project that aims to create a standardized format for customizing the behavior of any given text editor. Lots of text editors (including VS Code) support and automatically read this file if it’s present. You can put something like this in the root of your workspace:
root = true
[*]
end_of_line = lf
In addition to a bunch of other settings, you can specify the line ending that should be used for any new files created through this text editor. That way, if you’re on Windows using VS Code and you create a new file, you’ll always see line endings as LF in your working tree. Linters are happy, and so is everyone on your team!