Files
gitea-mirror/docs/FORCE_PUSH_PROTECTION.md
T
ARUNAVO RAYandGitHub 882e147504 Redesign dashboard and configuration screens (#353)
* feat(ui): redesign dashboard and configuration screens

New settings design language built from the design/giteamirror.pen file:
cards with icon headers and status footers, header-level enable switches,
toggle switches instead of checkboxes, uppercase section titles, selection
tiles with icon chips and a check on the active option, segmented controls,
and an indigo accent. Implemented via shared primitives in
src/components/config/settings-ui.tsx and applied across:

- Automation: header switches, schedule card, one-line auto-mirror copy
  with info tooltip, full-width Repository Cleanup card with Skip/Archive/
  Delete tiles and dry run row
- Notifications: segmented provider picker (ntfy/Apprise/Gotify/Webhook),
  events card with per-event switches
- Connections: GitHub/Gitea connection cards with token creation guide and
  field helpers, Repository Selection and Mirror Content cards covering
  every mirror option, Organization Structure card with strategy tiles,
  destructive update protection tiles (BETA label removed)
- Authentication: sign-in methods status card, identity providers restyle
- Dashboard: flatter stat cards, icon panel headers, indigo view-all links

All existing state handling, autosave and API behavior is unchanged.
Light mode keeps working via theme tokens. README and website screenshots
regenerated, docs references to renamed cards updated.

* fix(ui): design polish pass from local review

- Recent Activity rows get status icon circles (check, sync, sparkles, alert)
- Connections tab restructured: connection cards share a stretched grid row
  so GitHub and Gitea stay equal height; Mirror Content moved to the right
  column; forms split into placeable cards via a part prop
- Token guide panel: link moved to header as icon, larger text; redundant
  card footers removed (scopes line, test-connection hint)
- Repository Selection gains a footer note; retention explanation moved
  below the selector
- Authentication tab matches the design: side-by-side cards, row dividers,
  disabled state-reflecting switches with info hints, footers; SSO dialog
  restyled (segmented protocol tabs, field labels, indigo primary)
- Import GitHub Data button is the indigo primary; disabled state is muted
- Time format menu redesigned (locale pill, live examples, live clock in
  the trigger); theme switcher moved to sidebar as icon segmented control,
  system preference now persists correctly; legacy ModeToggle removed
- Automation timezone pill no longer shows stored legacy UTC as a choice
- Config tab bar wraps 2x2 on narrow screens instead of overflowing
- README, website and PR screenshots regenerated
2026-08-03 11:27:45 +05:30

8.6 KiB

Force-Push Protection

This document describes the smart force-push protection system introduced in gitea-mirror v3.11.0+.

The Problem

GitHub repositories can be force-pushed at any time — rewriting history, deleting branches, or replacing commits entirely. When gitea-mirror syncs a force-pushed repo, the old history in Gitea is silently overwritten. Files, commits, and branches disappear with no way to recover them.

The original workaround (backupBeforeSync: true) created a full git bundle backup before every sync. This doesn't scale — a user with 100+ GiB of mirrors would need up to 2 TB of backup storage with default retention settings, even though force-pushes are rare.

Solution: Smart Detection

Instead of backing up everything every time, the system detects force-pushes before they happen and only acts when needed.

How Detection Works

Before each sync, the app compares branch SHAs between Gitea (the mirror) and GitHub (the source):

  1. Fetch branches from both sides — lightweight API calls to get branch names and their latest commit SHAs
  2. Compare each branch:
    • SHAs match → nothing changed, no action needed
    • SHAs differ → check if the change is a normal push or a force-push
  3. Ancestry check — for branches with different SHAs, call GitHub's compare API to determine if the new SHA is a descendant of the old one:
    • Fast-forward (new SHA descends from old) → normal push, safe to sync
    • Diverged (histories split) → force-push detected
    • 404 (old SHA doesn't exist on GitHub anymore) → history was rewritten, force-push detected
    • Branch deleted on GitHub → flagged as destructive change

What Happens on Detection

Depends on the configured strategy (see below):

  • Backup strategies (always, on-force-push): create a git bundle snapshot, then sync
  • Block strategy (block-on-force-push): halt the sync, mark the repo as pending-approval, wait for user action

Fail-Open Design

If detection itself fails (GitHub rate limits, network errors, API outages), sync proceeds normally. Detection never blocks a sync due to its own failure. Individual branch check failures are skipped — one flaky branch doesn't affect the others.

Backup Strategies

Configure via Configuration → Connections → Destructive Update Protection.

Strategy What It Does Storage Cost Best For
Disabled No detection, no backups Zero Repos you don't care about losing
Always Backup Snapshot before every sync (original behavior) High Small mirror sets, maximum safety
Smart (default) Detect force-pushes, backup only when found Near-zero normally Most users — efficient protection
Block & Approve Detect force-pushes, block sync until approved Zero Critical repos needing manual review

Strategy Details

Disabled

Syncs proceed without any detection or backup. If a force-push happens on GitHub, the mirror silently overwrites.

Always Backup

Creates a git bundle snapshot before every sync regardless of whether a force-push occurred. This is the legacy behavior (equivalent to the old backupBeforeSync: true). Safe but expensive for large mirror sets.

Runs the force-push detection before each sync. On normal days (no force-pushes), syncs proceed without any backup overhead. When a force-push is detected, a snapshot is created before the sync runs.

This gives you protection when it matters with near-zero cost when it doesn't.

Block & Approve (block-on-force-push)

Runs detection and, when a force-push is found, blocks the sync entirely. The repository is marked as pending-approval and excluded from future scheduled syncs until you take action:

  • Approve: creates a backup first, then syncs (safe)
  • Dismiss: clears the flag and resumes normal syncing (no backup)

Use this for repos where you want manual control over destructive changes.

Additional Settings

These appear when any non-disabled strategy is selected:

Snapshot Retention Count

How many backup snapshots to keep per repository. Oldest snapshots are deleted when this limit is exceeded. Default: 5.

Snapshot Retention Days

Maximum age (in days) for backup snapshots. Bundles older than this are deleted during retention enforcement, though at least one bundle is always kept. Set to 0 to disable time-based retention. Default: 30.

Snapshot Directory

Where git bundle backups are stored. Default: data/repo-backups. Bundles are organized as <directory>/<owner>/<repo>/<timestamp>.bundle.

Block Sync on Snapshot Failure

Available for Always Backup and Smart strategies. When enabled, if the snapshot creation fails (disk full, permissions error, etc.), the sync is also blocked. When disabled, sync continues even if the snapshot couldn't be created.

Recommended: enabled if you rely on backups for recovery.

Backward Compatibility

The old backupBeforeSync boolean is still recognized:

Old Setting New Equivalent
backupBeforeSync: true backupStrategy: "on-force-push"
backupBeforeSync: false backupStrategy: "disabled"
Neither set backupStrategy: "on-force-push" (new default)

Existing configurations are automatically mapped. The old field is deprecated but will continue to work.

Environment Variables

No new environment variables are required. The backup strategy is configured through the web UI and stored in the database alongside other config.

API

Approve/Dismiss Blocked Repos

When using the block-on-force-push strategy, repos that are blocked can be managed via the API:

# Approve sync (creates backup first, then syncs)
curl -X POST http://localhost:4321/api/job/approve-sync \
  -H "Content-Type: application/json" \
  -H "Cookie: <session>" \
  -d '{"repositoryIds": ["<id>"], "action": "approve"}'

# Dismiss (clear the block, resume normal syncing)
curl -X POST http://localhost:4321/api/job/approve-sync \
  -H "Content-Type: application/json" \
  -H "Cookie: <session>" \
  -d '{"repositoryIds": ["<id>"], "action": "dismiss"}'

Blocked repos also show an Approve / Dismiss button in the repository table UI.

Architecture

Key Files

File Purpose
src/lib/utils/force-push-detection.ts Core detection: fetch branches, compare SHAs, check ancestry
src/lib/repo-backup.ts Strategy resolver, backup decision logic, bundle creation
src/lib/gitea-enhanced.ts Sync flow integration (calls detection + backup before mirror-sync)
src/pages/api/job/approve-sync.ts Approve/dismiss API endpoint
src/components/config/GitHubConfigForm.tsx Strategy selector UI
src/components/repositories/RepositoryTable.tsx Pending-approval badge + action buttons

Detection Flow

syncGiteaRepoEnhanced()
  │
  ├─ Resolve backup strategy (config → backupStrategy → backupBeforeSync → default)
  │
  ├─ If strategy needs detection ("on-force-push" or "block-on-force-push"):
  │    │
  │    ├─ fetchGiteaBranches() — GET /api/v1/repos/{owner}/{repo}/branches
  │    ├─ fetchGitHubBranches() — octokit.paginate(repos.listBranches)
  │    │
  │    └─ For each Gitea branch where SHA differs:
  │         └─ checkAncestry() — octokit.repos.compareCommits()
  │              ├─ "ahead" or "identical" → fast-forward (safe)
  │              ├─ "diverged" or "behind" → force-push detected
  │              └─ 404/422 → old SHA gone → force-push detected
  │
  ├─ If "block-on-force-push" + detected:
  │    └─ Set repo status to "pending-approval", return early
  │
  ├─ If backup needed (always, or on-force-push + detected):
  │    └─ Create git bundle snapshot
  │
  └─ Proceed to mirror-sync

Troubleshooting

Repos stuck in "pending-approval": Use the Approve or Dismiss buttons in the repository table, or call the approve-sync API endpoint.

Detection always skipped: Check the activity log for skip reasons. Common causes: Gitea repo not yet mirrored (first sync), GitHub API rate limits, network errors. All are fail-open by design.

Backups consuming too much space: Lower the retention count, or switch from "Always Backup" to "Smart" which only creates backups on actual force-pushes.

False positives: The detection compares branch-by-branch. A rebase (which is a force-push) will correctly trigger detection. If you routinely rebase branches, consider using "Smart" instead of "Block & Approve" to avoid constant approval prompts.