# Git-Arbeitsweise — verbindlich für alle Repositories Stand 04.09.2026. Diese Regeln gelten für **jedes** Repository der Organisation `AG_QGIS`, für Menschen und für KI-Agenten gleichermaßen. Die Kurzfassung für den Agenten steht in `AGENTS.md`; hier steht das Vollständige mit Begründung. Wer eine Abweichung braucht, macht ein Issue auf — nicht einfach einen Push. > **Die goldene Regel** > > In `stable`, `testing` und `unstable` wird nicht entwickelt. Diese Branches nehmen ausschließlich fertige Arbeit > per Pull Request entgegen. Entwickelt wird in kurzlebigen Branches, die nach dem Merge gelöscht werden. > > **Und: Der Mensch drückt ab.** Agenten bereiten alles vor. Push, Merge und Tag macht eine Person mit ihrem > eigenen Zugang. ## 1. Die drei Kanäle Jedes Plugin-Repository hat drei geschützte Branches. Sie entsprechen genau den drei QGIS-Plugin-Feeds im Repository `Repository` (`plugins.xml`, `plugins-testing.xml`, `plugins-unstable.xml`). | Branch | Bedeutung | Feed | Tag-Suffix | Direkter Push | |---|---|---|---|---| | `stable` | stabile Releases, produktionsreif | `plugins.xml` | ohne Suffix | nein | | `testing` | Release-Kandidaten, wird getestet | `plugins-testing.xml` | `-t` | nein | | `unstable` | fertige Features, noch nicht vollständig getestet | `plugins-unstable.xml` | `-u` | nein | ```mermaid flowchart LR F["feature/123-kurzname
bugfix/456-kurzname"] -->|"Pull Request"| U["unstable
Arbeitsstand"] U -->|"Pull Request"| T["testing
Release-Kandidat"] T -->|"Pull Request"| M["stable
stabil"] U -.->|"Tag v26.9.1-u"| RU(["Feed unstable"]) T -.->|"Tag v26.9.1-t"| RT(["Feed testing"]) M -.->|"Tag v26.9.1"| RM(["Feed stable"]) style U fill:#fff4e6,stroke:#d9822b style T fill:#e6f2ff,stroke:#2b7dd9 style M fill:#e8f6ec,stroke:#2f9e44 ``` **Stand heute (04.09.2026):** `stable` existiert in den Plugin-Repositories noch **nicht** und muss vom Maintainer angelegt werden (siehe `docs/konzept/08_repo_einrichten.md`). Bis dahin endet die Kette bei `testing`, und der stabile Feed bleibt leer. Der Branch `oldstable` in `Plugin_SN_Basis` und `Plugin_SN_Plan41` ist eine Altlast aus der Zeit davor; er wird nicht mehr bespielt und nach dem Anlegen von `stable` archiviert oder gelöscht. > **Warum `stable` und nicht `main`?** Der Branchname ist hier nicht frei wählbar. Der Release-Workflow im > Repository `Repository` (Branch `hidden/workflows`) klont das Plugin-Repo und führt `git checkout "$CHANNEL"` > aus. `$CHANNEL` kommt aus dem Tag-Suffix und ist `unstable`, `testing` oder `stable`. **Der Branch muss also > genauso heißen wie der Kanal.** Ein Branch `main` würde den stabilen Release-Pfad brechen, solange die > Infrastruktur nicht mit angepasst wird (ADR 0007). ## 2. Der normale Weg: ein Feature ```bash # 1. Kanal-Branch aktualisieren (macht auch /hallo bzw. python scripts/hallo.py) cd Plugin_SN_Verfahrensgebiet git switch unstable git pull --ff-only # 2. Eigenen Branch abzweigen — immer von unstable, nie von stable oder testing git switch -c feature/41-fbschl-feldname # 3. In kleinen Schritten arbeiten und committen git add functions/verfahrensgebiet_shape_komplett.py tests/test_shape_komplett.py git commit -m "Shape-Import akzeptiert Feldnamen FBSchl. mit Punkt (#41)" # 4. Tests python ../scripts/run_tests.py sn_verfahrensgebiet --baseline # 5. Push — durch den Menschen, mit dessen Zugangsdaten git push -u origin feature/41-fbschl-feldname ``` Danach im Gitea-Browser einen **Pull Request nach `unstable`** öffnen, nach der Vorlage `docs/vorlagen/pull_request.md`, mit Übergabenotiz (Skill `sn-uebergabe`). Ein Pull Request sollte an einem Tag entstehen und in einer Viertelstunde reviewbar sein. Mehr als etwa 400 geänderte Zeilen heißt: das Issue war zu groß. ## 3. Branch-Namen | Präfix | Beispiel | Wofür | |---|---|---| | `feature/` | `feature/41-fbschl-feldname` | neue Funktion | | `bugfix/` | `bugfix/57-basis-laedt-ohne-plan41` | Fehlerbehebung | | `hotfix/` | `hotfix/61-absturz-beim-beenden` | dringender Fix auf `stable` | | `doku/` | `doku/git-arbeitsweise` | nur Dokumentation, ohne Issue zulässig | | `release/` | `release/26.9.1` | Release-Vorbereitung, nur Maintainer | **Regeln für den Namen:** Kleinbuchstaben, ASCII, Bindestriche statt Leerzeichen. Keine Umlaute, kein `ß`, keine Punkte. Issue-Nummer direkt nach dem Präfix, damit Gitea den Branch dem Ticket zuordnet. Gegenbeispiele aus dem Bestand, die es künftig nicht mehr geben soll: | Vorhanden | Problem | Richtig wäre | |---|---|---| | `feature/Drucklayout-überarbeiten` | Umlaut, Großbuchstabe, keine Issue-Nummer | `feature/37-drucklayout` | | `bug/crash_on_exit` | Präfix `bug/` statt `bugfix/`, Unterstriche, keine Nummer | `bugfix/61-absturz-beim-beenden` | | `alkis-nur-laden-stop` | gar kein Präfix | `feature/43-alkis-stop-abfrage` | | `fix-fbschl-feldname` | Präfix `fix-` gibt es nicht | `bugfix/45-fbschl-feldname` | ## 4. Wo fange ich bei einem Fix an? Das ist die Frage, bei der am meisten schiefgeht. Sie hängt nicht davon ab, wie dringend der Fehler ist, sondern davon, **in welchem Kanal er sitzt**. ```mermaid flowchart TD A{"In welchem Kanal
tritt der Fehler auf?"} A -->|"nur in unstable"| B["Branch von unstable
PR nach unstable"] A -->|"in einem testing-Kandidaten"| C["Branch von testing
PR nach testing"] A -->|"in einem stable-Release"| D["Branch von stable
PR nach stable"] B --> B2(["fertig"]) C --> C2["Rückführung:
PR testing → unstable"] D --> D2["Rückführung:
PR stable → testing"] D2 --> D3["Rückführung:
PR testing → unstable"] C2 --> E(["fertig"]) D3 --> E style C fill:#e6f2ff,stroke:#2b7dd9 style D fill:#ffe9e9,stroke:#d94b2b style C2 fill:#fff4e6,stroke:#d9822b style D2 fill:#fff4e6,stroke:#d9822b style D3 fill:#fff4e6,stroke:#d9822b ``` | Fehler sitzt in | Branch von | Pull Request nach | Danach zwingend | |---|---|---|---| | nur `unstable` | `unstable` | `unstable` | — | | `testing`-Kandidat | **`testing`** | `testing` | Rückführung `testing` → `unstable` | | `stable`-Release | **`stable`** | `stable` | Rückführung nach `testing` **und** `unstable` | ### Warum die Rückführung nicht optional ist Ein Fix, der nur in `testing` landet, ist beim nächsten Hochstufen wieder weg: dann wird `unstable` nach `testing` gemergt, und `unstable` kennt den Fix nicht. Der Fehler kommt zurück, und niemand versteht warum. Deshalb gehört zu jedem Fix, der nicht in `unstable` beginnt, ein zweiter Pull Request — sofort, nicht „später". ```bash # Fix in einem testing-Kandidaten git switch testing git pull --ff-only git switch -c bugfix/61-absturz-beim-beenden # ... beheben, testen, committen ... git push -u origin bugfix/61-absturz-beim-beenden # PR nach testing öffnen und mergen lassen # Rückführung, direkt im Anschluss git switch testing && git pull --ff-only git switch -c doku/rueckfuehrung-61-nach-unstable testing git push -u origin doku/rueckfuehrung-61-nach-unstable # PR nach unstable öffnen: "Rückführung Fix #61 aus testing" ``` Führt die Rückführung zu Konflikten, weil `unstable` inzwischen weit vorausgelaufen ist, wird der Fix dort neu angewendet (`git cherry-pick `) — auch das als eigener Branch mit eigenem Pull Request. ## 5. Tags und Releases Version ist `JJ.M.N` — Jahr, Monat, laufende Nummer im Monat. Beispiel: `26.9.1` ist das erste Release im September 2026. Jeder Kanal zählt getrennt hoch. | Kanal | Tag | Wird gebaut aus | |---|---|---| | unstable | `v26.9.1-u` | Kopf von `unstable` | | testing | `v26.9.1-t` | Kopf von `testing` | | stable | `v26.9.1` | Kopf von `stable` | **Achtung:** Der Workflow baut den **Kopf des Kanal-Branches**, nicht den getaggten Commit. Vor dem Taggen also sicherstellen, dass der Branch genau den gewünschten Stand hat. Tags setzt ausschließlich ein Maintainer. Ablauf und Prüfliste: Skill `sn-release`. ## 6. Was der Agent darf und was nicht | Der Agent darf | Der Agent darf nicht | |---|---| | `git fetch`, `git pull --ff-only`, `git status`, `git log`, `git diff` | `git push` in jeder Form | | lokale Branches anlegen und wechseln | `git merge`, `git rebase` | | `git add`, `git commit` auf einem Arbeitsbranch | `git tag` | | Tests laufen lassen, `changelog.txt` ergänzen | Pull Requests mergen | | Issue- und PR-Texte als Entwurf vorbereiten | `git reset --hard`, `git push --force`, `git clean` | | Tickets über `scripts/gitea.py` lesen | irgendetwas auf dem Gitea-Server verändern | | | `docs/status/testbaseline.json` ändern | Das ist nicht nur eine Bitte. `.claude/settings.json` im Repo verbietet die entsprechenden Kommandos technisch; der Agent bekommt eine Ablehnung statt einer Ausführung. Wer die Regel für einen Einzelfall aussetzen will, tut das bewusst und lokal, nicht im Repo. **Warum so streng:** Ein Push trägt den Namen einer Person und ihre Verantwortung. Ein Agent kann fachlich nicht beurteilen, ob ein Stand veröffentlichungsreif ist — und ein versehentlicher Push auf einen Kanal-Branch trifft sofort alle anderen Behörden. ## 7. Branch-Schutz auf Gitea einrichten (Maintainer) Gitea 1.27, je Repository unter **Einstellungen → Branches** (englisch: *Settings → Branches*), dort eine neue Branch-Schutzregel anlegen. Für jeden der drei Kanäle eine Regel: | Einstellung | Wert | Wirkung | |---|---|---| | Branch-Name | `stable` bzw. `testing`, `unstable` | betrifft genau diesen Branch | | Push deaktivieren | an | kein direkter Push, nur Merge über Pull Request | | Merge-Freigabe erforderlich | mindestens 1 | Review durch eine andere Person ist Pflicht | | Statusprüfungen erforderlich | an, sobald CI läuft | Tests müssen grün sein (Vorlage `docs/vorlagen/gitea_workflow_tests.yaml`) | | Veraltete Freigaben verwerfen | an | ein neuer Push macht das Review ungültig | Zusätzlich den Standard-Branch auf `unstable` belassen — dort landet die tägliche Arbeit, und ein neu geklontes Repo steht damit sofort richtig. ## 8. Aufräumen Nach dem Merge wird der Arbeitsbranch gelöscht — in Gitea per Schaltfläche direkt am gemergten Pull Request, lokal so: ```bash git switch unstable git pull --ff-only git branch -d feature/41-fbschl-feldname git fetch --prune ``` Nicht gelöschte Branches sammeln sich an und niemand weiß später, ob sie noch etwas enthalten. Im Bestand liegen davon derzeit vier herum (`feature/Drucklayout-überarbeiten`, `print_layout`, `alkis-nur-laden-stop`, `fix-fbschl-feldname`) — die sind zu prüfen und dann zu entfernen. ## 9. Commit-Nachrichten Deutsch, Präsens, erste Zeile höchstens 72 Zeichen, Issue-Nummer am Ende in Klammern. ``` Shape-Import akzeptiert Feldnamen FBSchl. mit Punkt (#41) DAVID-Exporte liefern das Feld teils mit, teils ohne Punkt. Die Suche prüft jetzt beide Schreibweisen, die Bedeutung der Schlüssel 901/10110F und 901/10120F bleibt unverändert. ``` Was die Nachricht beantworten muss: **was** ändert sich und **warum**. Das *Wie* steht im Diff. Nicht brauchbar: „Fix", „Update", „Änderungen", „wip". ## 10. Sitzungsbeginn Vor dem ersten Handgriff — auch wenn es „nur schnell etwas" ist: ```bash python scripts/hallo.py # oder in Claude Code: /hallo ``` Das holt für alle Klone den Stand vom Server (`git fetch`), aktualisiert die Kanal-Branches per Fast-Forward, wo das gefahrlos möglich ist, und meldet alles Übrige, ohne es anzufassen. Es merged nicht, es stasht nicht, und es fasst keine ungesicherten Änderungen an. Ohne diesen Schritt entstehen Konflikte, die niemand braucht — und im schlimmsten Fall wird ein Fehler behoben, den jemand anderes vor drei Tagen schon behoben hat. ## 11. Häufige Fehler **„Ich habe direkt auf `unstable` committet."** Solange nichts gepusht ist, ist das reparabel: `git switch -c feature/-` legt einen Branch auf dem aktuellen Stand an, danach `git switch unstable && git reset --hard origin/unstable`. Der `reset` ist hier zulässig, weil er nur die lokale Kopie auf den Serverstand zurücksetzt — trotzdem vorher `git log` prüfen. **„Mein Branch ist hinter `unstable`."** Vor dem Pull Request aktualisieren: `git switch unstable && git pull --ff-only`, dann zurück auf den Arbeitsbranch und `git merge unstable`. Kein `rebase` auf bereits gepushten Branches — das schreibt Historie um, die andere schon haben. **„`git pull` will einen Merge-Commit machen."** Dann ist der lokale Branch vorausgelaufen. `--ff-only` bricht in dem Fall bewusst ab. Erst klären, was die eigenen Commits sind, statt blind zu mergen. **„Der Fix ist wieder weg."** Fast immer die fehlende Rückführung aus Abschnitt 4. **„Ich habe im falschen Repo gearbeitet."** Passiert, weil alle Klone nebeneinander liegen. `git status` zeigt oben den Pfad; `python scripts/hallo.py` zeigt alle Repos auf einmal. ## Siehe auch - `AGENTS.md` — Kurzfassung der harten Regeln - `docs/leitfaden/03_arbeitsweise.md` — der Arbeitszyklus vom Issue bis zum Merge - `docs/leitfaden/06_tests.md` — was vor dem Pull Request grün sein muss - `docs/entscheidungen/0007-branchmodell.md` — warum das Modell so aussieht - `docs/konzept/08_repo_einrichten.md` — `stable` anlegen, Branch-Schutz setzen - Skill `sn-release` — Tags und Releases