Skip to main content
RunBook Academy

Git, CI/CD & GitOpsXXII · Force PushForcePush

The reflog as safety net — what is recoverable after a force-push

Advanced⏱ ~22 mingit

What you'll learn

  • Distinguish the local reflog from the server-side reflog and what each one covers after a force-push
  • Identify the default 90-day expiry window and the gc boundary that follows it
  • Recover a force-pushed branch from the local reflog when the engineer still has the pre-push clone
  • Recognise when a force-push is irrecoverable (no clone, no server reflog, gc has run)

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.

The previous lessons established that a force-push produces unreachable commits and downstream breakage. This lesson is about the recovery window: the period during which the lost commits are still recoverable, and the boundary at which they become irrecoverable. Two reflogs cover the window: the local reflog on the engineer’s machine, and the server-side reflog on the remote. Together they determine whether the rewrite is reversible.

The local reflog covers the engineer’s own work

The local reflog (refs/heads/$BRANCH and the per-ref reflogs under logs/refs/heads/$BRANCH) records every update to a local branch ref, including updates that came from git reset --hard and updates that came from git push of a force-push. The default expiry for the local reflog is 90 days; entries older than that are pruned on the next git gc.

git reflog show feature/iam-rotation
# a2c1b7f feature/iam-rotation@{0}: commit: rebase onto origin/main
# d3e4f5a feature/iam-rotation@{1}: commit: amend commit message
# 7a8b9c0 feature/iam-rotation@{2}: branch: Created from origin/main

The local reflog is the first place to look when an engineer needs to recover from their own mistake: a bad rebase, a bad amend, a force-push that rewrote too much. If the engineer’s local clone still has the pre-push branch ref, the recovery is trivial:

# Identify the pre-push tip from the local reflog
git reflog show feature/iam-rotation
# a2c1b7f feature/iam-rotation@{0}: commit: rebase onto origin/main  <- new tip
# d3e4f5a feature/iam-rotation@{1}: commit: amend commit message      <- lost tip

# Reset to the pre-push tip
git reset --hard d3e4f5a

# Force-push the restored tip back to the remote
git push --force-with-lease origin feature/iam-rotation

The local reflog covers the engineer’s own clone only. If the force-push broke a teammate’s clone, the teammate needs their own local reflog (which they have, assuming they had the branch checked out before the force-push) or the server-side reflog.

The server-side reflog covers the team’s work

The server-side reflog is maintained by the Git server (GitHub, GitLab, a self-hosted Gitea). It records every update to the remote’s branch refs, including force-pushes. The retention window varies by host:

  • GitHub. Repository-level reflog is not exposed by default; the closest equivalent is the Events API, which retains events for 90 days for free accounts and longer for enterprise plans. For force-push recovery specifically, GitHub retains the forced-update event in the Events API for the same window.
  • GitLab. Repository reflog is exposed via the API; retention follows the instance’s git_reflog_expiry setting, typically 30 or 90 days.
  • Self-hosted Gitea / bare Git. Retention is whatever the server is configured with; the default is gc.reflogExpire = 90 days and gc.reflogExpireUnreachable = 30 days.
# Read the server-side reflog via the GitHub Events API
gh api /repos/acme/iac/events --jq '.[] | select(.type=="PushEvent") | .payload.commits[]'
# ... or via the git CLI if the server exposes the reflog:
git reflog show origin/feature/iam-rotation
# a2c1b7f origin/feature/iam-rotation@{0}: forced-update d3e4f5a -> a2c1b7f
# d3e4f5a origin/feature/iam-rotation@{1}: update by teammate

The server-side reflog is the safety net for the team as a whole: if no local clone has the pre-push tip, the server-side reflog is the next place to look.

flowchart LR
    PUSH["force-push at T0"] --> L["local reflog retains old tip until T0+90d"]
    PUSH --> S["server reflog retains old tip until T0+server-window"]
    PUSH --> OBJ["object store retains commits until gc prune"]
    L --> W1["local window expires"]
    S --> W2["server window expires"]
    OBJ --> W3["gc prune expires"]
    W1 --> GONE["commits only recoverable from object store if no gc has run"]
    W2 --> GONE
    W3 --> IRREV["irrecoverable: commits deleted from object store"]

The 90-day default and the gc boundary

The default expiry for the local reflog is 90 days; this is set by gc.reflogExpire = 90 days in the Git configuration. The default for unreachable reflog entries is 30 days (gc.reflogExpireUnreachable = 30 days). After expiry, the reflog entries are deleted; the commits they pointed at become unreachable from the reflog and are then subject to the standard gc prune window.

The prune window (gc.pruneExpire) is typically 2 weeks for hosted services and longer for self-hosted instances. After the prune window expires and gc has run, the unreachable objects are deleted from the object store; the rewrite is then irrecoverable.

# Inspect the local reflog expiry configuration
git config --get gc.reflogExpire
# 90 days
git config --get gc.reflogExpireUnreachable
# 30 days
git config --get gc.pruneExpire
# 2 weeks

The combined window from force-push to irrecoverability is: the reflog window (90 days for reachable entries, 30 days for unreachable) plus the prune window (2 weeks). After roughly 90 days plus 2 weeks, an unrecovered force-push is irrecoverable.

--force-if-includes as a complementary guard rail

Git 2.30 added --force-if-includes, which adds a second check on top of --force-with-lease. The check verifies that the remote’s current tip is reachable from the local branch’s tip, i.e. that the local branch has actually incorporated the remote’s commits. The combined command:

git push --force-with-lease --force-if-includes origin feature/iam-rotation

The --force-if-includes check catches a different case than --force-with-lease. The lease catches “remote moved since last fetch”; the --force-if-includes check catches “remote moved and you did not incorporate the new commits into your local branch”. Together they form the strongest combination of client-side checks for safe force-pushing.

Production discipline

  1. Know your reflog window. git config --get gc.reflogExpire on every engineer’s machine; the server-side window is whatever the host is configured with. The recovery plan depends on both.
  2. Treat the first 90 days after a force-push as the recovery window. If a force-push has broken downstream consumers, the recovery must happen inside the window, not “eventually”.
  3. Use --force-if-includes on top of --force-with-lease for the strongest client-side check. Git 2.30+ supports the combination; together they catch both tip divergence and not-yet-incorporated remote commits.
  4. Push to a fresh clone as a backup. Before any force-push of a branch with downstream consumers, push the pre-push tip to a fresh clone (or a tag, or a backup branch) so that the pre-push state is recoverable even if the server-side reflog has expired.
  5. Test the recovery procedure before the incident. The first run of git reflog show $BRANCH and git reset --hard $OLD_TIP should happen in a drill, not during the incident.

Cross-course references

  • Git, CI/CD & GitOps - Part XVII (Reflog) covers the reflog mechanism in detail; this lesson is the force-push recovery application of that mechanism.
  • Git, CI/CD & GitOps - Part XI (Rebasing) lesson 06 discusses the recovery from a bad rebase, which uses the same reflog procedure.
  • Git, CI/CD & GitOps - Part XVIII (Recovery) lesson 05 covers the recovery from a shared rebase, which is the combined procedure when both the local and server reflogs are needed.

Quiz

Knowledge check · 4 questions

  1. Q1. What is the default retention window for the local reflog, and what is the next thing that happens after it expires?

  2. Q2. The server-side reflog is not guaranteed to retain every ref update for the same 90 days as the local reflog.

  3. Q3. What is the difference between `--force-with-lease` and `--force-if-includes`, and why use them together?

  4. Q4. Plan the recovery procedure for a force-push that broke a teammate's clone, given the local and server reflog windows.

    An engineer force-pushed `feature/iam-rotation` with plain `--force` two hours ago. A teammate has pulled the branch and has local commits based on the old tip. The team uses GitHub for the remote and the default 90-day reflog on local clones. The engineer wants to recover the pre-push tip and restore the branch.

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