# 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