Skip to main content
RunBook Academy

Git, CI/CD & GitOpsXVIII · Git RecoveryRecovery

Recovering a commit after a shared rebase — the original is still in your local reflog

Advanced⏱ ~26 mingit

What you'll learn

  • Apply the reflog recipe to recover a commit that has been force-pushed away by a teammate's rebase
  • Identify the original OID at `HEAD@{1}` (or the appropriate reflog entry) as the recovery target even after the remote has been rewritten
  • Recognise the difference between the local reflog (preserved) and the remote ref (rewritten) and why the local reflog is the recovery path
  • Distinguish `git cherry-pick <oid>` (replay the commit on the new tip) from `git branch <name> <oid>` (preserve the commit as a branch for review)

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 shared rebase is the most dangerous recovery scenario because the accident is committed by a teammate, not by the engineer who discovers the loss. The engineer fetches the remote, sees the team’s branch has been rewritten by a teammate’s rebase, and discovers that the commit they were working on is no longer reachable from the remote ref. The commit is not lost: it is in the local clone’s reflog, where it has been since the engineer first created it. The reflog is local to the clone and is not affected by the remote’s rewrite.

The recovery is mechanical: read the local reflog, identify the OID, replay the commit on the new remote tip or preserve it as a branch. The discipline is to never rebase a shared branch without coordinating with the team, and to never force-push without --force-with-lease.

What the shared rebase does to the local clone

The shared rebase is a rebase performed on a branch that other team members have also fetched. The teammate rebases their local commits onto the new tip of the parent branch, then force-pushes the rewritten branch to the remote. The remote ref is rewound to the new tip; the original commits are no longer reachable from the remote ref.

flowchart LR
    A["local commit (8a3f9d2)"] --> B["fetched by engineer"]
    B --> C["local reflog: 8a3f9d2 reachable"]
    C --> D["teammate rebases and pushes"]
    D --> E["remote ref rewound to (a1b2c3d)"]
    D --> F["original (8a3f9d2) no longer on remote"]
    F -. "engineer fetches" .-> G["engineer sees non-fast-forward"]
    G --> H["read local reflog"]
    H --> I["8a3f9d2 still at HEAD@{1}"]
    I --> J["git cherry-pick 8a3f9d2"]
    J --> K["commit replayed on new remote tip"]

The fork in the diagram is the recovery path. The original commit is preserved in the local reflog because the reflog is local to the clone and is not affected by the remote’s rewrite. The recovery is to read the local reflog, identify the OID, and replay the commit on the new tip.

The recovery recipe

# Step 1: identify the non-fast-forward and stop
git fetch origin feature/iam-rotation
# From https://github.com/team/repo
#  * branch            feature/iam-rotation -> FETCH_HEAD
#    a1b2c3d..3e4f5a6  feature/iam-rotation -> origin/feature/iam-rotation
# ! [rejected]        feature/iam-rotation -> feature/iam-rotation (non-fast-forward)

# Step 2: read the local reflog for the original commit
git reflog -10
# 3e4f5a6 HEAD@{0}: commit: rotate IAM credentials (teammate's rebase)
# a1b2c3d HEAD@{1}: commit: bump ansible collection version
# 8a3f9d2 HEAD@{2}: commit: rotate IAM credentials              <-- the original
# 7e8f9a0 HEAD@{3}: commit: bump ansible collection version

# Step 3: copy the OID to a durable location
ORIGINAL_OID=$(git reflog -1 HEAD@{2} | awk '{print $1}')
echo "Original commit: $ORIGINAL_OID"

# Step 4: verify the OID is the right one
git show --stat $ORIGINAL_OID
# confirms the original commit before any further action

# Step 5: replay on the new remote tip
git fetch origin feature/iam-rotation
git checkout feature/iam-rotation
git reset --hard origin/feature/iam-rotation
git cherry-pick $ORIGINAL_OID
# or
git branch feature/iam-rotation-recovery $ORIGINAL_OID

The recipe has five steps because the recovery requires fetching the new remote state, resetting the local branch to the new remote tip, and then replaying the original commit. The reset is destructive of the local branch’s pre-fetch state; the safety net is the reflog entry at HEAD@{1} (before the reset) and the OID at HEAD@{2} (the original commit).

The local reflog is the recovery path

The crucial property of the shared rebase recovery is that the local reflog is unaffected by the remote’s rewrite. The reflog is a file in .git/logs/HEAD (or .git/logs/refs/heads/&lt;branch&gt;) that records every update to the local ref. The remote does not write to the local reflog; the local clone writes to the local reflog when it fetches. The fetch updates the remote-tracking ref (origin/feature/iam-rotation) and records the previous value in the reflog of HEAD, but the local branch (feature/iam-rotation) is not updated until the engineer merges or resets.

flowchart LR
    A["local clone"] --> B["local reflog: per-ref"]
    B --> C["HEAD: every HEAD update"]
    B --> D["refs/heads/&lt;branch&gt;: branch updates"]
    B --> E["refs/remotes/origin/&lt;branch&gt;: remote updates"]
    A --> F["remote: rewrite"]
    F --> G["remote ref rewound"]
    A -. "fetch" .-> H["origin/&lt;branch&gt; ref updated"]
    H --> E
    E --> I["local &lt;branch&gt; NOT updated"]
    I --> J["local &lt;branch&gt; still at original OID"]
    J --> K["recovery: read local reflog"]

The diagram shows the isolation between the local reflog and the remote ref. The remote’s rewrite is independent of the local reflog; the local reflog retains the original OID for the 90-day default window. The recovery is to read the local reflog and replay the commit on the new remote tip.

The —force-with-lease discipline

The shared rebase is preventable. The discipline is for the teammate who performs the rebase to use --force-with-lease when force-pushing, and for the team to have a policy that shared branches are not rebased without coordination.

# The unsafe form: force-pushes regardless of concurrent updates
git push --force origin feature/iam-rotation

# The safe form: refuses to push if the remote has been concurrently updated
git push --force-with-lease origin feature/iam-rotation

The --force-with-lease form verifies that the remote has not been concurrently updated since the local fetch. If the remote has been updated, the push fails. The verification is the safety net against the shared rebase problem: if the teammate’s rebase was intended to be the only update, --force-with-lease succeeds; if another teammate has pushed in the meantime, --force-with-lease fails and the rebase is forced to reconcile with the concurrent update.

The case where the engineer wants the original back

The recovery recipe is the same regardless of whether the engineer wants the original commit as a branch or replayed on the new tip. The branch form is preferred when the engineer wants to compare the original commit with the teammate’s rebased version; the cherry-pick form is preferred when the engineer wants to fold the original commit back into the working branch.

# Form 1: preserve the original as a branch for review
git branch feature/iam-rotation-recovery $ORIGINAL_OID
git diff feature/iam-rotation feature/iam-rotation-recovery
# shows the differences between the teammate's rebase and the original

# Form 2: replay the original on the new remote tip
git reset --hard origin/feature/iam-rotation
git cherry-pick $ORIGINAL_OID
# the commit is replayed on the new tip; the original is preserved as a dangling commit

The two forms are not mutually exclusive. The engineer can preserve the original as a branch and then replay the commit on the new tip; the branch is the durable reference, the replay is the working state.

Production discipline

  1. Never rebase a shared branch without coordinating with the team. The rebase is recoverable from the local reflog but the recovery is a coordination problem, not a Git problem. The discipline is a Slack message, a ticket update, or a team-meeting announcement.
  2. Always use --force-with-lease when force-pushing. The --force-with-lease form verifies that the remote has not been concurrently updated; the plain --force form does not. The lease check is the safety net against the shared rebase.
  3. Always copy the OID to a durable location before the reset. The OID at HEAD@{1} (or the appropriate reflog entry) before the reset is the recovery target; copying it to a PR comment means the recovery is possible even if the reflog expires.
  4. Document the recovery in the runbook. The recipe above is a one-page runbook entry; paste it into the team’s on-call documentation.

Cross-course references

  • Git, CI/CD & GitOps — Part XIII (Rebase) — the rebase mechanics; the prerequisite for this lesson.
  • Git, CI/CD & GitOps — Part XVII (Reflog) — the reflog’s location and scope; the prerequisite for the reading step.
  • GitOps with Argo CD — Part VI (MergeStrategies) — the GitOps controller’s recovery from a force-pushed rebase; the controller’s clone is configured with gc.reflogExpire never so the recovery window is effectively infinite.

Quiz

Knowledge check · 4 questions

  1. Q1. An engineer on `feature/iam-rotation` fetches the remote and sees a non-fast-forward warning. The branch was rebased by a teammate. What is the precise sequence to recover the original commit?

  2. Q2. When a teammate force-pushes a rebase to the shared remote, the original commit is no longer reachable from the remote ref but is still reachable from the engineer's local reflog for the 90-day default retention window.

  3. Q3. What is the difference between `--force` and `--force-with-lease` when force-pushing a rebase, and why does `--force-with-lease` prevent the shared rebase problem?

  4. Q4. Walk the recovery for an engineer whose teammate has force-pushed a rebase to a shared branch and the engineer discovers the loss on the next fetch.

    An engineer on `feature/iam-rotation` fetches the remote and sees a non-fast-forward warning. The branch was rebased by a teammate the previous day; the teammate force-pushed without coordinating with the team. The original commit was the engineer's contribution to the IAM rotation feature. The engineer needs to recover the original commit and replay it on the new remote tip.

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