.gitattributes für PHP-Projekte richtig konfigurieren
git
HEAD
Git · .gitattributes · PHP-Projekte · Best Practices
.gitattributes für PHP-Projekte richtig konfigurieren
Von Zeilenenden bis Deploy-Archiven: die Attribute, die jedes Team braucht

Eine sauber konfigurierte .gitattributes-Datei löst Probleme, die viele PHP-Teams für unvermeidbar halten: CRLF-Rauschen zwischen Windows und Linux, aufgeblähte Deployment-Archive, verzerrte GitHub-Sprachstatistik durch Vendor-Code und unlesbare Pull-Request-Diffs bei generierten oder minifizierten Dateien. Dieser Artikel zeigt jede relevante Direktive an echten Beispielen für PHP- und Magento-Projekte, inklusive Zeilenenden, export-ignore, linguist-Attributen und Diff-Treibern für den produktiven Entwickleralltag.

13 Min. Lesezeit .gitattributes · Zeilenenden · export-ignore linguist · Diff-Treiber · Magento 2.4.8

1. Was .gitattributes ist und warum es über .gitignore hinausgeht

Wer ein PHP-Projekt nur mit einer .gitignore ausstattet, deckt lediglich die halbe Miete ab: .gitignore entscheidet, welche Dateien überhaupt in die Versionskontrolle aufgenommen werden. .gitattributes hingegen steuert, wie Git mit den bereits getrackten Dateien umgeht, bei jedem checkout, commit, diff, merge und archive. Die Datei liegt als einfache Textdatei mit Pfadmuster-zu-Attribut-Zuordnungen im Projektroot oder in Unterverzeichnissen und wird, anders als die rein lokale .git/info/attributes, mit ins Repository committet und ist damit für das gesamte Team sofort wirksam.

Für PHP- und insbesondere Magento-Projekte ist das kein Nice-to-have: Teams arbeiten plattformübergreifend an Windows-, macOS- und Linux-Maschinen, composer.lock und Vendor-Verzeichnisse haben andere Anforderungen als handgeschriebener Anwendungscode, und Deployment-Pipelines erzeugen aus demselben Repository sowohl Entwicklungs- als auch Produktionsartefakte. Ohne eine bewusst konfigurierte .gitattributes entstehen genau hier die typischen Symptome: unnötige Zeilenend-Diffs, aufgeblähte Deploy-Archive, verzerrte Sprachstatistiken auf GitHub und unlesbare Pull-Request-Diffs bei generierten oder minifizierten Dateien. Die folgenden Abschnitte zeigen, welche Direktive welches dieser Probleme konkret löst.

2. Zeilenenden normalisieren: text=auto und eol=lf gegen CRLF-Chaos

Das häufigste .gitattributes-Problem in gemischten Teams ist CRLF/LF-Rauschen: Ein Windows-Entwickler mit falsch konfiguriertem Editor speichert eine PHP-Datei mit CRLF-Zeilenenden, während der Rest des Teams auf Linux oder macOS mit LF arbeitet. Das Ergebnis ist ein Commit, der scheinbar jede einzelne Zeile ändert, obwohl inhaltlich nichts Relevantes passiert ist - Code-Reviews werden unbrauchbar, git blame zeigt falsche Autoren, und echte Änderungen gehen im Rauschen unter. Die Direktive * text=auto löst das grundlegend: Git erkennt automatisch Textdateien und normalisiert deren Zeilenenden im Repository intern immer auf LF.

Für den Checkout reicht text=auto allein oft nicht, weil dabei je nach core.autocrlf-Einstellung des einzelnen Entwicklers weiterhin CRLF ausgecheckt werden kann. Für PHP-Projekte ist die robustere Empfehlung, zusätzlich eol=lf explizit zu setzen - PHP-Interpreter und die meisten Build-Tools kommen überall problemlos mit LF klar, und das Team erzwingt damit ein einheitliches Verhalten unabhängig vom lokalen Autocrlf-Setting jedes Entwicklers. Wichtig: Eine neu hinzugefügte oder geänderte .gitattributes-Regel wirkt sich nicht rückwirkend auf bereits im Index liegende Dateien aus. Erst ein expliziter Renormalisierungs-Schritt wendet die neue Regel auf den kompletten bestehenden Bestand an, und dieser Schritt sollte immer als eigener, isolierter Commit erfolgen.


# .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 für saubere git archive-Ausgaben und Deployment-Tarballs

git archive erzeugt aus einem Commit oder Branch ein Tarball- oder Zip-Archiv, das ausschließlich Dateiinhalte enthält, keine .git-Metadaten, keine Historie. Das macht es zum naheliegenden Werkzeug für Deployment-Pipelines, die aus einem sauberen Repository-Zustand ein Artefakt für den Produktivserver bauen wollen. Ohne export-ignore landet in diesem Archiv aber alles, was getrackt ist: Testsuiten, CI-Konfiguration, Entwicklerdokumentation, .github-Workflows und die .gitattributes-Datei selbst. Bei einem typischen Magento-Projekt bläht das jedes Deploy-Artefakt unnötig auf und kann im schlimmsten Fall interne CI-Konfiguration oder Platzhalter-Zugangsdaten auf den Produktivserver transportieren.

Die Direktive export-ignore markiert Pfadmuster, die zwar im Repository getrackt bleiben und normal committet, gepusht und historisiert werden, aber aus jedem git archive-Ergebnis herausgefiltert werden. Typische Kandidaten sind tests/, .github/, docs/, .gitattributes selbst sowie projektinterne Tooling-Skripte, die auf dem Produktivsystem nichts verloren haben. Wichtig für Magento-Teams: GitHub- und GitLab-Downloadlinks sowie Composer-Archive-Befehle nutzen intern denselben Mechanismus, export-ignore wirkt also auch dort konsistent. Das Ergebnis ist ein schlankeres, sichereres Deployment-Artefakt, ohne dass im Entwickler-Workflow selbst irgendetwas fehlt.


# 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 und linguist-generated für saubere GitHub-Sprachstatistik

GitHub Linguist bestimmt zwei sichtbare Dinge im Repository: den Sprachbalken auf der Projektseite und ob ein geänderter Pfad in Pull Requests vollständig oder eingeklappt als Diff angezeigt wird. Ohne Hinweise zählt Linguist jede Datei nach Dateiendung - ein eingechecktes vendor/-Verzeichnis mit Composer-Abhängigkeiten oder ein generated/-Ordner mit von Magento kompiliertem DI-Code wird dann fälschlich als eigener Projektcode gewertet. Das verzerrt nicht nur die Sprachstatistik zugunsten fremder Bibliotheken, sondern sorgt vor allem dafür, dass Pull Requests, die versehentlich generierte Dateien enthalten, mit tausenden Diff-Zeilen überflutet werden und echte Änderungen unauffindbar machen.

linguist-vendored=true markiert Pfade wie vendor/ explizit als Drittanbieter-Code: Sie fließen nicht in die Sprachstatistik ein und werden in Pull-Request-Diffs standardmäßig eingeklappt, bleiben aber über „Load diff" bei Bedarf einsehbar. linguist-generated=true ist für automatisch erzeugte Dateien gedacht, etwa kompiliertes CSS/JS aus dem Tailwind-Build oder Magentos generated/metadata - Reviewer werden nicht mehr aufgefordert, sie Zeile für Zeile zu prüfen. Wichtig: Beide Attribute beeinflussen ausschließlich die Darstellung auf GitHub, nicht das Verhalten von Git selbst, diff und merge laufen davon unabhängig weiter normal.

5. Custom Diff-Treiber für lesbare Diffs bei Lockfiles und Minified Assets

Der Standard-Diff von Git arbeitet zeilenbasiert und ist für bestimmte Dateitypen praktisch nutzlos: Eine minifizierte JS- oder CSS-Datei besteht oft aus einer einzigen, mehrere tausend Zeichen langen Zeile, jede noch so kleine Änderung erscheint als vollständiger Zeilenaustausch ohne erkennbaren Unterschied. Die diff-Direktive erlaubt, für ein Pfadmuster einen benannten Diff-Treiber zu hinterlegen, der in .git/config oder ~/.gitconfig definiert wird. Für minifizierte Assets reicht oft schon -diff (Diff komplett deaktivieren, „Binary files differ") oder ein Treiber mit textconv, der vor dem Vergleich automatisch reformatiert.

Noch wertvoller wird das bei strukturierten Lockfiles wie composer.lock oder package-lock.json: Ein textconv-Skript kann daraus vor dem Diff gezielt Paketname und Version extrahieren, statt den kompletten, durch Hash-Neuberechnung fast immer vollständig veränderten JSON-Blob zu vergleichen. Das Ergebnis ist ein Diff, der tatsächlich zeigt, dass Paket foo von Version 2.1.0 auf 2.3.0 aktualisiert wurde, statt hunderter scheinbar geänderter Zeilen. Der Treiber wird zentral einmal pro Entwicklerumgebung in der Git-Konfiguration hinterlegt, referenziert wird er ausschließlich über den Pfad-Pattern-Eintrag in der versionierten .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: automatisches Zusammenführen für Changelogs und additive Dateien

Der Standard-3-Wege-Merge von Git erkennt Konflikte zeilenbasiert und markiert überlappende Änderungen mit Konfliktmarkern, unabhängig davon, ob die Änderungen inhaltlich überhaupt kollidieren. Bei rein additiven Dateien wie einem CHANGELOG.md, in das mehrere parallele Feature-Branches jeweils eine neue Zeile am Ende anhängen, erzeugt das unnötige Konflikte, obwohl beide Änderungen problemlos nebeneinander bestehen können. Die eingebaute Merge-Strategie union, aktiviert über die Direktive merge=union in der .gitattributes, führt beide Versionen automatisch zusammen, statt einen Konflikt zu melden, und benötigt dafür keinen zusätzlichen Eintrag in der Git-Konfiguration.

Union-Merge ist bewusst ein Spezialwerkzeug für Dateien, bei denen Zeilenreihenfolge und gelegentliche Duplikate tolerierbar sind - für Changelogs, Übersetzungsdateien oder additive Listen von Feature-Flags passt das gut. Für strukturierte Konfigurationsdateien wie composer.json, .gitattributes selbst oder PHP-Klassen ist union-Merge dagegen gefährlich: Zwei widersprüchliche Änderungen an derselben JSON-Property oder Methode würden ohne Fehlermeldung stillschweigend beide übernommen und könnten die Datei syntaktisch oder semantisch beschädigen. Die Regel lautet deshalb: union-Merge nur für Dateien einsetzen, bei denen ein Mensch jede stillschweigend zusammengeführte Zeile im Zweifel auch manuell akzeptiert hätte.

7. Magento-spezifische Pfade: generated/, var/, pub/static/, vendor/ richtig behandeln

Magento-Projekte bringen mehrere Verzeichnisklassen mit, die jeweils eigene .gitattributes-Überlegungen brauchen. var/ mit Cache, Logs und Sessions gehört praktisch immer vollständig in die .gitignore und ist für .gitattributes irrelevant, weil diese Inhalte nie getrackt werden. generated/ mit kompiliertem DI- und Interception-Code wird im Standard-Setup ebenfalls ignoriert, in manchen Deployment-Strategien mit vorkompiliertem Artefakt-Branch aber bewusst mit eingecheckt - dann sollten diese Pfade sowohl export-ignore als auch linguist-generated=true erhalten, weil sie weder im Quellcode-Review noch in einem für Entwickler bestimmten Archiv etwas verloren haben.

pub/static/ mit kompiliertem Tailwind-CSS und gebündeltem JavaScript folgt derselben Logik wie generated/: relevant für den Produktivbetrieb, aber kein Code, den ein Reviewer inhaltlich prüfen sollte. vendor/ ist der heikelste Fall: Wird es aus Compliance- oder Airgapped-Deployment-Gründen committet, sollte es linguist-vendored=true bekommen, damit die Sprachstatistik nicht von Drittanbieter-Bibliotheken dominiert wird, aber explizit kein export-ignore, weil eine Produktivumgebung ohne die Composer-Abhängigkeiten schlicht nicht startet. Die Faustregel: export-ignore für alles, was die laufende Anwendung nicht braucht, linguist-Attribute für alles, was nicht als eigener Code bewertet werden soll, beide Entscheidungen unabhängig voneinander treffen.

8. .gitattributes debuggen mit git check-attr und typische Stolperfallen

Wenn eine .gitattributes-Regel scheinbar nicht greift, ist git check-attr das erste Debugging-Werkzeug. git check-attr -a pfad/zur/datei.php zeigt exakt, welche Attribute für diesen Pfad tatsächlich aktiv sind, inklusive aller Regeln, die aus mehreren, unterschiedlich tief verschachtelten .gitattributes-Dateien im Projekt zusammengeführt wurden. Die Präzedenzregel ist dabei entscheidend: Spezifischere Muster und Regeln aus .gitattributes-Dateien, die näher am betroffenen Pfad liegen, überschreiben allgemeinere Regeln aus dem Projektroot. Ein häufiger Fehler ist, eine neue Regel in einer Unterordner-.gitattributes zu erwarten, obwohl die Root-Datei mit einem breiteren Muster bereits vorher greift und Vorrang hat.

Ein zweiter, sehr häufiger Stolperstein: Attribute wirken nur für Dateien, die Git tatsächlich über seine Checkout-, Diff- oder Commit-Maschinerie anfasst. Eine frisch hinzugefügte Regel betrifft nicht automatisch bereits im Repository liegende, unveränderte Dateien, dafür ist der in Abschnitt 2 gezeigte git add --renormalize .-Schritt nötig. Zusätzlich sollte core.autocrlf nicht parallel zu einem gesetzten eol-Attribut aktiv auf true stehen, weil sich beide Mechanismen sonst gegenseitig überschreiben können, empfohlen ist core.autocrlf=input auf Unix-Systemen und ein sauberes eol=lf in .gitattributes als eigentliche Quelle der Wahrheit.


# 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-Direktiven im direkten Vergleich

Die folgende Tabelle fasst die wichtigsten .gitattributes-Direktiven für PHP- und Magento-Projekte zusammen: was sie konkret lösen und was ohne sie typischerweise schiefgeht.

Direktive Löst folgendes Problem Typischer Fehler ohne diese Regel Ergebnis mit korrekter Regel
* text=auto eol=lf Einheitliche Zeilenenden im Repository CRLF/LF-Mix erzeugt Diff-Rauschen bei jedem Commit Saubere, plattformunabhängige Diffs
export-ignore Schlanke git archive / Deploy-Artefakte Tests, CI-Config und Docs landen im Produktiv-Tarball Nur Laufzeit-relevante Dateien im Archiv
linguist-vendored Korrekte Sprachstatistik auf GitHub vendor/ dominiert den Sprachbalken Statistik zeigt echten Projektcode
linguist-generated Übersichtliche PR-Diffs bei generiertem Code Generierte Dateien fluten den Pull Request Diff standardmäßig eingeklappt
diff=driver / textconv Lesbare Diffs bei Lockfiles/Minified Assets composer.lock-Diff zeigt kompletten JSON-Blob Diff zeigt nur Paket- und Versionsänderung
merge=union Konfliktfreies Zusammenführen additiver Dateien CHANGELOG.md erzeugt unnötige Merge-Konflikte Beide Zeilen werden automatisch übernommen

Keine dieser Direktiven ersetzt eine gute .gitignore, beide Mechanismen ergänzen sich und lösen unterschiedliche Probleme. Wer alle sechs Direktiven aus der Tabelle in einer versionierten .gitattributes-Datei kombiniert, deckt die häufigsten Quellen für Diff-Rauschen, aufgeblähte Deploy-Artefakte und verzerrte Sprachstatistik in einem einzigen, für das ganze Team wirksamen Schritt ab.

Mironsoft

Git-Workflows, Deployment-Pipelines und Repository-Hygiene für PHP- und Magento-Teams

Saubere .gitattributes-Konfiguration für euer PHP-Projekt?

Wir analysieren bestehende Repositories, richten Zeilenenden, export-ignore-Regeln, linguist-Attribute und Diff-Treiber ein und optimieren Magento-Deploy-Pipelines für schlanke, saubere Artefakte.

.gitattributes-Audit

Bestehende Repositories auf Zeilenenden, export-ignore und Diff-Verhalten prüfen

CI/CD-Pipeline-Integration

git archive, Renormalisierung und Diff-Treiber in Build-Prozesse einbinden

Magento-Deploy-Optimierung

generated/, pub/static/ und vendor/ sauber und deploy-tauglich konfigurieren

10. Zusammenfassung

Eine bewusst konfigurierte .gitattributes-Datei löst mehrere unabhängige Probleme gleichzeitig, die viele PHP-Teams sonst einzeln und wiederholt debuggen: * text=auto eol=lf beseitigt CRLF/LF-Rauschen zwischen Windows-, macOS- und Linux-Entwicklern. export-ignore hält Tests, CI-Konfiguration und Dokumentation aus Deployment-Artefakten heraus, die per git archive gebaut werden. linguist-vendored und linguist-generated sorgen für eine Sprachstatistik und Pull-Request-Diffs, die tatsächlich den eigenen Code widerspiegeln. Custom Diff-Treiber machen Lockfiles und minifizierte Assets lesbar, und merge=union verhindert unnötige Konflikte bei additiven Dateien wie Changelogs.

In Magento-Projekten kommt die zusätzliche Aufgabe hinzu, Pfade wie generated/, var/, pub/static/ und vendor/ bewusst einzuordnen: export-ignore für alles, was die laufende Anwendung nicht braucht, linguist-Attribute für alles, was nicht als eigener Code bewertet werden soll. Wer diese Regeln einmal sauber in einer versionierten .gitattributes hinterlegt, profitiert bei jedem Commit, jedem Pull Request und jedem Deployment automatisch davon, ohne dass ein einzelner Entwickler manuell eingreifen muss.

.gitattributes für PHP-Projekte: Das Wichtigste auf einen Blick

Zeilenenden

* text=auto eol=lf normalisiert alle Textdateien auf LF. Nachträgliche Änderungen brauchen git add --renormalize ..

export-ignore

Hält Tests, CI-Config und Docs aus git archive-Deploy-Tarballs heraus, ohne sie aus dem Repository zu entfernen.

linguist-Attribute

linguist-vendored/linguist-generated säubern GitHub-Sprachstatistik und Pull-Request-Diffs.

Diff & Merge

Custom diff-Treiber für Lockfiles, merge=union nur für additive Dateien wie Changelogs.

11. FAQ: .gitattributes für PHP-Projekte

1Was ist der Unterschied zwischen .gitattributes und .gitignore?
.gitignore entscheidet, was getrackt wird. .gitattributes steuert, wie Git bereits getrackte Dateien behandelt, etwa bei Zeilenenden, Diffs, Merges und git archive.
2Warum reicht core.autocrlf allein nicht aus?
core.autocrlf ist lokal und nicht versioniert. eol=lf in .gitattributes gilt einheitlich fürs ganze Team, unabhängig von individueller Konfiguration.
3Was macht export-ignore in Magento-Deploy-Pipelines?
Schließt markierte Pfade aus git archive-Ergebnissen aus, ohne sie aus dem Repository zu entfernen. Verhindert Tests und CI-Config im Produktiv-Tarball.
4linguist-vendored vs. linguist-generated?
vendored markiert Drittanbieter-Code wie vendor/, generated markiert automatisch erzeugte Dateien. Beide säubern Sprachstatistik und Pull-Request-Diffs.
5Wie richte ich einen Diff-Treiber für composer.lock ein?
diff=composerlock in .gitattributes setzen, textconv-Skript unter [diff "composerlock"] in der Git-Konfiguration hinterlegen. Zeigt nur die semantische Änderung.
6Wann ist merge=union sinnvoll, wann gefährlich?
Sinnvoll für additive Dateien wie Changelogs. Gefährlich für strukturierte Dateien wie composer.json, weil widersprüchliche Änderungen stillschweigend übernommen werden.
7Sollte vendor/ export-ignore bekommen?
Nein. Höchstens linguist-vendored, kein export-ignore, da die Anwendung ohne die Abhängigkeiten in der Produktion nicht startet.
8Wie debugge ich eine nicht greifende Regel?
git check-attr -a pfad zeigt alle aktiven Attribute. Häufige Ursachen: Präzedenzkonflikte zwischen mehreren .gitattributes-Dateien oder noch nicht committete Regeln.
9Muss ich nach einer Änderung manuell etwas tun?
Ja, bei Zeilenenden-Regeln. git add --renormalize . wendet neue Regeln rückwirkend auf bereits getrackte Dateien an.
10Gilt .gitattributes auch für generated/ und pub/static/?
Nur wenn diese Verzeichnisse getrackt werden. Bei einem Deployment-Branch mit vorkompilierten Artefakten sollten sie export-ignore und linguist-generated erhalten.