Skip to main content
RunBook Academy

Git, CI/CD & GitOpsXII · Merge vs RebasePolicy

Team policy and consistency — why the team must pick one verb and stick to it

Advanced⏱ ~18 mingit

What you'll learn

  • Articulate why a team must pick one merge-rebase policy and enforce it consistently
  • Identify the cost of an inconsistent policy (graph noise, audit gaps, force-push incidents)
  • Configure `merge.ff`, `pull.rebase`, and `branch.autosetuprebase` to enforce the policy at the tooling level
  • Document the policy in CONTRIBUTING so the choice is explicit and reviewable
  • Migrate an existing repository to a consistent policy without rewriting shared history

Prerequisites

Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x

Not yet marked complete on this device.

A team that has not picked a merge-rebase policy will accumulate both verbs in the same repository. Some engineers will rebase their feature branches before merging; some will merge without rebasing; some will rebase and force-push after review; some will fast-forward when fast-forward is possible and produce merge commits when it is not. The resulting history is neither linear (because of the merge bubbles from the merge-everything engineers) nor consistently topologically informative (because of the silent fast-forwards from the rebase-then-merge engineers). The audit trail is incomplete, the graph is noisy, and the post-incident review cannot reconstruct what happened. The cost of an inconsistent policy is paid every day, in small increments, by every engineer who has to read the graph.

The cost of inconsistency

The three costs of an inconsistent merge-rebase policy are graph noise, audit gaps, and force-push incidents.

Graph noise. git log --graph on an inconsistently-managed repository reads as a mixture of linear segments, merge bubbles, and rebase-induced fast-forwards. The auditor cannot tell at a glance whether a section of history is the result of a merge or a rebase — both look the same after the fact. The visual signal that distinguishes them (the merge commit node) is absent for the rebase cases, so the auditor must consult the PR system to disambiguate. The graph is a worse tool for navigation than it would be under either consistent policy.

Audit gaps. Under a rebase policy, the branch event is gone from the graph. Under a merge policy, the branch event is in the graph. Under an inconsistent policy, the branch event is in the graph for some integrations and gone for others. The auditor cannot use a single query (git log --first-parent, or a PR system join) to reconstruct the integration timeline — they must use both, applied selectively based on which verb was used for each integration. The audit trail is complete only by accident, when the engineer who performed the integration remembered to record the PR number in a place the auditor will find.

Force-push incidents. An inconsistent policy encourages force-pushes. An engineer who rebases their feature branch before merging is used to force-pushing; an engineer who merges without rebasing is not. When the two interact — the rebaser force-pushes a branch the merger had already pulled — the merger’s local view diverges from the remote. The recovery is the breakage chain from XI-06: teammates diverge, CI caches are stale, artifact pins are invalid, signed tags are unreachable. The incidents are not caused by either policy individually; they are caused by the interaction of inconsistent policies on the same branch.

flowchart LR
    subgraph INCONSISTENT["inconsistent policy graph noise"]
        I1["main"] --> I2["merge commit"]
        I2 --> I3["commit"]
        I3 --> I4["commit"]
        I4 --> I5["fast-forward"]
        I5 --> I6["commit"]
        I6 --> I7["merge commit"]
    end
    subgraph CONSISTENT_MERGE["consistent merge policy"]
        C1["main"] --> C2["merge"]
        C2 --> C3["merge"]
        C3 --> C4["merge"]
        C4 --> C5["merge"]
    end
    subgraph CONSISTENT_REBASE["consistent rebase policy"]
        R1["main"] --> R2["commit"]
        R2 --> R3["commit"]
        R3 --> R4["commit"]
        R4 --> R5["commit"]
    end

The first diagram shows what an inconsistent policy produces: a mixture of merge commits and fast-forwards, with no way to tell from the graph which integrations were merges and which were fast-forwards. The second and third diagrams show what each consistent policy produces: a uniform topology that is easy to read and easy to audit.

Making the policy explicit

The policy must be written down. The conventional location is the repository’s CONTRIBUTING.md file — the file that every contributor reads before opening a PR. The policy should specify:

  1. The default verb for feature branches. “All feature branches are rebased onto the trunk tip before merge” or “All feature branches are merged into the trunk with --no-ff”.
  2. The exception cases. “Release branches are always merged with --no-ff” or “Hotfix branches may be rebased if no signed tag points into them”.
  3. The force-push policy. “Force-pushes are forbidden on protected branches; feature branches may be force-pushed with --force-with-lease before review”.
  4. The configuration that enforces the policy.merge.ff = false is set on main and develop; branch.autosetuprebase = local is set in .gitconfig”.

The policy is not a guideline; it is a contract. Engineers who violate the policy are violating a contract the team agreed to, and the violation is reviewable in the PR review.

Configuration that enforces the policy

The policy is enforced at three levels: the repository’s .git/config (shared via git config and visible in the repo), the team’s .gitconfig template (distributed via onboarding or a setup script), and the server-side branch protection (set on GitHub, GitLab, Gitea).

# Repository-level: enforce --no-ff on protected branches
git config merge.ff false
git config merge.branchdesc true
git config merge.log true

# Team-level: distribute via onboarding
git config --global pull.rebase true
git config --global branch.autosetuprebase local
git config --global push.default simple

# Per-branch: feature branches rebase on pull, protected branches do not
git config branch.feature/iam-rotation.rebase true
git config branch.main.rebase false
git config branch.develop.rebase false

The merge.ff = false setting forces every merge into the configured branch to produce a merge commit. The merge.branchdesc = true setting prefixes the merge commit message with the branch description (from the branch’s last commit or from the PR title). The merge.log = true setting includes the full list of merged commits in the merge commit message. Together, these three settings produce a merge commit whose message is a self-contained audit record: which branch came in, which commits were integrated, which PR was the source.

Migrating an existing repository

A repository with an inconsistent history can be migrated to a consistent policy without rewriting shared history. The migration has three steps:

  1. Adopt the policy going forward. Document the new policy in CONTRIBUTING.md. Configure the repository’s git config and the server-side branch protection. From this commit forward, the policy holds.
  2. Do not rewrite existing history. The existing history is what it is — a mixture of merges and rebases. Rewriting it would invalidate every signed tag, every artifact pin, every downstream fork that references the existing OIDs. The migration is a forward-going commitment, not a retroactive cleanup.
  3. Use the new policy for every new integration. New feature branches follow the new policy. The graph gradually becomes more consistent as new merges (or rebases) overwrite the old pattern.
# Step 1: document the policy
cat >> CONTRIBUTING.md <<'EOF'
## Merge and rebase policy

All feature branches are rebased onto `main` before merge, with the
rebase happening at the moment of merge (not the day before). The
merge into `main` is a fast-forward. Release branches are merged
into `main` with `--no-ff` to record the release event.

Force-pushes are forbidden on `main`, `develop`, and any release
branch. Feature branches may be force-pushed with
`--force-with-lease` before review begins.

The repository's `git config` sets `merge.ff = false` on protected
branches and `branch.autosetuprebase = local` in the team
`.gitconfig` template.
EOF

# Step 2: do not rewrite history — the existing graph is preserved

# Step 3: configure the new policy
git config merge.ff false
git config merge.branchdesc true
git config merge.log true
git config --global branch.autosetuprebase local

# Server-side: enable "Require linear history" on main (GitHub)
# or "Fast-forward merge only" + "Reject non-fast-forward pushes" (GitLab)

Production discipline

  1. Pick one verb as the default; document the exceptions. The choice is between merge and rebase; the exceptions are the release branches and integration branches where the policy is the opposite of the default. Write the policy in CONTRIBUTING.md.
  2. Configure both the advisory policy and the enforcement policy. The advisory policy is git config; the enforcement policy is server-side branch protection. Both are required; neither alone is sufficient.
  3. Do not rewrite existing history when migrating. The existing graph is what it is. The migration is forward-going; the old history stays as it was.
  4. Audit the policy annually. A team that adopted a policy two years ago may have drifted from it as new engineers joined. A yearly audit of the git log --graph of the trunk branch reveals drift.
  5. Treat policy violations as reviewable events. An engineer who fast-forwards when the policy says --no-ff, or who rebases when the policy says merge, is violating a contract. The PR review is the place to catch the violation.

Cross-course references

  • GitOps with Argo CD - Part VI (MergeStrategies) maps the team policy onto GitOps: the GitOps controller’s sync options must match the team’s merge-rebase policy, or the controller will produce a cluster state that does not match the Git history.
  • CI/CD Pipeline Patterns - Part V (MergeQueues) requires a team policy on merge-rebase before the merge queue can be configured: the queue rebases each PR onto the trunk tip, and the team’s policy on merge commits determines whether the final merge is a fast-forward or a --no-ff.
  • Terraform for Production Sysadmins - Part XI (PRWorkflows) requires merge.ff = false on the Terraform main so every applied plan is recorded as a merge commit, giving the audit trail a node in the DAG that links the production state to the change author.

Quiz

Knowledge check · 4 questions

  1. Q1. A team has not adopted an explicit merge-rebase policy. Some engineers rebase before merging, some merge without rebasing, and the trunk history is a mixture of merge bubbles and linear segments. What is the primary cost of this inconsistency?

  2. Q2. Repository-level `git config` settings like `merge.ff = false` are not sufficient to enforce the team's merge-rebase policy, even without server-side branch protection.

  3. Q3. Name the three configuration keys that enforce the merge-rebase policy at the repository level and explain what each one does.

  4. Q4. Diagnose an inconsistent merge-rebase policy in an existing repository and recommend a migration plan that does not rewrite shared history.

    A team has been working on a Terraform repository for two years. The trunk history is a mixture of merge commits (from engineers who merged without rebasing) and linear segments (from engineers who rebased before merging). A post-incident review revealed that several recent integrations cannot be traced back to a PR because the branch event was erased by a rebase. The team needs to adopt a consistent policy going forward without invalidating the existing signed tags, artifact pins, and CI cache keys that reference the existing OIDs.

Passing score: 75%. Answers are checked in this browser.