@@ -67,6 +67,56 @@ func CommitsBetween(ctx context.Context, gitDir, base, head string, max int) ([] |
| 67 | 67 | return parseLogOutput(out) |
| 68 | 68 | } |
| 69 | 69 | |
| 70 | +// UpdateRefCAS performs an atomic compare-and-swap on a ref: only |
| 71 | +// succeeds if the ref currently points at oldOID. Returns |
| 72 | +// ErrRefRaced when the ref moved underneath us (a concurrent push |
| 73 | +// is the canonical case). Used by S27's sync-fork to fast-forward |
| 74 | +// the fork's default branch only when nothing else has touched it. |
| 75 | +// |
| 76 | +// The git update-ref `<oldvalue>` argument is what enforces the CAS; |
| 77 | +// passing it gives git's exact-match semantics (no off-by-one |
| 78 | +// races even when oldvalue happens to equal newvalue). |
| 79 | +func UpdateRefCAS(ctx context.Context, gitDir, ref, newOID, oldOID string) error { |
| 80 | + cmd := exec.CommandContext(ctx, "git", "-C", gitDir, |
| 81 | + "update-ref", ref, newOID, oldOID) |
| 82 | + out, err := cmd.CombinedOutput() |
| 83 | + if err == nil { |
| 84 | + return nil |
| 85 | + } |
| 86 | + // git update-ref's "ref-changed-from-under-us" failure mode is |
| 87 | + // signalled via stderr text rather than a distinct exit code. |
| 88 | + // The two phrasings we care about: "old value is %s, but expected" |
| 89 | + // and "cannot lock ref" — both indicate the CAS lost the race. |
| 90 | + s := string(out) |
| 91 | + if strings.Contains(s, "old value") || strings.Contains(s, "cannot lock ref") { |
| 92 | + return ErrRefRaced |
| 93 | + } |
| 94 | + return fmt.Errorf("update-ref %s %s..%s: %w (%s)", ref, oldOID, newOID, err, strings.TrimSpace(s)) |
| 95 | +} |
| 96 | + |
| 97 | +// ErrRefRaced is the typed sentinel UpdateRefCAS returns when the |
| 98 | +// ref moved between our read and our update. |
| 99 | +var ErrRefRaced = errors.New("repogit: ref moved concurrently") |
| 100 | + |
| 101 | +// FetchIntoNamespace fetches a single ref from `srcRepoDir` into |
| 102 | +// `dstRepoDir` under the supplied refspec. The dst-side ref name is |
| 103 | +// the second half of the refspec. Used by S27 cross-fork PR support |
| 104 | +// to pull a fork's head branch into the base repo's |
| 105 | +// `refs/shithub-pr/<pr_id>/head` namespace (private — never |
| 106 | +// advertised via `info/refs`). |
| 107 | +// |
| 108 | +// Idempotent at the git layer; calling repeatedly with the same |
| 109 | +// refspec just updates the dst ref. |
| 110 | +func FetchIntoNamespace(ctx context.Context, dstRepoDir, srcRepoDir, srcRef, dstRef string) error { |
| 111 | + refspec := srcRef + ":" + dstRef |
| 112 | + cmd := exec.CommandContext(ctx, "git", "-C", dstRepoDir, |
| 113 | + "fetch", "--quiet", "--no-tags", srcRepoDir, refspec) |
| 114 | + if out, err := cmd.CombinedOutput(); err != nil { |
| 115 | + return fmt.Errorf("fetch %s into %s: %w (%s)", srcRef, dstRef, err, strings.TrimSpace(string(out))) |
| 116 | + } |
| 117 | + return nil |
| 118 | +} |
| 119 | + |
| 70 | 120 | // IsAncestor reports whether commit a is an ancestor of commit b. |
| 71 | 121 | // Used by the pre-receive force-push detector: a fast-forward is |
| 72 | 122 | // `IsAncestor(old, new)`. |