Skip to content

Repo Reconciliation

Detecting and resolving orphaned repository data when repos move between GitHub accounts.

The problem

When a GitHub repository is deleted and re-created under a different owner (e.g. xjeddah/GitWireElephant-Rock-Lab/GitWire), GitWire ends up with two rows in the repositories table sharing the same repo name but with different github_id values.

This happens because:

  1. GitHub assigns a new github_id when a repo is created
  2. The webhook worker inserts a new row for each installation
  3. The old row stays behind with stale references

Not the same as a transfer

A true GitHub transfer keeps the same github_id — the webhook worker's ON CONFLICT (github_id) DO UPDATE SET full_name = EXCLUDED.full_name handles this automatically. No manual intervention needed.

The orphan case is different: the repo was deleted and re-created, or forked and the original deleted. Different github_id, same name, split data.

Detection

GitWire automatically detects orphaned repos by finding name values that appear more than once in the active repositories table:

sql
SELECT name
FROM repositories
WHERE deleted_at IS NULL
GROUP BY name
HAVING COUNT(*) > 1

For each duplicate, the system determines which variant is "live" (more recent webhook activity) and which is the "orphan."

Resolution options

Two options are available for each orphan:

Merge into live repo

Re-parents all data from the orphan to the live repo:

  1. FK re-parenting — Updates repo_id in 32 tables from orphan.github_id to live.github_id
  2. Text backfill — Updates denormalized repo/repo_full_name columns in 4 tables
  3. Soft-delete — Marks the orphan row with deleted_at = NOW()

All changes happen in a single database transaction — either everything merges or nothing does.

Discard

Soft-deletes the orphan row. Historical data remains in the database but is hidden from queries. The orphan row is marked with deleted_at = NOW().

Use this when the old data is not worth preserving.

Affected tables

FK-linked tables (32)

These tables reference repositories.github_id via repo_id. Data is re-parented during merge:

TableData
ai_review_configAI review settings per repo
ai_reviewsAI code review results
branch_rulesBranch protection rules
ci_runsCI workflow run records
config_history.gitwire.yml change history
config_validation_resultsConfig validation outcomes
decision_logGovernance decision records
dependency_manifestsPackage dependency snapshots
dependency_update_batchesBatch dependency updates
duplicate_signalsDuplicate issue detection data
enforcement_violationsPolicy enforcement records
fix_attemptsIssue fix attempt records
flaky_testsFlaky test detection results
gate_evaluationsQuality gate check results
heal_prsCI healing PR records
issue_embeddingsIssue text embeddings
issuesTracked issues
maintainer_actionsMaintainer automation records
maintainer_settingsPer-repo maintainer config
managed_actionsAction lifecycle records
merge_queue_configMerge queue settings
merge_queue_entriesQueue entries
pipeline_eventsPipeline processing events
policy_repo_configsPolicy config overrides
policy_waiversPolicy waiver records
pull_requestsTracked pull requests
quality_gatesQuality gate definitions
repo_collaboratorsRepository collaborators
repo_configPer-repo GitWire config
rollback_eventsRollback event records
test_resultsCI test results
vulnerability_advisoriesSecurity vulnerability data

Denormalized text columns (4)

These tables store repo_full_name or repo as text. Updated during merge:

TableColumnData
managed_actionsrepo_full_nameAction records
webhook_deliveriesrepoWebhook delivery logs
action_feedrepoActivity feed entries
audit_trail_entriesrepo_full_nameAudit trail

API

Detect orphans

GET /api/repos/reconcile

Returns detected orphan pairs with data counts:

json
{
  "data": [
    {
      "orphan": {
        "github_id": "1240417604",
        "full_name": "xjeddah/GitWire",
        "delivery_count": 131
      },
      "live": {
        "github_id": "1242582513",
        "full_name": "Elephant-Rock-Lab/GitWire",
        "delivery_count": 393
      },
      "data": {
        "fk_tables": { "ci_runs": 32, "issues": 2, "pull_requests": 5 },
        "fk_total": 53,
        "denorm_tables": { "webhook_deliveries": 131, "action_feed": 137 },
        "denorm_total": 274,
        "grand_total": 327
      }
    }
  ]
}

Merge orphan into live repo

POST /api/repos/reconcile/merge
Body: { "orphan": "xjeddah/GitWire", "live": "Elephant-Rock-Lab/GitWire" }

Returns merge results with per-table counts:

json
{
  "status": "merged",
  "orphan": "xjeddah/GitWire",
  "live": "Elephant-Rock-Lab/GitWire",
  "reparented": { "ci_runs": 32, "issues": 2, "pull_requests": 5 },
  "backfilled": { "webhook_deliveries": 131, "action_feed": 137 },
  "total_affected": 327
}

Discard orphan

POST /api/repos/reconcile/discard
Body: { "orphan": "xjeddah/GitWire" }
json
{
  "status": "discarded",
  "orphan": "xjeddah/GitWire",
  "note": "Orphan soft-deleted. Its data remains in DB but is hidden."
}

Dashboard

The Repositories page (/repos) automatically shows a reconciliation banner when orphans are detected:

  • ⚠️ N Orphaned Repos Detected — amber warning banner
  • Per-orphan details: delivery counts, FK-linked tables with row counts
  • Merge into live — re-parents data, soft-deletes orphan
  • Discard — soft-deletes orphan without merging

The banner disappears once all orphans are resolved.

Comparison with competitors

ToolHandles repo account change?
SonarCloudForces re-import, loses history
CodecovBreaks silently
LinearBLoses history
GitWireUser chooses: merge history or fresh start

GitWire is the only tool that offers the user a choice between preserving historical data and starting clean.

See also

Released under the MIT License.