* 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
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):
- Fetch branches from both sides — lightweight API calls to get branch names and their latest commit SHAs
- Compare each branch:
- SHAs match → nothing changed, no action needed
- SHAs differ → check if the change is a normal push or a force-push
- 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 aspending-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.
Smart (on-force-push) — Recommended
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.