Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,8 @@

<!-- The label IS the changelog — there is no CHANGELOG.md to update. -->
<!-- Write the PR title as the line you'd want to read in the release notes. -->

<!-- Base branch: develop for normal work. main only for a release or hotfix. -->
<!-- Merge method: SQUASH into develop, REBASE into main. -->
<!-- docs/branching-and-release.md#merging -->

26 changes: 24 additions & 2 deletions docs/branching-and-release.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,31 @@ Admins are deliberately exempt (`enforce_admins: false`), which keeps an escape

## Merging

Merge commits are disabled. **Squash or rebase only**, and linear history is enforced on all protected branches.
**Merge commits are disabled** and linear history is enforced on every protected branch. Which of the two remaining methods to use depends on the direction:

One consequence specific to GitFlow: a `release/*` or `hotfix/*` branch has to merge into **two** branches. Squashing into `main` and then squashing the same work into `develop` produces two unrelated commits with the same content, which is fine here — we don't rely on `git branch --contains` for anything except [tag validation](ci.md#publishyml), which checks reachability from `main` and `support/*` only.
| Merging | Method | Why |
|---|---|---|
| `feature/*` → `develop` | **Squash** | Working branches accumulate WIP. One commit per landed change keeps the trunk readable. |
| `develop` → `main` | **Rebase** | Squashing would collapse an entire release into a single commit on the production branch, losing the per-change history. |
| `release/*` → `main` | **Rebase** | Same, and the individual stabilisation commits are what you cherry-pick back to `develop`. |
| `hotfix/*` → `main` | **Rebase** | Same — you need a real commit to port back. |
| anything → `develop` (port-back) | **Squash** | It's a working branch like any other. |

GitHub can't enforce a method per branch, so this is discipline rather than configuration. Both methods stay enabled because both are correct somewhere.

### Why rebase across two long-lived branches is safe here

Rebase-merge rewrites commits, so `main` never becomes an ancestor of `develop` — and once a hotfix has landed on `main` and been ported back, the merge base falls behind both. The obvious worry is that the *next* release would try to replay commits already present on `main`.

It doesn't. `git rebase` detects already-applied commits by patch-id and drops them, so a second release replays only the genuinely new work. Verified rather than assumed: after a release, a hotfix on `main`, and a cherry-pick back to `develop`, a rebase of `develop` onto `main` listed three candidate commits and replayed exactly one.

The edge case to know: if a port-back was **conflict-resolved differently** from the original, its patch no longer matches and rebase will try to apply it again. That surfaces as a conflict at release time — visible and fixable, not silent.

### The double merge-back

A `release/*` or `hotfix/*` branch has to reach **two** branches. Because merge commits are disabled, the second one is a cherry-pick or a fresh PR rather than a literal merge — see [releasing.md](releasing.md#hotfix). The effect is what matters: a fix that never reaches `develop` ships once and then disappears on the next release.

We don't rely on `git branch --contains` anywhere except [tag validation](ci.md#publishyml), which checks reachability from `main` and `support/*` only — so the SHA divergence between the two branches costs us nothing.

## See also

Expand Down
2 changes: 1 addition & 1 deletion docs/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ gh release create v10.4.30-rc.1 --target main --prerelease --generate-notes

The tag must match the version `nbgv` computes for that commit — the workflow packages from the checked-out tag, not from the tag name, so a mismatch ships a `.vsix` whose version disagrees with its release.

> **Merge with rebase, not squash, for `develop` → `main`.** Squashing collapses a whole release into one commit, which loses the per-change history on the production branch and makes later comparisons between the two branches useless. Merge commits are disabled repo-wide and linear history is enforced, so rebase is the only option that keeps commits intact.
> **Merge with rebase, not squash, into `main`.** Squashing collapses a whole release into one commit, losing the per-change history on the production branch. Merge commits are disabled and linear history is enforced, so rebase is the option that keeps commits intact. Repeated rebase-merges stay clean across releases — `git rebase` drops already-applied commits by patch-id. Full reasoning and the one edge case in [branching-and-release.md](branching-and-release.md#merging).

> **"Merge back to develop" is a cherry-pick or a second PR here**, not a literal merge. GitFlow assumes merge commits; this repo enforces linear history. The effect is the same — the fix must reach `develop`, or it ships to users and then disappears on the next release — but the mechanism is a PR carrying the same changes.

Expand Down