Git, CI/CD & GitOpsXXII · Force PushForcePush
The reflog as safety net — what is recoverable after a force-push
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
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-updateevent in the Events API for the same window. - GitLab. Repository reflog is exposed via the API; retention
follows the instance’s
git_reflog_expirysetting, typically 30 or 90 days. - Self-hosted Gitea / bare Git. Retention is whatever the
server is configured with; the default is
gc.reflogExpire = 90 daysandgc.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
- Know your reflog window.
git config --get gc.reflogExpireon every engineer’s machine; the server-side window is whatever the host is configured with. The recovery plan depends on both. - 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”.
- Use
--force-if-includeson top of--force-with-leasefor the strongest client-side check. Git 2.30+ supports the combination; together they catch both tip divergence and not-yet-incorporated remote commits. - 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.
- Test the recovery procedure before the incident. The
first run of
git reflog show $BRANCHandgit reset --hard $OLD_TIPshould 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
Q1. What is the default retention window for the local reflog, and what is the next thing that happens after it expires?
Q2. The server-side reflog is not guaranteed to retain every ref update for the same 90 days as the local reflog.
Q3. What is the difference between `--force-with-lease` and `--force-if-includes`, and why use them together?
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.