Synchronisation de fork avec upstream

Rapprocher la branche master du fork avec celle de l'amont. Détecte la topologie port-only et émet un rapport de couverture ; bascule vers une synchronisation par fusion lorsque les historiques sont connectés.

Spar Skills Guide Bot
DevOpsIntermédiaire
1023/07/2026
Claude Code
#git#fork-sync#upstream-merge#port-only#coverage

Recommandé pour


name: sync-upstream description: Reconcile fork master with Netflix/vmaf master. Detects the fork's port-only topology (no shared merge-base) and emits a coverage report; falls back to merge-based sync when the histories are actually connected.

<!-- markdownlint-disable MD029 MD046 -->

/sync-upstream

Invocation

/sync-upstream [--open-pr]

Background — why pre-flight matters

The fork has been maintained with a port-only strategy: each upstream commit is cherry-picked, rebranded (feat: port upstream X, subject preserved), and squash-merged. That produces commit SHAs on fork master that do not descend from the upstream-master SHAs they originated from. As a result, git merge-base master upstream/master returns empty — the histories are formally unrelated.

Running a bare git merge upstream/master --no-ff in that state requires --allow-unrelated-histories and produces thousands of spurious conflicts (every fork-local file collides with its upstream counterpart by path, regardless of content). The skill must detect this topology before attempting a merge.

Steps

  1. Pre-flight: topology detection.

    git fetch upstream
    mb=$(git merge-base master upstream/master 2>/dev/null) || true
    
    • If mb is empty → port-only topology. Go to step 2a (coverage check).
    • If mb is non-empty → merge-based topology. Go to step 2b (classic merge).

2a. Port-only coverage check. For each upstream commit reachable from upstream/master since the last fork-side port anchor: bash # Derive a reasonable upper bound: the last 50 upstream commits. # Expand as needed; subjects are the match key. git log upstream/master --pretty=format:'%H%x09%s' -50 > /tmp/sync-upstream-candidates.tsv

**Pass 1 — subject-line match (cheap, catches PRs that cite the upstream
SHA in their subject):**
```bash
while IFS=$'\t' read -r sha subj; do
  if git log master --pretty=format:'%s' \
       | grep -Fxq "$subj"; then
    echo "PORTED    $sha  $subj"
  else
    echo "UNPORTED  $sha  $subj"
  fi
done < /tmp/sync-upstream-candidates.tsv
```

**Pass 2 — content-hash similarity for `UNPORTED` rows (catches silent
ports where the same change was made by a fork dev without citing
upstream).** PR #295's 2026-05-02 sync report missed 4 of 6 candidates
that turned out to be already on the fork — Pass 2 catches that class:
```bash
# For each UNPORTED upstream commit, extract added/changed identifiers
# and check if they exist in fork master's HEAD. If yes, it's silently
# ported (the change shipped without an SHA citation in the commit subject).
for sha in $(awk '/^UNPORTED/ {print $2}' /tmp/sync-upstream-pass1.tsv); do
    # Extract added/changed identifiers from the upstream commit
    # (function names, variable names, string literals it INTRODUCES).
    idents=$(git show "$sha" --no-color --pretty=format:'' \
             | grep -E '^\+[^+]' \
             | grep -oE '[a-zA-Z_][a-zA-Z0-9_]{4,}' \
             | sort -u)
    # Skip if no useful identifiers (e.g. doc-only or formatting commit).
    [ -z "$idents" ] && continue
    # Probe fork master for at least 80% of the identifiers.
    n_total=$(echo "$idents" | wc -l)
    n_present=$(echo "$idents" | xargs -I{} sh -c \
        'git grep -lq "{}" -- master 2>/dev/null && echo present' | wc -l)
    ratio=$((n_present * 100 / n_total))
    if [ "$ratio" -ge 80 ]; then
        echo "PORTED-SILENTLY    $sha  ratio=$ratio% n_total=$n_total"
    else
        echo "UNPORTED           $sha  ratio=$ratio%"
    fi
done
```
The 80% threshold is empirical: at `ratio=80%+`, the upstream commit's
semantic content is overwhelmingly already in fork master; at `<50%` the
commit is genuinely missing. The 50–80% band is fuzzy and requires
eyeballing the commit's substance vs the fork's tree.

**Categorise the final output:**
- `PORTED` (Pass 1 hit) — fork commit cites the SHA in its subject.
- `PORTED-SILENTLY` (Pass 2 hit) — semantic content present, no SHA
  citation. Surface in the report so the maintainer can decide whether
  to backfill a citation. NOT a `/port-upstream-commit` candidate.
- `UNPORTED` (neither pass hit) — genuinely missing. Recommend
  `/port-upstream-commit <sha>`.

- If every listed commit is `PORTED` or `PORTED-SILENTLY` → exit with
  `sync-upstream: no action — fork at parity with upstream/master @ <tip-sha>`.
- If any are `UNPORTED` → list them and recommend
  `/port-upstream-commit <sha>` for each. Do NOT attempt a merge. This is
  the expected outcome in port-only mode.

2b. Merge-based sync. Only reached when mb is non-empty: bash git switch -c sync/upstream-$(date +%Y%m%d) master git merge upstream/master --no-ff Conflict policy (matches D16): - Fork wins for: .github/, README.md, CLAUDE.md, AGENTS.md, .claude/, Dockerfile*, core/meson_options.txt, anything under core/src/cuda/, core/src/sycl/, core/src/feature/{cuda,sycl}/, core/src/feature/x86/, core/src/feature/arm64/. - Upstream wins for: feature metric code not touched by the fork — identify by checking git log --follow origin/master -- <path> for any fork commits. - Manual resolution required: core/include/libvmaf/libvmaf.h, core/src/libvmaf.c, core/meson.build, core/tools/cli_parse.cpp, core/tools/vmaf.cpp. For manual conflicts, STOP and surface them with file:line context. Do NOT resolve.

  1. On clean merge (step 2b only): /build-vmaf --backend=cpu, meson test -C build, /cross-backend-diff on the normal Netflix pair.

  2. If --open-pr (step 2b only): gh pr create with title chore(upstream): sync to upstream/master @ <sha> and body including upstream commit count, conflict summary, and test results.

    • In port-only mode (step 2a), --open-pr is a no-op when coverage is complete; when gaps exist, the recommendation is one PR per /port-upstream-commit <sha> invocation, not a single sync PR.

Guardrails

  • Refuses to run if working tree is dirty.
  • Refuses to open a PR if the Netflix CPU golden tests fail after merge.
  • Never git push --force.
  • Pre-flight short-circuit is mandatory — the classic merge step MUST NOT be reached when no merge-base exists, even under operator override, because the --allow-unrelated-histories fallback has been known to corrupt hand-curated port history (see ADR-0028 and the post-mortem thread in docs/rebase-notes.md).
Skills similaires