tenseleyflow/shithub / 0544eb7

Browse files

S27: repogit.UpdateRefCAS + FetchIntoNamespace primitives

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
0544eb7b1102352db3539b3fecc02857f1084b7f
Parents
d8b70e1
Tree
6625539

1 changed file

StatusFile+-
M internal/repos/git/branchops.go 50 0
internal/repos/git/branchops.gomodified
@@ -67,6 +67,56 @@ func CommitsBetween(ctx context.Context, gitDir, base, head string, max int) ([]
6767
 	return parseLogOutput(out)
6868
 }
6969
 
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
+
70120
 // IsAncestor reports whether commit a is an ancestor of commit b.
71121
 // Used by the pre-receive force-push detector: a fast-forward is
72122
 // `IsAncestor(old, new)`.