skip to content

In Git, why does `* text=auto` in .gitattributes beat asking everyone to set core.autocrlf?

level: middleimportance: must knowfreq 40%

answer

  1. One mechanism is committed, the other is not
  2. Think about a brand-new clone
  3. Per-path versus one global switch
  4. Which one wins when both apply
  5. CI runners never ran your setup script

basics

~20 s

In Git, .gitattributes is committed, so the rule travels with every clone and CI checkout and cannot be forgotten on a new machine. It also overrides per-machine config and allows per-path exceptions, which core.autocrlf cannot express.

solid answer

~40 s

`core.autocrlf` lives in a user's config file, so it is opt-in per machine: a new laptop, a contractor, a container image or a CI runner that never sets it silently reintroduces the churn, and a wrong value on the wrong platform makes things worse. `.gitattributes` is a tracked file, so `* text=auto` reaches every clone automatically and applies identically in CI. It is also more expressive: attributes are per-path, so you can say `*.sh text eol=lf`, `*.bat text eol=crlf`, and `*.png binary` in the same file, while `core.autocrlf` is one blunt switch for the whole working tree. Where both apply, the attribute wins. In practice teams commit the attributes file, normalise once with `git add --renormalize .`, and stop configuring line endings on individual machines.

code

gitattributes · 9 lines
gitattributes
* text=auto

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

*.png  binary
*.jar  binary
*.snap -text

go deeper

for a junior

Remember that .gitattributes is committed with the code while core.autocrlf sits in a per-machine config file, so only the first one reaches everybody.

for a middle

Explain the precedence (attribute beats config), what text=auto means versus plain text, and give a per-path example such as *.sh text eol=lf.

for a senior

Argue it operationally: CI runners and containers never run your onboarding steps, so a policy that depends on local setup will regress; show how you would verify the effective policy with git check-attr and git ls-files --eol.

for a principal

Own the general principle — repository policy belongs in versioned content, not in per-developer configuration — and budget the one-time normalisation and its blame and merge fallout as part of adopting it.

## The two ways to state the rule Git converts line endings at check-in and checkout, and there are two mechanisms that decide whether it does. `core.autocrlf` (and its companion `core.eol`) are **configuration**: they live in `/etc/gitconfig`, `~/.gitconfig` or `.git/config`. `.gitattributes` entries are **content**: the file sits in the working tree, is tracked, and is committed and cloned like any source file. That difference is the whole answer, and it splits into four concrete advantages. ## 1. It travels A clone carries `.gitattributes` with it. The moment anyone clones the repository — a new hire, a fork, a CI runner, a Docker build, a release job — the rule is in effect without a setup step. Configuration carries none of that: it must be applied on every machine that will ever touch the repository, forever, and there is no way to detect from inside a clone whether somebody did. Line-ending churn is exactly the kind of defect that appears months later, from the one environment nobody configured. ## 2. It is one decision, not N decisions With per-machine config, the correct value differs by platform (`true` on Windows, `input` on Unix), so the team's policy is really "everyone must set the right one for their own OS". Set `true` on Linux and you fill the working tree with carriage returns. With `* text=auto` the repository states one platform-independent fact — *text blobs are stored with LF* — and Git derives the platform-appropriate working-tree form from `core.eol` or an explicit `eol` attribute. ## 3. It is per-path `core.autocrlf` is a single switch over the whole tree, backed by a content heuristic that guesses which files are text. Attributes let you be explicit where it matters, in one committed file: - `* text=auto` — detect text, store it as LF. - `*.sh text eol=lf` — always LF on disk, even on Windows, so shebang lines work. - `*.bat text eol=crlf` — always CRLF on disk, even on Linux, for tools that require it. - `*.png binary` — never touch it (the `binary` macro unsets `text`, `diff` and `merge`). - `*.snap -text` — a golden-file fixture whose bytes must not change. No configuration key can express "LF for scripts, CRLF for batch files, hands off the fixtures". ## 4. It wins Where both mechanisms cover a path, the attribute decides. That means the repository's committed policy is not at the mercy of somebody's inherited global config, and you can reason about the stored bytes from the repository content alone. `git check-attr text eol -- path` prints the effective attributes for a path, and `git ls-files --eol` shows what actually landed, so the policy is inspectable rather than assumed. ## What `text=auto` actually means `text=auto` asks Git to apply its text/binary heuristic per file: if the content looks like text, normalise it to LF in the object database and convert on checkout according to `eol`/`core.eol`; if it looks binary, leave it alone. Contrast plain `text`, which asserts the path *is* text and normalises unconditionally — dangerous applied globally with `*`, because a mis-classified binary would be corrupted. `auto` is the safe global default; explicit `text` or `binary` is the right tool for a specific path you know about. ## What the attributes file does not do by itself Adding `.gitattributes` changes the rule going forward; it does not rewrite blobs already committed. A repository with CRLF already in history keeps showing churn until you normalise it once with `git add --renormalize .` and commit the result. Nor do attributes stop a developer from committing a file whose *content* has mixed endings inside it — `core.safecrlf` at `warn` is the signal for that case. One more limitation worth naming: `.gitattributes` binds Git, not editors. It guarantees consistent blobs and consistent checkouts, but if an editor insists on rewriting a file, Git will convert it back on the way in — which is exactly the point: the noise stops at the repository boundary. ## Interview framing Lead with "config is per-machine and opt-in, attributes are committed and travel", then add the per-path expressiveness and the precedence rule, and mention that an existing repository needs one renormalisation pass before the policy is actually true of the stored blobs.

  • Why use text=auto rather than plain text for the * pattern?
    `text` asserts every matched path is text and normalises unconditionally, which would corrupt any binary that slipped through. `text=auto` applies Git's content heuristic per file and leaves binary-looking content untouched, so it is the safe form for a catch-all pattern. Use explicit `text` or `binary` on specific paths you know about.
  • How do you keep a Windows-only batch file CRLF on a Linux developer's checkout?
    Give it an explicit working-tree ending: `*.bat text eol=crlf`. The blob is still stored with LF, but checkout writes CRLF on every platform, so the file works regardless of who cloned it. The mirror-image rule, `eol=lf`, is what keeps shell scripts runnable on Windows checkouts.
  • Does committing .gitattributes fix files already in history?
    No. It changes what happens to content from now on. Blobs already stored with CRLF stay as they are until you renormalise the repository once — commit the attributes file, run `git add --renormalize .`, and commit the resulting changes.

saying these in an interview costs you the question

  • Says .gitattributes is a local file like .git/config
  • Thinks core.autocrlf overrides the text attribute
  • Applies plain text to * and risks corrupting binaries
  • Assumes adding the file retroactively fixes history
  • Cannot name a per-path exception attributes make possible

context