In Git, what does a .gitattributes file control, and how are its rules resolved?
answer
- Per-path settings, not repository-wide ones
- Pattern dialect borrowed from another Git file
- Four states: set, unset, valued, unspecified
- One source outranks the committed files
- Assignments are versioned, driver definitions are not
basics
~10 sA .gitattributes file assigns per-path attributes that change how Git diffs, merges, filters and archives matching files. Patterns use .gitignore syntax; within one file the last matching line wins, and .git/info/attributes outranks committed files.
solid answer
~50 s`.gitattributes` attaches settings to **paths** rather than to the repository as a whole, and Git consults it whenever the answer depends on the file type: how to diff it (`diff=<name>`, `-diff`), how to merge it (`merge=union`, `-merge`), whether to run content filters (`filter=<name>`), what to leave out of `git archive` (`export-ignore`), and end-of-line handling. Each line is a pattern in the same glob dialect as `.gitignore`, followed by attributes in four forms: set (`attr`), unset (`-attr`), set to a value (`attr=value`), and unspecified (`!attr`). Resolution goes `.git/info/attributes` first, then `.gitattributes` nearest the path and upward to the work-tree root, then the global and system files; inside a single file a later line beats an earlier one. Check the outcome with `git check-attr -a -- <path>`. Crucially, the attributes are committed and shared, but the *driver definitions* they name live in config, which is not.
code
gitattributes · 5 lines*.png binary
*.md diff=markdown
CHANGELOG.md merge=union
.github/ export-ignore
src/generated/** -diffgo deeper
Recognise the file and know it applies settings per path using .gitignore-style patterns. Being able to read a line marking an image extension as binary is enough at this level.
Explain the four attribute states, the precedence chain from the repository-local attributes file down to the global one, and last-match-wins inside a file. Know git check-attr as the inspection command.
Demonstrate the versioning split: attributes ship with the repository while the drivers they name live in unversioned config, so behaviour silently differs per machine unless you rely on built-ins or automate setup.
Own it as shared infrastructure: review changes to this file like build configuration, prefer built-in behaviours that degrade safely, and document or bootstrap any driver a repository depends on so contributors and automation agree.
## What the file is for Most Git configuration is repository-wide or user-wide. `.gitattributes` is the mechanism for saying "this rule applies to these paths". It is how a repository declares that `*.png` is not worth diffing, that `CHANGELOG.md` should merge by union, that `*.docx` needs a text conversion before display, or that a `.github/` directory should not appear in an exported archive. ## Syntax A line is a pattern followed by one or more attributes. The pattern dialect is the same as `.gitignore`: no slash means match the name at any depth, a slash anchors to the directory containing the file, `*` does not cross a separator, `**` does. Attributes take four states: - `text` sets the attribute. - `-diff` unsets it. - `diff=pdf` sets it to a value, usually naming a driver. - `!merge` makes it *unspecified*, deliberately clearing any value a lower-precedence file assigned. Comments begin with `#`. Macro attributes, written `[attr]name attr1 -attr2`, expand to a bundle; Git ships one built-in macro, `binary`, and custom macros may only be defined in a top-level attributes file. ## Precedence For a given path Git consults, from highest precedence to lowest: `$GIT_DIR/info/attributes`, then the `.gitattributes` in the path's own directory, then those in each parent directory up to the top of the work tree (nearer wins), then the per-user file named by `core.attributesFile`, then the system-wide file. Note the contrast with ignore rules, where the repository-local `info/exclude` sits *below* committed files. Here the local override wins, which makes `info/attributes` a useful escape hatch for one clone. Within a single file, evaluation is last-match-wins, so a broad line at the bottom silently overrides more specific lines above it. ## The split that surprises people `.gitattributes` is committed, so the *assignment* travels with the repository. But an assignment such as `*.pdf diff=pdf` only names a driver; the driver itself is defined in config, for example `diff.pdf.textconv`, and config is per-machine and not versioned. A teammate who clones the repository gets the attribute and none of the behaviour. Anything you rely on must therefore either use a built-in behaviour that needs no config (`-diff`, `binary`, `merge=union`, `export-ignore`) or come with setup instructions, a bootstrap script, or a documented onboarding step. ## Inspecting `git check-attr -a -- path/to/file` lists every attribute in effect for a path, and `git check-attr diff -- path` asks about one. The output names the attribute and its resolved value, which is the fastest way to confirm that the line you wrote is the line that is winning. It is the direct analogue of `git check-ignore -v` for this file. ## When new rules take effect Attributes affect operations as they happen: a diff you run now, a merge you start now, an archive you create now. They do **not** retroactively change blobs already stored. Adding a filter or a text attribute does not reprocess files that are already in the index and the working tree; the content only passes through the new rules the next time it goes into or out of the repository. That is why changing attributes is usually paired with a deliberate re-checkout or re-add of the affected paths. ## Attributes Git does not interpret Unknown attribute names are legal and simply carried; Git assigns them and does nothing else. That is how hosting and analysis tools piggyback on the file, for instance the `linguist-` family of attributes, which Git itself never acts on. Reading such a line tells you the behaviour it triggers is implemented by some other tool, not by Git. ## Small file, large blast radius Because a single pattern can change how every merge of a file type behaves for every contributor, `.gitattributes` deserves the same review attention as build configuration. A stray `-diff` on a source glob makes reviews unreadable; a stray `binary` on text makes merges conflict wholesale.
- What do export-ignore and export-subst do in a Git .gitattributes file?Both affect `git archive`. A path marked `export-ignore` is left out of the generated archive, which is how repositories keep CI configuration and test fixtures out of a source tarball without deleting them from history. `export-subst` makes Git expand `$Format:...$` placeholders in the file's content while archiving, using the same formatting codes as log output, so an archive can carry the commit it was built from.
- How does the precedence of .gitattributes differ from that of .gitignore?They invert the repository-local file. For attributes, `.git/info/attributes` has the *highest* precedence, above every committed `.gitattributes`, then nearer files beat further ones, then the global file. For ignore rules, the command line comes first, then committed per-directory files, and only then `.git/info/exclude` and the global list. So an attribute can be overridden locally in one clone, while a project's ignore rules cannot.
Repository config is a house rule for the whole building; .gitattributes is a label stuck on particular doors telling Git how to handle what is behind each one.
saying these in an interview costs you the question
- Thinks .gitattributes is just about line endings
- Assumes naming a driver is enough for teammates
- Believes the first matching line wins
- Expects new attributes to reprocess existing blobs
- Confuses it with .gitignore's precedence order