Skip to main content
RunBook Academy

Git, CI/CD & GitOpsXI · RebasingSafety

Shared history risks — why rewriting pushed commits is dangerous

Advanced⏱ ~22 min🧪 Lab requiredgit

What you'll learn

  • State the golden rule of rebasing and explain why it exists
  • Identify what breaks when pushed commits are rewritten by a force-push
  • Use `git push --force-with-lease` instead of `--force` to catch remote-tip divergence
  • Recognise when a force-push is acceptable (a feature branch owned by one engineer) and when it is not (a shared branch)
  • Diagnose the breakage chain when a force-push has invalidated downstream pins, tags, or attestations

Prerequisites

Practice

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.

Rebasing a branch that has been pushed is one of the most dangerous operations in a collaborative Git workflow. The local-vs-shared boundary from XI-01 is not a guideline; it is the line between a safe rebase and a chain breakage that takes days to repair. The single rule that prevents the breakage is: never rebase commits that have been pushed to a shared branch. The single tool that catches the violation when the rule is broken is git push --force-with-lease, which refuses to clobber a remote tip that has moved since the last fetch. This lesson is the production discipline for the boundary.

The golden rule of rebasing

Never rebase commits that have been pushed to a shared branch.

A “shared branch” is any branch that has at least one downstream consumer — a teammate who has pulled, a CI runner that has built against it, an artifact registry that has pinned to a commit on it, a GitOps controller that tracks it, a signed tag that points into it. The presence of any one of these consumers is enough to make the branch shared.

The reason for the rule is structural. A rebase rewrites OIDs; a force-push replaces the remote’s branch ref from the old tip to the new tip. Downstream consumers that hold the old OIDs see the replacement as divergence: their local view of the branch no longer matches the remote. Recovery requires every downstream consumer to either reset to the new tip (losing any local commits based on the old OIDs) or rebase onto the new tip (which rewrites their local OIDs too).

flowchart LR
    subgraph REMOTE_BEFORE["remote before force-push"]
        RB1["feature tip = A1"]
    end
    subgraph TEAMMATE["teammate local"]
        TB1["feature = A1"]
        TB1 --> TC1["C1 (teammate's commit based on A1)"]
    end
    subgraph REMOTE_AFTER["remote after force-push"]
        RA1["feature tip = A2 (rewritten)"]
    end
    TEAMMATE -.fetches.-> REMOTE_AFTER
    TEAMMATE -.sees divergence.-> RA1

The teammate’s local feature ref points at A1; the remote’s feature ref now points at A2 (the rewritten equivalent). The teammate’s commit C1 was based on A1 and is now based on an unreachable commit. git fetch does not fix this; git fetch is a read-only operation that adds the new A2 to the local object store but does not move the teammate’s feature ref. The teammate’s local branch is now in a state that requires explicit reconciliation.

What --force does

git push --force replaces the remote’s branch ref unconditionally. Whatever the remote had at the tip, the local tip wins. The operation is silent; no warning is given if the remote’s tip has moved since the last fetch.

git push --force origin feature/iam-rotation
# Total 0 (delta 0), reused 0 (delta 0)
# To git@github.com:acme/iac.git
#  + A1...A2 feature/iam-rotation -> forced update

The ”+” in the output indicates a forced update; the old tip A1 has been replaced by A2. The remote’s reflog retains the old tip for the server’s reflog-retention window (typically 30 or 90 days, depending on server configuration), but any client that fetched between the original push and the force-push holds A1 locally and is now divergent.

What --force-with-lease does

git push --force-with-lease checks the remote’s current tip against the local view of the remote’s tip (stored as the remote-tracking ref, e.g. origin/feature/iam-rotation). If the two match, the push proceeds; if they differ, the push is refused.

git push --force-with-lease origin feature/iam-rotation
# To git@github.com:acme/iac.git
#  ! [rejected]        feature/iam-rotation -> feature/iam-rotation (stale info)
# error: failed to push some refs to 'git@github.com:acme/iac.git'

The “stale info” rejection is the safety net: it means the remote’s tip is no longer what the local view expected. The most common cause is that a teammate has pushed a commit to the same branch since the last fetch; the force-push would clobber that commit.

# Refused: teammate pushed a commit between fetch and push
git fetch origin   # updates origin/feature/iam-rotation to teammate's tip
git rebase origin/feature/iam-rotation  # rebases local commits onto teammate's
git push --force-with-lease origin feature/iam-rotation
# (succeeds: local is now based on teammate's tip)

The right recovery from a --force-with-lease rejection is to fetch, rebase (or merge) onto the new remote tip, and try again. The engineer never clobbers the teammate’s commit, because the lease caught it.

The breakage chain when a force-push goes wrong

When a force-push invalidates downstream consumers, the breakage chain has four links:

  1. Teammates’ local branches diverge from the remote. Every teammate who pulled the branch before the force-push holds commits based on the old OIDs. After the force-push, their git fetch produces a non-fast-forward warning; their next git pull requires explicit reconciliation.
  2. CI cache keys are stale. If the CI pipeline’s cache key is the commit OID (a recommended pattern), the cache lookup for the new OIDs is a miss; the pipeline rebuilds from scratch. If the cache key is a branch name, the cache lookup is a hit but the cache contains artifacts built from the old OIDs — a silent corruption that is worse than the miss.
  3. Artifact registry pins are stale. If the artifact registry holds a tag pinned to a commit OID (e.g. a Terraform module version, a container image), the pin now points at an unreachable commit. Downstream consumers that fetch by pin get an error; consumers that fetch by tag name get a different artifact than the one the pin recorded.
  4. Signed tags and attestations are invalid. A signed tag commits to a specific OID; rewriting the OID invalidates the referent. The signature itself remains cryptographically valid against the new OID, but the meaning of the tag — “this object” — is lost. Supply-chain attestations (in-toto, SLSA, Sigstore) that point at the old OIDs are similarly invalidated.

The recovery from a force-push gone wrong is: fetch, identify the old OIDs from the server’s reflog (if still available), reset the branch to one of the old OIDs, force-push to restore the remote to the pre-rewrite state. Every downstream consumer must then re-fetch and re-pin.

When a force-push is acceptable

A force-push is acceptable when the engineer is the only consumer of the branch:

  • The branch is local and has never been pushed. There is no remote to force-push to.
  • The branch has been pushed but is owned by a single engineer (no teammates have pulled, no CI has built, no artifact has been pinned). The engineer can force-push without coordination.
  • The branch has been pushed and the engineer is correcting a mistake that has not been consumed downstream (e.g. a typo in the previous commit’s message, made within the last few minutes and not yet pulled by anyone).

A force-push is not acceptable when the branch has downstream consumers, regardless of whether the engineer thinks the rewrite is “safe”. The decision is about who else holds the OIDs, not about whether the rewrite itself is well-formed.

Recovering from a force-push gone wrong

The recovery procedure when a force-push has invalidated a branch:

# 1. Identify the previous tip from the server's reflog (if available)
git reflog show origin/feature/iam-rotation
# A2 refs/heads/feature/iam-rotation@{0}: forced-update A1 -> A2
# A1 refs/heads/feature/iam-rotation@{1}: update by teammate

# 2. Reset the local branch to the previous tip
git reset --hard A1

# 3. Force-push the restored tip back to the remote
git push --force-with-lease origin feature/iam-rotation
# (succeeds: local matches the previous remote tip)

If the server’s reflog has already expired the previous tip, the recovery is harder: the engineer must coordinate with every downstream consumer to revert their references to a known-good state. For a trunk branch, this is a production incident.

Production discipline

  1. The golden rule: never rebase pushed commits on a shared branch. This is the single rule that prevents the breakage chain. Every other rule in this lesson is a guard rail for the case where the rule is about to be violated.
  2. Use --force-with-lease, never --force. --force-with-lease is a one-character change (--force--force-with-lease) that catches the most common cause of clobbered teammate commits. There is no operational reason to use plain --force.
  3. Fetch before force-pushing. The --force-with-lease check is only as fresh as the engineer’s last fetch. Fetching immediately before force-pushing ensures the local view is current and the lease will catch a recent teammate commit.
  4. Coordinate before force-pushing a branch anyone else has touched. A force-push that breaks a teammate’s local view is a social failure, not a technical one. The fix is a heads-up message before the push, not a recovery procedure afterwards.
  5. Never force-push a trunk branch. Force-pushing main, master, or any other branch that is the upstream of other branches is a production incident. The rule is absolute.
  6. Document the force-push policy in CONTRIBUTING. A team’s policy on which branches can be force-pushed, who must be notified, and what guard rails are required (e.g. signed commits, lease-only pushes) belongs in the repository’s contributing guide, not in individual engineers’ heads.

Cross-course references

  • GitOps with Argo CD - Part VI (MergeStrategies) maps the rebase-versus-force-push trade-off onto GitOps: a GitOps controller reading from a rebase-rewritten branch sees the new OIDs as the desired state; an old OID in a deployed manifest is unreachable, and the controller reports drift that cannot be reconciled without an out-of-band intervention.
  • CI/CD Pipeline Patterns - Part V (MergeQueues) avoids the shared-history problem entirely by rebasing onto a temporary branch in the merge queue, not the contributor’s branch. The temporary branch is force-pushed (or recreated) without affecting the contributor’s branch, and the trunk’s tip moves forward only after a successful build.
  • Terraform for Production Sysadmins - Part XI (PRWorkflows) requires --force-with-lease on Terraform module branches: the module’s consumers pin to specific commits, and a force-push that rewrites those commits is a module-consumption incident.

Quiz

Knowledge check · 4 questions

  1. Q1. What does `git push --force-with-lease` check, and what does it do when the check fails?

  2. Q2. Force-pushing a trunk branch like `main` is not necessarily acceptable just because the engineer uses `--force-with-lease` and the CI pipeline is configured to handle rewrites.

  3. Q3. State the golden rule of rebasing in one sentence and explain why it exists in terms of the local-vs-shared boundary.

  4. Q4. Diagnose the breakage chain from an accidental force-push and recommend the recovery procedure.

    An engineer force-pushes `feature/iam-rotation` with `git push --force` (not `--force-with-lease`) after a rebase. Two teammates have already pulled the branch; the CI pipeline has built against the old tip; the artifact registry holds a Terraform module tag pinned to the old tip OID; and a signed tag points at the old tip. The engineer does not notice the breakage until the next morning, when the artifact registry's pin lookup fails and a teammate reports that their local branch is divergent.

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