@agentsdance/codejury 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -8
- package/bin/jury.js +12 -3
- package/lib/repository.js +22 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,10 +23,14 @@ the target repository; the caller's working tree is not switched or modified.
|
|
|
23
23
|
|
|
24
24
|
GitLab and compatible self-hosted services are supported through
|
|
25
25
|
`/merge_requests/<id>` URLs. Jury clones with Git and fetches the standard
|
|
26
|
-
`refs/merge-requests/<id>/head` ref, so existing Git credentials are used.
|
|
26
|
+
`refs/merge-requests/<id>/head` ref, so existing Git credentials are used.
|
|
27
|
+
When that ref is missing, Jury discovers `refs/merge-requests/<shard>/<id>/<revision>`
|
|
28
|
+
refs and fetches the highest numeric revision, verifying the advertised commit.
|
|
29
|
+
The shard is discovered independently of the MR number. If
|
|
27
30
|
the source branch is not uniquely visible on the target remote (commonly a
|
|
28
31
|
fork), use `--no-push` for a read-only review or check out the source branch
|
|
29
|
-
locally and
|
|
32
|
+
locally, omit the MR URL, and run
|
|
33
|
+
`jury --dir /path/to/checkout --trunk <target-branch>`.
|
|
30
34
|
|
|
31
35
|
Node 20 or newer. The reviewers are separate CLIs you install yourself — `jury` spawns whatever you
|
|
32
36
|
have and skips the rest:
|
|
@@ -43,14 +47,17 @@ The former `cr` command remains available as a compatibility alias.
|
|
|
43
47
|
|
|
44
48
|
## Working and state directory
|
|
45
49
|
|
|
46
|
-
With
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
With an MR/PR URL, Jury always resolves the URL into an isolated checkout. By default,
|
|
51
|
+
checkouts live under `~/.jury/checkouts/` and run records under `~/.jury/runs/`, regardless
|
|
52
|
+
of the current directory. `--dir <path>` instead uses `<path>/checkouts/` and `<path>/runs/`.
|
|
53
|
+
The integrated `--web` console reads records from the same root.
|
|
54
|
+
|
|
55
|
+
Without a URL, Jury reviews the current repository, or the repository selected by `--dir`.
|
|
56
|
+
Other commands keep the current directory when it is a Git worktree and otherwise use
|
|
57
|
+
`~/.jury`. An explicitly empty `--dir ""` selects `~/.jury`. `~` is expanded consistently.
|
|
50
58
|
|
|
51
59
|
Run records live in `<dir>/runs`. For example, `jury runs --dir ""` and `jury web --dir ""` read
|
|
52
|
-
`~/.jury/runs
|
|
53
|
-
`runs` directory.
|
|
60
|
+
`~/.jury/runs`. Use those commands to inspect URL-based reviews launched with the default root.
|
|
54
61
|
|
|
55
62
|
## The loop
|
|
56
63
|
|
package/bin/jury.js
CHANGED
|
@@ -533,9 +533,18 @@ async function cmdAgent(argv) {
|
|
|
533
533
|
}
|
|
534
534
|
|
|
535
535
|
values.push = values.push && !values["no-push"];
|
|
536
|
-
const requestedWorktree = await commandDirectory(values.dir);
|
|
536
|
+
const requestedWorktree = await commandDirectory(values.pr ? (values.dir ?? "") : values.dir);
|
|
537
|
+
// Two different questions, and conflating them cost the local path entirely.
|
|
538
|
+
// `root` is where the isolated checkout and run records are written; `dir` is
|
|
539
|
+
// the repository to resolve the request FROM. A caller sitting in the very
|
|
540
|
+
// repository under review already has its commits, and asking them for the
|
|
541
|
+
// one ref the host may have pruned — rather than reading what is on disk —
|
|
542
|
+
// is what issue #30 was.
|
|
543
|
+
const resolveFrom = values.dir ? path.resolve(values.dir) : process.cwd();
|
|
537
544
|
const resolved = values.pr
|
|
538
|
-
? await resolvePrCheckout(values.pr, {
|
|
545
|
+
? await resolvePrCheckout(values.pr, {
|
|
546
|
+
allowPush: values.push, root: requestedWorktree, dir: resolveFrom,
|
|
547
|
+
})
|
|
539
548
|
: null;
|
|
540
549
|
const worktree = resolved?.worktree ?? requestedWorktree;
|
|
541
550
|
resolvedCheckoutCleanup = resolved?.cleanup ?? null;
|
|
@@ -632,7 +641,7 @@ async function cmdAgent(argv) {
|
|
|
632
641
|
// directory and nothing else: the server re-reads it per request and tails
|
|
633
642
|
// the event log, so it sees each round land without the loop telling it.
|
|
634
643
|
if (values.web) {
|
|
635
|
-
const { url } = await serve({ port: Number(values.port), onLog: (m) => console.log(st.field("console", st.muted(m))) });
|
|
644
|
+
const { url } = await serve({ port: Number(values.port), cwd: requestedWorktree, onLog: (m) => console.log(st.field("console", st.muted(m))) });
|
|
636
645
|
liveConsole = url;
|
|
637
646
|
console.log(st.field("console", `${url}${st.muted(" → the conversation streams live")}`));
|
|
638
647
|
openBrowser(url);
|
package/lib/repository.js
CHANGED
|
@@ -122,7 +122,7 @@ class MergeRequestCheckoutError extends Error {}
|
|
|
122
122
|
*
|
|
123
123
|
* Not every GitLab-compatible host publishes refs/merge-requests/<id>/head.
|
|
124
124
|
* Some expose the individual revisions instead, as
|
|
125
|
-
* refs/merge-requests/<
|
|
125
|
+
* refs/merge-requests/<shard>/<id>/<revision>, where the highest revision is the
|
|
126
126
|
* current head. Returns the advertised sha when that fallback was used, so the
|
|
127
127
|
* caller can check the fetched commit against what the remote said: a request
|
|
128
128
|
* updated mid-fetch would otherwise be reviewed as though it were the head.
|
|
@@ -137,18 +137,23 @@ async function fetchMergeRequestHead(exec, worktree, number) {
|
|
|
137
137
|
// transport failure keeps its own error, or every network problem would be
|
|
138
138
|
// reported as an unusual ref layout.
|
|
139
139
|
if (!/couldn't find remote ref/i.test(String(err.stderr ?? "") + String(err.message))) throw err;
|
|
140
|
-
|
|
141
|
-
|
|
140
|
+
// The shard can differ from the MR id (and can contain leading zeroes).
|
|
141
|
+
// Discover it rather than assuming a particular host's sharding scheme.
|
|
142
|
+
const { stdout } = await exec("git", ["ls-remote", "--refs", "origin", `refs/merge-requests/*/${number}/*`], options);
|
|
142
143
|
const revisions = String(stdout).split("\n").flatMap((line) => {
|
|
143
144
|
const [sha, ref] = line.trim().split(/\s+/);
|
|
144
|
-
|
|
145
|
-
|
|
145
|
+
const match = ref?.match(/^refs\/merge-requests\/\d+\/(\d+)\/([1-9]\d*)$/);
|
|
146
|
+
if (!/^(?:[a-f\d]{40}|[a-f\d]{64})$/i.test(sha ?? "") || match?.[1] !== number) return [];
|
|
147
|
+
const revision = match[2];
|
|
146
148
|
// Numeric, so revision 10 sorts above revision 9 rather than beside 1.
|
|
147
149
|
return /^[1-9]\d*$/.test(revision) ? [{ sha, ref, revision: BigInt(revision) }] : [];
|
|
148
150
|
});
|
|
149
151
|
revisions.sort((a, b) => (a.revision > b.revision ? -1 : a.revision < b.revision ? 1 : 0));
|
|
150
152
|
if (!revisions.length) throw err; // no revisions either: the original error is the true one
|
|
151
153
|
const latest = revisions[0];
|
|
154
|
+
if (revisions.some(r => r.revision === latest.revision && r.sha !== latest.sha)) {
|
|
155
|
+
throw new MergeRequestCheckoutError(`merge request !${number} has conflicting latest revision refs`);
|
|
156
|
+
}
|
|
152
157
|
await exec("git", ["fetch", "--quiet", "origin", latest.ref], options);
|
|
153
158
|
return latest.sha;
|
|
154
159
|
}
|
|
@@ -248,13 +253,13 @@ async function localHead(dir, target, number, { exec, namespace, branch = null }
|
|
|
248
253
|
}
|
|
249
254
|
|
|
250
255
|
async function resolveMergeRequestCheckout(prUrl, target, number, {
|
|
251
|
-
allowPush, exec, makeTemp, remove, dir,
|
|
256
|
+
allowPush, exec, makeTemp, remove, dir, root,
|
|
252
257
|
}) {
|
|
253
258
|
// Where the clone lands when one is needed: the same ~/.jury the rest of the
|
|
254
259
|
// CLI already uses, rather than the system temp directory. The checkout is
|
|
255
260
|
// still removed when the review ends — this puts jury's working files under
|
|
256
261
|
// one predictable, inspectable root, and does not make them persist.
|
|
257
|
-
const worktree = await makeTemp(path.join(
|
|
262
|
+
const worktree = await makeTemp(path.join(root, "checkouts", `jury-mr-${number}-`));
|
|
258
263
|
let advertisedSha = null;
|
|
259
264
|
// Refresh the cached ref before trusting it. A ref fetched before the author
|
|
260
265
|
// pushed again names an obsolete head, and nothing about matching repository
|
|
@@ -335,8 +340,8 @@ async function resolveMergeRequestCheckout(prUrl, target, number, {
|
|
|
335
340
|
}
|
|
336
341
|
if (allowPush && !branch) {
|
|
337
342
|
throw new MergeRequestCheckoutError(
|
|
338
|
-
`
|
|
339
|
-
"
|
|
343
|
+
`Can't determine where to push fixes for merge request !${number}.\n` +
|
|
344
|
+
"Add --no-push to review it without pushing changes.",
|
|
340
345
|
);
|
|
341
346
|
}
|
|
342
347
|
|
|
@@ -352,17 +357,15 @@ async function resolveMergeRequestCheckout(prUrl, target, number, {
|
|
|
352
357
|
} catch (err) {
|
|
353
358
|
await remove(worktree);
|
|
354
359
|
if (err instanceof MergeRequestCheckoutError) throw err;
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
// reported as a probable authentication problem. Whatever git said is the
|
|
358
|
-
// one thing that distinguishes the cases.
|
|
360
|
+
// Preserve the diagnostic even when a connection warning precedes it.
|
|
361
|
+
// Missing refs alone do not establish whether an MR is open or closed.
|
|
359
362
|
const said = [err.stderr, err.message].map((t) => String(t ?? "").trim()).find(Boolean) ?? "";
|
|
360
363
|
const missingRef = /couldn't find remote ref|no matching remote head/i.test(said);
|
|
361
364
|
throw new Error(
|
|
362
|
-
`could not resolve ${target.display} merge request !${number}: ${said
|
|
365
|
+
`could not resolve ${target.display} merge request !${number}: ${said}\n` +
|
|
363
366
|
(missingRef
|
|
364
|
-
?
|
|
365
|
-
"
|
|
367
|
+
? "No usable merge request ref was found. Check out the source branch locally, omit the MR URL, " +
|
|
368
|
+
"and run jury --dir /path/to/checkout --trunk <target-branch>."
|
|
366
369
|
: "check Git authentication and refs/merge-requests support, or check out the source branch and pass --dir"),
|
|
367
370
|
);
|
|
368
371
|
}
|
|
@@ -400,13 +403,14 @@ export async function resolvePrCheckout(prUrl, {
|
|
|
400
403
|
// repository that already holds the request's commits needs no head ref.
|
|
401
404
|
dir = null,
|
|
402
405
|
home = homedir(),
|
|
406
|
+
root = path.join(home, ".jury"),
|
|
403
407
|
} = {}) {
|
|
404
408
|
const target = repositoryFromPrUrl(prUrl);
|
|
405
409
|
const number = githubNumber(prUrl);
|
|
406
410
|
const mrNumber = mergeRequestNumber(prUrl);
|
|
407
411
|
if (target && mrNumber) {
|
|
408
412
|
return resolveMergeRequestCheckout(prUrl, target, mrNumber, {
|
|
409
|
-
allowPush, exec, makeTemp, remove, dir,
|
|
413
|
+
allowPush, exec, makeTemp, remove, dir, root,
|
|
410
414
|
});
|
|
411
415
|
}
|
|
412
416
|
if (!target || !number) {
|
|
@@ -442,7 +446,7 @@ export async function resolvePrCheckout(prUrl, {
|
|
|
442
446
|
);
|
|
443
447
|
}
|
|
444
448
|
|
|
445
|
-
const worktree = await makeTemp(path.join(
|
|
449
|
+
const worktree = await makeTemp(path.join(root, "checkouts", `jury-pr-${number}-`));
|
|
446
450
|
// `gh` already gave the authoritative head oid, so that — not a cached ref —
|
|
447
451
|
// is what to look for locally. Consulting refs/pull/<id>/head first checked
|
|
448
452
|
// out whatever it pointed at when it was last fetched, which then failed the
|