From line endings to deploy archives: the attributes every team needs
A properly configured .gitattributes file solves problems many PHP teams consider unavoidable: CRLF noise between Windows and Linux, bloated deployment archives, skewed GitHub language stats caused by vendored code, and unreadable pull request diffs for generated or minified files. This article walks through every relevant directive with real examples for PHP and Magento projects, covering line endings, export-ignore, linguist attributes, and diff drivers for everyday development.
Table of Contents
- 1. What .gitattributes is and why it goes beyond .gitignore
- 2. Normalizing line endings: text=auto and eol=lf against CRLF chaos
- 3. export-ignore for clean git archive output and deployment tarballs
- 4. linguist-vendored and linguist-generated for clean GitHub language stats
- 5. Custom diff drivers for readable diffs on lockfiles and minified assets
- 6. merge=union: automatic merging for changelogs and additive files
- 7. Magento-specific paths: handling generated/, var/, pub/static/, vendor/ correctly
- 8. Debugging .gitattributes with git check-attr and common pitfalls
- 9. .gitattributes directives compared side by side
- 10. Summary
- 11. FAQ
1. What .gitattributes is and why it goes beyond .gitignore
A PHP project that only has a .gitignore is covering half the ground: .gitignore decides which files get into version control in the first place. .gitattributes, on the other hand, controls how Git treats files that are already tracked, on every checkout, commit, diff, merge, and archive. The file is a plain text list of path-pattern-to-attribute mappings that lives in the project root or in subdirectories, and unlike the purely local .git/info/attributes, it gets committed into the repository and applies to the whole team immediately.
For PHP projects, and Magento projects in particular, this is not a nice-to-have: teams work across Windows, macOS, and Linux machines, composer.lock and vendor directories have different requirements than hand-written application code, and deployment pipelines generate both development and production artifacts from the same repository. Without a deliberately configured .gitattributes, this is exactly where the familiar symptoms show up: unnecessary line-ending diffs, bloated deploy archives, skewed language stats on GitHub, and unreadable pull request diffs for generated or minified files. The following sections show which directive solves which of these problems.
2. Normalizing line endings: text=auto and eol=lf against CRLF chaos
The most common .gitattributes problem in mixed teams is CRLF/LF noise: a Windows developer with a misconfigured editor saves a PHP file with CRLF line endings while the rest of the team works with LF on Linux or macOS. The result is a commit that appears to change every single line even though nothing meaningful actually changed, code reviews become unusable, git blame shows the wrong authors, and real changes get lost in the noise. The directive * text=auto fixes this at the root: Git automatically detects text files and always normalizes their line endings to LF internally inside the repository.
For checkout alone, text=auto is often not enough, because depending on each developer's local core.autocrlf setting, CRLF can still be checked out. For PHP projects, the more robust recommendation is to explicitly add eol=lf as well, PHP interpreters and most build tools handle LF everywhere without issues, and the team enforces consistent behavior regardless of each developer's local autocrlf setting. Important: a newly added or changed .gitattributes rule does not retroactively affect files already sitting in the index. Only an explicit renormalization step applies the new rule to the entire existing codebase, and that step should always happen as its own, isolated commit.
# .gitattributes for a Magento 2 / PHP project
# 1. Line endings: normalize everything to LF in the repository
* text=auto eol=lf
# Explicit overrides for common PHP/Magento file types
*.php text eol=lf diff=php
*.phtml text eol=lf
*.xml text eol=lf
*.twig text eol=lf
*.less text eol=lf
*.css text eol=lf
*.js text eol=lf
*.json text eol=lf
*.md text eol=lf
composer.lock text eol=lf diff=composerlock
package-lock.json text eol=lf -diff
# Binary files: never touch line endings or run a text diff
*.png binary
*.jpg binary
*.gif binary
*.woff2 binary
*.ico binary
# 2. Export-ignore: keep these out of git archive / deploy tarballs
/tests export-ignore
/.github export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore
/docs export-ignore
/phpunit.xml.dist export-ignore
/phpcs.xml export-ignore
# 3. GitHub language stats and PR diff behavior
vendor/* linguist-vendored
generated/* linguist-generated
pub/static/* linguist-generated
*.min.js linguist-generated
*.min.css linguist-generated
# 4. Additive files that should merge automatically
CHANGELOG.md merge=union
# Apply new .gitattributes rules retroactively to already-tracked files
$ git add --renormalize .
$ git status
# Lists every file whose stored line endings changed, no content changes
# Commit the renormalization separately from real code changes
$ git commit -m "Normalize line endings via .gitattributes"
# Verify no working tree changes remain afterwards
$ git status
nothing to commit, working tree clean
3. export-ignore for clean git archive output and deployment tarballs
git archive builds a tarball or zip archive from a commit or branch that contains only file contents, no .git metadata, no history. That makes it a natural tool for deployment pipelines that want to build a production artifact from a clean repository state. Without export-ignore, though, everything that is tracked ends up in that archive: test suites, CI configuration, developer documentation, .github workflows, and the .gitattributes file itself. In a typical Magento project this needlessly bloats every deploy artifact and, in the worst case, can ship internal CI configuration or placeholder credentials to the production server.
The export-ignore directive marks path patterns that remain tracked in the repository and are committed, pushed, and kept in history as normal, but get filtered out of every git archive result. Typical candidates are tests/, .github/, docs/, .gitattributes itself, and internal tooling scripts that have no business being on the production system. Important for Magento teams: GitHub and GitLab download links as well as Composer archive commands use the same mechanism internally, so export-ignore behaves consistently there too. The result is a leaner, safer deployment artifact without losing anything from the developer workflow itself.
# Without export-ignore: everything tracked ends up in the archive
$ git archive --format=tar HEAD | tar -tf - | grep -E 'tests/|\.github/'
tests/Unit/ExampleTest.php
.github/workflows/ci.yml
# After adding export-ignore rules and committing .gitattributes:
$ git archive --format=tar HEAD | tar -tf - | grep -E 'tests/|\.github/'
# (no output: both paths are excluded from the archive)
# Build a deployment tarball the same way a CI pipeline would
$ git archive --format=tar.gz --output=deploy-$(git rev-parse --short HEAD).tar.gz HEAD
$ tar -tzf deploy-*.tar.gz | wc -l
842 # noticeably smaller than a full checkout
4. linguist-vendored and linguist-generated for clean GitHub language stats
GitHub Linguist determines two visible things about a repository: the language bar on the project page, and whether a changed path shows up fully expanded or collapsed as a diff in pull requests. Without hints, Linguist counts every file purely by extension, a committed vendor/ directory full of Composer dependencies or a generated/ folder with Magento's compiled DI code then gets wrongly counted as first-party project code. That not only skews the language stats in favor of third-party libraries, it also means pull requests that accidentally include generated files get flooded with thousands of diff lines, burying the actual change.
linguist-vendored=true explicitly marks paths like vendor/ as third-party code: they are excluded from the language stats and collapsed by default in pull request diffs, while still remaining viewable via "Load diff" when needed. linguist-generated=true is meant for automatically produced files, such as compiled CSS/JS from the Tailwind build or Magento's generated/metadata, reviewers are no longer prompted to review them line by line. Important: both attributes only affect how GitHub renders the repository, not Git's own behavior, diff and merge continue to work independently of them.
5. Custom diff drivers for readable diffs on lockfiles and minified assets
Git's default diff works line by line and is practically useless for certain file types: a minified JS or CSS file often consists of a single line thousands of characters long, so even the smallest change shows up as a complete line replacement with no discernible difference. The diff directive lets you attach a named diff driver to a path pattern, defined in .git/config or ~/.gitconfig. For minified assets, -diff (disabling diffing entirely, showing "Binary files differ") or a driver with a textconv that reformats content before comparison is often enough.
This becomes even more valuable for structured lockfiles like composer.lock or package-lock.json: a textconv script can extract just the package name and version before diffing, instead of comparing the entire JSON blob, which is nearly always fully rewritten due to hash recalculation. The result is a diff that actually shows package foo was updated from version 2.1.0 to 2.3.0, instead of hundreds of seemingly changed lines. The driver is configured once per developer environment in the Git configuration, and referenced solely through the path pattern entry in the versioned .gitattributes.
# .git/config (or ~/.gitconfig): register custom diff drivers
[diff "composerlock"]
# Extract package name/version instead of comparing raw JSON
textconv = php bin/composer-lock-textconv.php
cachetextconv = true
[diff "minified"]
# Skip line-based diff for minified assets, just flag as changed
binary = true
[diff "php"]
# Show the enclosing function/class name in diff hunk headers
xfuncname = "^[\\t ]*((abstract|final|public|protected|private|static)[\\t ]+)*(class|function)[\\t ]+.*$"
6. merge=union: automatic merging for changelogs and additive files
Git's default three-way merge detects conflicts line by line and marks overlapping changes with conflict markers, regardless of whether those changes actually collide in meaning. For purely additive files like a CHANGELOG.md, where several parallel feature branches each append a new line at the end, that produces unnecessary conflicts even though both changes could easily coexist. The built-in union merge strategy, enabled via the merge=union directive in .gitattributes, automatically combines both versions instead of reporting a conflict, and requires no additional entry in the Git configuration to work.
Union merge is deliberately a specialized tool for files where line order and occasional duplicates are tolerable, it fits well for changelogs, translation files, or additive lists of feature flags. For structured configuration files like composer.json, .gitattributes itself, or PHP classes, union merge is dangerous instead: two conflicting changes to the same JSON property or method would silently both be kept without any error, potentially corrupting the file syntactically or semantically. The rule of thumb: only use union merge on files where a human would, in doubt, have manually accepted every silently merged line anyway.
7. Magento-specific paths: handling generated/, var/, pub/static/, vendor/ correctly
Magento projects bring several classes of directories that each need their own .gitattributes considerations. var/, with cache, logs, and sessions, practically always belongs entirely in .gitignore and is irrelevant to .gitattributes because this content is never tracked in the first place. generated/, with compiled DI and interception code, is also ignored in the standard setup, but some deployment strategies deliberately commit it on a precompiled artifact branch, in that case these paths should get both export-ignore and linguist-generated=true, since they have no business in a source code review or in an archive meant for developers.
pub/static/, with compiled Tailwind CSS and bundled JavaScript, follows the same logic as generated/: relevant to production but not code a reviewer should evaluate line by line. vendor/ is the trickiest case: if it is committed for compliance or air-gapped deployment reasons, it should get linguist-vendored=true so the language stats aren't dominated by third-party libraries, but explicitly no export-ignore, because a production environment simply won't start without the Composer dependencies. The rule of thumb: export-ignore for anything the running application doesn't need, linguist attributes for anything that shouldn't count as first-party code, decide each independently of the other.
8. Debugging .gitattributes with git check-attr and common pitfalls
When a .gitattributes rule appears not to apply, git check-attr is the first debugging tool. git check-attr -a path/to/file.php shows exactly which attributes are actually active for that path, including all rules merged from multiple, differently nested .gitattributes files across the project. The precedence rule is decisive here: more specific patterns and rules from .gitattributes files closer to the affected path override more general rules from the project root. A common mistake is expecting a new rule in a subdirectory .gitattributes to apply, when a broader pattern in the root file already matches first and takes precedence.
A second, very common pitfall: attributes only affect files that Git actually touches through its checkout, diff, or commit machinery. A freshly added rule does not automatically affect files that are already sitting unchanged in the repository, for that the git add --renormalize . step shown in section 2 is required. Additionally, core.autocrlf should not be actively set to true alongside a configured eol attribute, since the two mechanisms can otherwise override each other, the recommendation is core.autocrlf=input on Unix systems and a clean eol=lf in .gitattributes as the actual source of truth.
# Show every attribute that actually applies to a given path
$ git check-attr -a app/code/Mironsoft/SeoSuite/Model/Sitemap.php
app/code/Mironsoft/SeoSuite/Model/Sitemap.php: diff: php
app/code/Mironsoft/SeoSuite/Model/Sitemap.php: eol: lf
app/code/Mironsoft/SeoSuite/Model/Sitemap.php: text: set
# Check a specific attribute across multiple paths at once
$ git check-attr export-ignore -- tests/Unit/ExampleTest.php vendor/autoload.php
tests/Unit/ExampleTest.php: export-ignore: set
vendor/autoload.php: export-ignore: unspecified
# Confirm which rules apply to a generated Magento path
$ git check-attr --all -- generated/metadata/global.php
generated/metadata/global.php: linguist-generated: set
generated/metadata/global.php: export-ignore: set
9. .gitattributes directives compared side by side
The table below summarizes the most important .gitattributes directives for PHP and Magento projects: what they actually solve and what typically goes wrong without them.
| Directive | Solves this problem | Typical mistake without it | Result with the correct rule |
|---|---|---|---|
| * text=auto eol=lf | Consistent line endings across the repository | CRLF/LF mix produces diff noise on every commit | Clean, platform-independent diffs |
| export-ignore | Lean git archive / deploy artifacts | Tests, CI config, and docs end up in the production tarball | Only runtime-relevant files in the archive |
| linguist-vendored | Accurate language stats on GitHub | vendor/ dominates the language bar | Stats reflect actual project code |
| linguist-generated | Readable PR diffs for generated code | Generated files flood the pull request | Diff collapsed by default |
| diff=driver / textconv | Readable diffs for lockfiles/minified assets | composer.lock diff shows the entire JSON blob | Diff shows only the package/version change |
| merge=union | Conflict-free merging of additive files | CHANGELOG.md produces unnecessary merge conflicts | Both lines are kept automatically |
None of these directives replace a good .gitignore, the two mechanisms complement each other and solve different problems. Combining all six directives from the table in a versioned .gitattributes file covers the most common sources of diff noise, bloated deploy artifacts, and skewed language stats in a single step that benefits the whole team.
Mironsoft
Git workflows, deployment pipelines, and repository hygiene for PHP and Magento teams
Need a clean .gitattributes setup for your PHP project?
We audit existing repositories, set up line endings, export-ignore rules, linguist attributes, and diff drivers, and optimize Magento deploy pipelines for lean, clean artifacts.
.gitattributes audit
Review existing repositories for line endings, export-ignore, and diff behavior
CI/CD pipeline integration
Wire git archive, renormalization, and diff drivers into build processes
Magento deploy optimization
Configure generated/, pub/static/, and vendor/ cleanly for deployment
10. Summary
A deliberately configured .gitattributes file solves several independent problems at once that many PHP teams would otherwise debug repeatedly, one by one: * text=auto eol=lf eliminates CRLF/LF noise between Windows, macOS, and Linux developers. export-ignore keeps tests, CI configuration, and documentation out of deployment artifacts built via git archive. linguist-vendored and linguist-generated keep language stats and pull request diffs focused on actual first-party code. Custom diff drivers make lockfiles and minified assets readable, and merge=union prevents unnecessary conflicts on additive files like changelogs.
Magento projects add the extra task of deliberately classifying paths like generated/, var/, pub/static/, and vendor/: export-ignore for anything the running application doesn't need, linguist attributes for anything that shouldn't count as first-party code. Once these rules are cleanly captured in a versioned .gitattributes, every commit, every pull request, and every deployment benefits automatically, without any individual developer having to intervene manually.
.gitattributes for PHP Projects: The Essentials at a Glance
Line endings
* text=auto eol=lf normalizes all text files to LF. Retroactive changes need git add --renormalize ..
export-ignore
Keeps tests, CI config, and docs out of git archive deploy tarballs without removing them from the repository.
linguist attributes
linguist-vendored/linguist-generated clean up GitHub language stats and pull request diffs.
Diff & merge
Custom diff drivers for lockfiles, merge=union only for additive files like changelogs.