Merge Conflicts in Git sicher lösen
Merge Conflicts entstehen, wenn Git Änderungen nicht automatisch verbinden kann. Du lernst, Konflikte sicher zu prüfen.
Merge Conflicts gehören zu den Momenten, in denen Git dich zwingt, genauer hinzusehen. Für Android-Entwicklung ist das keine Nebensache: Kotlin-Code, Compose-Funktionen, Ressourcen, Gradle-Konfiguration und Architekturentscheidungen werden oft parallel verändert. Wenn du Konflikte sauber löst, schützt du nicht nur deinen Branch, sondern auch die gemeinsame Verständlichkeit des Projekts.
Was ist das?
Ein Merge Conflict entsteht, wenn Git zwei Änderungen nicht automatisch zu einer gemeinsamen Version zusammenführen kann. Das passiert typischerweise, wenn zwei Branches dieselbe Stelle in derselben Datei unterschiedlich geändert haben. Git weiß dann nicht, welche Fassung richtig ist, und markiert den betroffenen Bereich. Deine Aufgabe ist nicht, eine Seite mechanisch zu akzeptieren, sondern den Sinn beider Änderungen zu verstehen und daraus eine korrekte Endfassung zu bauen.
Das mentale Modell ist wichtig: Git vergleicht Textstände, aber es versteht keine Android-Architektur, keine Compose-State-Regeln und keine fachliche Absicht. Wenn in einem Branch ein ViewModel umbenannt wurde und in einem anderen Branch eine neue StateFlow-Eigenschaft ergänzt wurde, sieht Git nur Textänderungen. Du musst erkennen, dass vielleicht beide Änderungen gebraucht werden. Ein Konflikt ist deshalb kein Signal für Schuld, sondern ein Koordinationspunkt in der Zusammenarbeit.
In echten Android-Teams treten Merge Conflicts oft an Stellen auf, die viele Entwickler berühren. Dazu gehören Navigationsgraphen, build.gradle.kts, zentrale Theme-Dateien, String-Ressourcen, Dependency-Injection-Module, Repository-Klassen, UI-State-Modelle und gemeinsame Testhelfer. Je zentraler eine Datei ist, desto höher ist die Wahrscheinlichkeit, dass mehrere Branches gleichzeitig daran arbeiten.
Für Lernende ist der wichtigste Schritt: Behandle jeden Konflikt als kleine fachliche Analyse. Frage dich: Was wollte Branch A erreichen? Was wollte Branch B erreichen? Welche Version erfüllt nach dem Merge beide Absichten, ohne Verhalten zu verlieren? Genau diese Haltung trennt saubere Konfliktlösung von riskantem Klicken auf „accept current“ oder „accept incoming“.
Im Android-Kontext hängt das eng mit Qualität und Architektur zusammen. Die offiziellen Android-Empfehlungen betonen klare Zuständigkeiten, testbare Schichten und nachvollziehbare Datenflüsse. Wenn du einen Konflikt löst, solltest du diese Struktur respektieren. Eine schnelle Konfliktlösung, die UI-Logik ins Repository verschiebt oder einen Test entfernt, kann später mehr Arbeit verursachen als der ursprüngliche Konflikt.
Wie funktioniert es?
Git markiert Konflikte in Dateien mit speziellen Trennlinien. Du siehst dann meist drei relevante Teile: den aktuellen Stand deines Branches, den eingehenden Stand des anderen Branches und die Grenze zwischen beiden Varianten. In der Datei kann das so aussehen: oben steht deine Version, unten die andere Version. Diese Markierungen dürfen niemals im fertigen Code bleiben. Solange sie vorhanden sind, ist die Datei nicht gültig und das Projekt sollte nicht gebaut werden.
Der typische Ablauf ist klar. Zuerst startest du einen Merge, Rebase oder Pull. Git meldet, welche Dateien Konflikte enthalten. Dann öffnest du diese Dateien, liest die betroffenen Blöcke und entscheidest für jede Stelle, wie die finale Version aussehen soll. Danach entfernst du die Konfliktmarker, speicherst die Datei, markierst sie als gelöst und führst den Merge oder Rebase fort. Der entscheidende Teil liegt nicht im Befehl, sondern im Verstehen.
Bei Android-Projekten solltest du zusätzlich die Art der Datei beachten. Ein Konflikt in einer Kotlin-Datei kann die Programmlogik verändern. Ein Konflikt in strings.xml kann Übersetzungen beschädigen. Ein Konflikt in libs.versions.toml oder build.gradle.kts kann Builds, Plugins oder Bibliotheksversionen beeinflussen. Ein Konflikt in Compose-UI kann visuelles Verhalten ändern, auch wenn der Code kompiliert. Deshalb reicht „Build ist grün“ nicht immer aus. Du musst je nach Datei prüfen, ob die App auch fachlich korrekt bleibt.
Ein Anfänger sollte außerdem verstehen, dass Git drei Perspektiven kennt: die gemeinsame Ausgangsversion, deine Änderung und die andere Änderung. Viele Merge-Tools zeigen genau diese drei Ansichten. Das hilft dir, nicht nur das Ergebnis zu sehen, sondern die Entwicklung. Wenn beide Branches denselben Parameter geändert haben, ist das ein echter Widerspruch. Wenn ein Branch eine Funktion umbenannt und der andere eine neue Zeile in derselben Funktion ergänzt hat, kannst du beide Absichten oft kombinieren.
In Kotlin und Compose sind Konflikte manchmal subtil. Ein Branch kann einen Composable-Parameter von String auf ein UI-State-Objekt umgestellt haben. Ein anderer Branch kann denselben Composable um einen Fehlerzustand erweitert haben. Wenn du nur eine Seite nimmst, verlierst du entweder die Architekturverbesserung oder die neue Funktion. Die bessere Lösung kombiniert den neuen State-Typ mit dem neuen Fehlerzustand.
Auch Coroutines und Flow können betroffen sein. Stell dir vor, ein Branch ersetzt einen einfachen Wert durch StateFlow, während ein anderer Branch Loading- und Error-Zustände ergänzt. Die korrekte Endfassung muss möglicherweise einen vollständigen UI-State als Flow anbieten. Wenn du hier nur nach Text entscheidest, erzeugst du leicht Code, der kompiliert, aber Zustände nicht mehr sauber abbildet.
Konflikte in Architekturdateien verdienen besondere Aufmerksamkeit. Wenn ein Repository, Use Case oder ViewModel geändert wurde, solltest du die Richtung der Abhängigkeiten prüfen. UI-Schicht, Domain-Logik und Datenzugriff sollten nicht durcheinandergeraten. Ein Merge ist kein guter Zeitpunkt, Architekturregeln nebenbei aufzuweichen. Gerade in Teams ist es besser, einen unklaren Konflikt kurz mit der Person zu klären, die die andere Änderung eingebracht hat.
Ein weiterer Punkt ist die Reihenfolge der Prüfung. Löse zuerst die Textkonflikte. Danach formatiere nicht sofort ganze Dateien, wenn das Projekt dafür keine klare Regel hat. Große Formatierungsänderungen erschweren Code-Reviews. Baue dann das Projekt. Starte relevante Unit-Tests. Prüfe UI-nahe Änderungen, wenn nötig, direkt in der App oder mit Compose-Tests. Erst danach ist der Konflikt wirklich erledigt.
In der Praxis
Nehmen wir ein kleines Beispiel aus einer Compose-App. Zwei Branches haben denselben Bildschirm verändert. Branch A hat eine einfache Begrüßung eingebaut. Branch B hat zusätzlich einen Ladezustand ergänzt. Nach dem Merge steht in der Datei ein Konflikt. Git könnte etwa so etwas markieren:
@Composable
fun ProfileScreen(state: ProfileUiState) {
<<<<<<< HEAD
Text(text = "Hallo, ${state.name}")
=======
if (state.isLoading) {
CircularProgressIndicator()
} else {
Text(text = state.name)
}
>>>>>>> feature/loading-state
}
Die riskante Lösung wäre, eine Seite komplett zu übernehmen. Wenn du HEAD akzeptierst, verlierst du den Ladezustand. Wenn du die eingehende Version akzeptierst, verlierst du vielleicht die gewünschte Begrüßung. Die bessere Lösung prüft beide Absichten und baut eine gemeinsame Fassung:
@Composable
fun ProfileScreen(state: ProfileUiState) {
if (state.isLoading) {
CircularProgressIndicator()
} else {
Text(text = "Hallo, ${state.name}")
}
}
Das Beispiel wirkt klein, zeigt aber die zentrale Regel: Ein gelöster Konflikt ist nicht die Wahl zwischen links und rechts. Es ist eine neue, bewusste Version. Du musst nach dem Speichern prüfen, ob die Datei syntaktisch gültig ist und ob die UI weiterhin das erwartete Verhalten zeigt. Bei Compose kannst du besonders gut in Zuständen denken: Welche Eingaben bekommt der Composable? Welche Ausgabe soll sichtbar sein? Welche Zustände müssen abgedeckt sein?
Eine praktische Entscheidungsregel lautet: Wenn beide Seiten unterschiedliche fachliche Ziele haben, kombiniere sie nicht blind im selben Block, sondern rekonstruiere zuerst das gewünschte Verhalten in einem Satz. Zum Beispiel: „Der Profilbildschirm zeigt während des Ladens einen Spinner, danach eine persönliche Begrüßung.“ Erst danach schreibst du Code. Dadurch vermeidest du Mischcode, der nur zufällig kompiliert.
Bei Kotlin-Dateien solltest du nach dem Konflikt auf Importe achten. Merge-Tools lassen oft ungenutzte oder doppelte Importe stehen. Android Studio kann sie markieren, aber du solltest nicht jede Warnung ignorieren. Ein falscher Import kann besonders bei Klassen mit gleichem Namen irritieren, etwa State, Result, Uri oder BuildConfig. Prüfe außerdem, ob Signaturen noch zusammenpassen. Wenn ein Branch eine Funktion erweitert hat und der andere alle Aufrufe angepasst hat, kann das finale Ergebnis neue Compilerfehler erzeugen.
Bei Gradle-Dateien ist die häufigste Stolperfalle das unkritische Zusammenführen von Versionsänderungen. Wenn zwei Branches dieselbe Library auf unterschiedliche Versionen setzen, ist nicht automatisch die höhere Version richtig. Prüfe, warum die Version geändert wurde. War es ein Bugfix? Eine API-Anpassung? Eine Vorgabe aus einem anderen Modul? Besonders in Android-Projekten können Plugin-Versionen, Kotlin-Versionen und Compose-Compiler-Kompatibilität voneinander abhängen. Ein Konflikt in libs.versions.toml sollte deshalb mit einem Build und möglichst den betroffenen Tests geprüft werden.
Bei Ressourcen entstehen Konflikte oft durch automatisch oder manuell sortierte XML-Dateien. Zwei neue Strings können denselben Platz in strings.xml beanspruchen. Hier ist die Lösung meist einfach: Beide Strings behalten, eindeutige Namen prüfen und XML korrekt sortieren, falls das Projekt eine Sortierung nutzt. Trotzdem gibt es eine Falle: Wenn beide Branches denselben Key mit unterschiedlichem Text ändern, musst du fachlich entscheiden. Ein String-Key ist Teil der Oberfläche. Falscher Text kann Nutzer verwirren oder Tests brechen, die auf bestimmte Inhalte prüfen.
Bei Tests ist besondere Vorsicht nötig. Entferne keinen Test nur, weil er im Konflikt steht. Tests dokumentieren erwartetes Verhalten. Wenn ein Test nach dem Merge nicht mehr passt, frage zuerst, ob sich das Verhalten wirklich geändert hat. In vielen Fällen musst du den Test aktualisieren, nicht löschen. Ein grüner Build ohne wichtigen Test ist weniger wert als ein kurz roter Build, der dich auf eine echte Unklarheit hinweist.
Für die Zusammenarbeit ist Kommunikation Teil der Konfliktlösung. Wenn du die andere Änderung nicht verstehst, lies den zugehörigen Commit, die Pull-Request-Beschreibung oder frage im Team nach. Das ist kein Zeichen von Unsicherheit, sondern solide Arbeitsweise. Besonders bei Architekturänderungen kann ein kurzes Gespräch verhindern, dass du eine neue Struktur versehentlich zurückbaust.
Eine typische Fehlentscheidung ist „accept incoming“, weil der andere Branch neuer aussieht. Das Alter eines Branches sagt nichts über fachliche Richtigkeit aus. Ebenso problematisch ist „accept current“, weil du deine eigene Änderung besser kennst. Beides sind Abkürzungen, die du nur nutzen solltest, wenn du wirklich geprüft hast, dass die andere Seite nicht gebraucht wird. Die bessere Frage lautet: Welche Änderung muss nach dem Merge im Produkt sichtbar oder im Code erhalten bleiben?
Ein guter persönlicher Workflow sieht so aus: Lies die Konfliktdatei vollständig genug, um den Kontext zu verstehen. Löse jeden Block bewusst. Entferne alle Marker. Formatiere nur die betroffenen Stellen, wenn nötig. Baue das Projekt. Starte die relevanten Tests. Prüfe git diff, bevor du commitest. In diesem Diff solltest du erklären können, warum jede finale Zeile so aussieht. Wenn du das nicht kannst, ist der Konflikt noch nicht sauber verstanden.
Für Android Studio gilt: Die grafischen Merge-Werkzeuge sind hilfreich, aber sie ersetzen dein Urteil nicht. Nutze die Vergleichsansicht, um Änderungen schneller zu sehen. Verlasse dich aber nicht auf Buttons allein. Gerade bei Kotlin kann eine visuell plausible Mischung semantisch falsch sein. Ein Composable kann einen alten Parameter behalten, während das ViewModel bereits einen neuen State liefert. Der Compiler hilft dir dann teilweise, aber nicht bei allen UI- und Architekturfragen.
Auch bei Rebase-Konflikten bleibt das Prinzip gleich. Beim Rebase werden deine Commits nacheinander auf einen anderen Stand angewendet. Dadurch kann derselbe Konflikt mehrfach ähnlich erscheinen. Das wirkt mühsam, hat aber einen Vorteil: Du siehst genauer, welcher Commit welche Änderung einführt. Wenn du lernst, kleine, klare Commits zu schreiben, werden Rebase-Konflikte leichter verständlich. Große Sammelcommits machen Konflikte schwerer, weil viele fachliche Themen vermischt sind.
In Pull Requests solltest du gelöste Konflikte transparent halten. Wenn du beim Konflikt eine fachliche Entscheidung getroffen hast, schreibe kurz in die Beschreibung oder einen Kommentar, was du kombiniert hast. Beispiel: „Loading-State aus Branch X beibehalten, Begrüßungstext aus diesem Branch integriert.“ So kann der Reviewer schneller prüfen, ob deine Entscheidung plausibel ist. Code-Review ist hier nicht nur Kontrolle, sondern ein zweites Paar Augen für eine Stelle, an der Git nicht entscheiden konnte.
Fazit
Merge Conflicts sind ein normaler Teil professioneller Android-Entwicklung und ein gutes Training für sauberes Denken im Team. Löse sie nicht durch reflexartiges Übernehmen einer Seite, sondern durch Verständnis beider Änderungen, klare Rekonstruktion des gewünschten Verhaltens und anschließende Prüfung mit Build, Tests, App-Lauf und Code-Review. Übe das bewusst an kleinen Branches: ändere denselben Composable, dieselbe Ressource oder dieselbe Gradle-Version in zwei Branches, führe sie zusammen und erkläre dir danach im Diff, warum die finale Version korrekt ist.