prreviewbuddy 0.30.0 → 0.34.0
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/CHANGELOG.md +109 -0
- package/dist/{delete_review-B_2J2Wn9.js → delete_review-TNIFNct3.js} +504 -26
- package/dist/main.js +75 -22
- package/dist/server.js +557 -127
- package/package.json +1 -1
- package/static/chunk-B80S6zbH.js +4 -4
- package/static/chunk-CBxYYqda.js +1 -0
- package/static/chunk-CUFGKuJi.js +1 -0
- package/static/chunk-X60fsCZA.js +1 -0
- package/static/notifications.js +1 -0
- package/static/reviews.js +1 -1
- package/static/settings.js +1 -1
- package/static/shell.css +885 -431
- package/static/workspace.js +44 -44
- package/static/chunk-CIsM3PMS.js +0 -1
|
@@ -2060,6 +2060,17 @@ var EmptyChangeError = class extends Error {
|
|
|
2060
2060
|
}
|
|
2061
2061
|
};
|
|
2062
2062
|
/**
|
|
2063
|
+
* Whether a stored change set is a review of one commit. See `ChangeSet.targetType`.
|
|
2064
|
+
*
|
|
2065
|
+
* A function rather than the comparison written out, and not only for tidiness:
|
|
2066
|
+
* `freshness_read_only.test.ts` refuses any quoted git subcommand in `freshness.ts`, and the
|
|
2067
|
+
* discriminator's value is spelled the same as one. That guard is right to be blunt, so the
|
|
2068
|
+
* comparison lives here, beside the field it reads.
|
|
2069
|
+
*/
|
|
2070
|
+
function isCommitReview(changeSet) {
|
|
2071
|
+
return changeSet.targetType === "commit";
|
|
2072
|
+
}
|
|
2073
|
+
/**
|
|
2063
2074
|
* Decide what this change is being compared against.
|
|
2064
2075
|
*
|
|
2065
2076
|
* Refuses rather than guessing. A base we picked wrongly produces a guide that looks completely
|
|
@@ -2142,7 +2153,7 @@ async function defaultRemoteBranch(repoPath) {
|
|
|
2142
2153
|
async function buildChangeSet(options) {
|
|
2143
2154
|
const repoPath = (await git(options.repoPath, ["rev-parse", "--show-toplevel"])).trim();
|
|
2144
2155
|
const base = options.resolvedBase ?? await resolveBase(repoPath, options.base);
|
|
2145
|
-
const mergeBase = await mergeBaseOf(repoPath, base, "HEAD");
|
|
2156
|
+
const mergeBase = base.exact ? base.revision : await mergeBaseOf(repoPath, base, "HEAD");
|
|
2146
2157
|
const headRef = await currentBranch(repoPath);
|
|
2147
2158
|
const isLinkedWorktree = await linkedWorktree(repoPath);
|
|
2148
2159
|
const files = await collectFiles(repoPath, [mergeBase, "HEAD"]);
|
|
@@ -2150,7 +2161,7 @@ async function buildChangeSet(options) {
|
|
|
2150
2161
|
disagreement(files.length, base.display, options.expected);
|
|
2151
2162
|
const blobShas = await headBlobShas(repoPath);
|
|
2152
2163
|
for (const file of files) file.sha = blobShas.get(file.name) ?? "";
|
|
2153
|
-
const { subjects, authors } = await commitLog(repoPath, mergeBase);
|
|
2164
|
+
const { subjects, authors } = await commitLog(repoPath, base.exact && !await refExists(repoPath, mergeBase) ? "HEAD" : `${mergeBase}..HEAD`);
|
|
2154
2165
|
return {
|
|
2155
2166
|
data: {
|
|
2156
2167
|
pullRequest: {
|
|
@@ -2286,12 +2297,12 @@ async function currentBranch(repoPath) {
|
|
|
2286
2297
|
* Uncommitted work has no author in the log, and needs none. It belongs to whoever is sitting at
|
|
2287
2298
|
* this checkout, and that is the person reading the page.
|
|
2288
2299
|
*/
|
|
2289
|
-
async function commitLog(repoPath,
|
|
2300
|
+
async function commitLog(repoPath, range) {
|
|
2290
2301
|
const out = await git(repoPath, [
|
|
2291
2302
|
"log",
|
|
2292
2303
|
"--reverse",
|
|
2293
2304
|
"--format=%an%x1f%s",
|
|
2294
|
-
|
|
2305
|
+
range
|
|
2295
2306
|
]);
|
|
2296
2307
|
const subjects = [];
|
|
2297
2308
|
const counts = /* @__PURE__ */ new Map();
|
|
@@ -2365,7 +2376,7 @@ async function mergeBaseOf(repoPath, base, head) {
|
|
|
2365
2376
|
*/
|
|
2366
2377
|
async function hasCommittedChanges(repoPath, base, head) {
|
|
2367
2378
|
return parseNameStatus(await git(repoPath, [
|
|
2368
|
-
...rangeDiff([await mergeBaseOf(repoPath, base, head), head]),
|
|
2379
|
+
...rangeDiff([base.exact ? base.revision : await mergeBaseOf(repoPath, base, head), head]),
|
|
2369
2380
|
"--name-status",
|
|
2370
2381
|
"-z"
|
|
2371
2382
|
])).length > 0;
|
|
@@ -2610,6 +2621,97 @@ function ambiguous(named, found) {
|
|
|
2610
2621
|
].join("\n");
|
|
2611
2622
|
}
|
|
2612
2623
|
//#endregion
|
|
2624
|
+
//#region ../../packages/review-harness/src/workspace/commit_target.ts
|
|
2625
|
+
/**
|
|
2626
|
+
* One commit the reviewer named, resolved to what it introduced, or refused.
|
|
2627
|
+
*
|
|
2628
|
+
* `prreviewbuddy review --commit 9907c64` reviews *the change that commit made*, which is the diff
|
|
2629
|
+
* against its first parent. That is a different object from everything else this product reviews,
|
|
2630
|
+
* and the difference is not the spelling of the target but what can be done to it afterwards.
|
|
2631
|
+
*
|
|
2632
|
+
* **A branch is followed; a commit is not.** `branch_target.ts` refuses a commit and says why: a
|
|
2633
|
+
* review follows its branch so it can be brought up to date as commits arrive, and a commit has
|
|
2634
|
+
* nowhere to move to. That refusal stands. This module is the other surface that docblock asked for,
|
|
2635
|
+
* and the thing it has to supply that `--branch` could not is an answer to what the lineage key is:
|
|
2636
|
+
* the commit's own full sha, never a ref manufactured from it. See `lineageKeyFor`.
|
|
2637
|
+
*
|
|
2638
|
+
* **The parent is the base, and the reviewer cannot name another one.** `--commit` with `--base` is
|
|
2639
|
+
* refused in the parser, because the two together are arbitrary range review arriving by the back
|
|
2640
|
+
* door: the findings would be about a different change under a page still naming the commit.
|
|
2641
|
+
*
|
|
2642
|
+
* **Nothing fetches,** for the same reason as `branch_target.ts`: the reviewer named something they
|
|
2643
|
+
* believe is here, and a commit-ish that does not resolve locally is far more often a typo than a
|
|
2644
|
+
* missing fetch.
|
|
2645
|
+
*/
|
|
2646
|
+
async function resolveNamedCommit(repoPath, value) {
|
|
2647
|
+
const named = value.trim();
|
|
2648
|
+
if (!named) throw new TargetError("No commit was named. Give `--commit` a commit, such as `--commit 9907c64` or `--commit HEAD~2`.");
|
|
2649
|
+
const sha = await peel(repoPath, named);
|
|
2650
|
+
const parents = (await git(repoPath, [
|
|
2651
|
+
"rev-list",
|
|
2652
|
+
"--parents",
|
|
2653
|
+
"-n",
|
|
2654
|
+
"1",
|
|
2655
|
+
sha
|
|
2656
|
+
])).trim().split(/\s+/).slice(1);
|
|
2657
|
+
const subject = (await git(repoPath, [
|
|
2658
|
+
"log",
|
|
2659
|
+
"-1",
|
|
2660
|
+
"--format=%s",
|
|
2661
|
+
sha
|
|
2662
|
+
])).trim();
|
|
2663
|
+
return {
|
|
2664
|
+
sha,
|
|
2665
|
+
parentSha: parents[0] ?? null,
|
|
2666
|
+
parentCount: parents.length,
|
|
2667
|
+
subject
|
|
2668
|
+
};
|
|
2669
|
+
}
|
|
2670
|
+
/**
|
|
2671
|
+
* The empty tree, asked of the repository rather than written down.
|
|
2672
|
+
*
|
|
2673
|
+
* A root commit is compared against nothing, and git spells nothing as the empty tree object. The
|
|
2674
|
+
* constant everybody knows, `4b825dc642cb6eb9a060e54bf8d69288fbee4904`, is the **sha-1** spelling of
|
|
2675
|
+
* it, and a repository created with `--object-format=sha256` has a different one. Hardcoding it
|
|
2676
|
+
* would make root-commit review silently wrong there rather than loudly broken, so the repository is
|
|
2677
|
+
* asked. `/dev/null` is an empty file, and hashing one as a tree is the documented way to name it.
|
|
2678
|
+
*/
|
|
2679
|
+
async function emptyTreeSha(repoPath) {
|
|
2680
|
+
return (await git(repoPath, [
|
|
2681
|
+
"hash-object",
|
|
2682
|
+
"-t",
|
|
2683
|
+
"tree",
|
|
2684
|
+
"/dev/null"
|
|
2685
|
+
])).trim();
|
|
2686
|
+
}
|
|
2687
|
+
/**
|
|
2688
|
+
* The full commit sha behind whatever was typed, or a refusal naming what it found instead.
|
|
2689
|
+
*
|
|
2690
|
+
* A tree or a blob is refused **by name**, the way `branch_target.ts` refuses `refs/tags/v1` by
|
|
2691
|
+
* name: `--commit HEAD^{tree}` resolves perfectly well to an object that is right there, and telling
|
|
2692
|
+
* the reviewer it could not be found would send them looking for a commit they have.
|
|
2693
|
+
*/
|
|
2694
|
+
async function peel(repoPath, named) {
|
|
2695
|
+
try {
|
|
2696
|
+
return (await git(repoPath, [
|
|
2697
|
+
"rev-parse",
|
|
2698
|
+
"--verify",
|
|
2699
|
+
"--quiet",
|
|
2700
|
+
`${named}^{commit}`
|
|
2701
|
+
])).trim();
|
|
2702
|
+
} catch {}
|
|
2703
|
+
let type = "";
|
|
2704
|
+
try {
|
|
2705
|
+
type = (await git(repoPath, [
|
|
2706
|
+
"cat-file",
|
|
2707
|
+
"-t",
|
|
2708
|
+
named
|
|
2709
|
+
])).trim();
|
|
2710
|
+
} catch {}
|
|
2711
|
+
if (type === "tree" || type === "blob") throw new TargetError(`\`${named}\` is a ${type}, and a review is of a commit.\n\nA commit review is the change that commit introduced, which is its diff against its parent. A ${type} is a snapshot with nothing to compare it to. Name the commit instead.`);
|
|
2712
|
+
throw new TargetError(`There is nothing called \`${named}\` in this repository.\n\n\`--commit\` takes anything git can resolve to a commit: a sha, \`HEAD~2\`, a tag. If the commit is on a remote this clone has not seen, run \`git fetch\` and try again.`);
|
|
2713
|
+
}
|
|
2714
|
+
//#endregion
|
|
2613
2715
|
//#region ../../packages/review-harness/src/workspace/target.ts
|
|
2614
2716
|
/**
|
|
2615
2717
|
* What is being reviewed, decided once and used everywhere.
|
|
@@ -2628,6 +2730,7 @@ function ambiguous(named, found) {
|
|
|
2628
2730
|
*/
|
|
2629
2731
|
async function resolveTarget(input) {
|
|
2630
2732
|
const root = await repositoryRoot(input.repoPath);
|
|
2733
|
+
if (input.commitRef) return commitTarget(root, input.commitRef);
|
|
2631
2734
|
if (input.base) await verifyBase(root, input.base);
|
|
2632
2735
|
else await verifyDefaultBase(root);
|
|
2633
2736
|
const named = input.base ? { base: input.base } : {};
|
|
@@ -2656,7 +2759,7 @@ async function resolveTarget(input) {
|
|
|
2656
2759
|
...named
|
|
2657
2760
|
};
|
|
2658
2761
|
}
|
|
2659
|
-
const branch = await branchOf(root);
|
|
2762
|
+
const branch = await branchOf$1(root);
|
|
2660
2763
|
let sha;
|
|
2661
2764
|
try {
|
|
2662
2765
|
sha = (await git(root, ["rev-parse", "HEAD"])).trim();
|
|
@@ -2684,6 +2787,46 @@ async function resolveTarget(input) {
|
|
|
2684
2787
|
};
|
|
2685
2788
|
}
|
|
2686
2789
|
/**
|
|
2790
|
+
* One commit, and the parent it will be compared against, settled here and never re-derived.
|
|
2791
|
+
*
|
|
2792
|
+
* The base is built as a `ResolvedBase` rather than left as a name for `resolveBase` to work out,
|
|
2793
|
+
* which is the seam the pull request path already uses: `ChangeSetOptions.resolvedBase` is
|
|
2794
|
+
* documented as "a base already decided by the caller... The caller that set it had an authority
|
|
2795
|
+
* this module does not". The forge is that authority there. Here it is git's own commit graph, which
|
|
2796
|
+
* is a stronger one still, and there is nothing for a ladder of branch names to contribute to it.
|
|
2797
|
+
*
|
|
2798
|
+
* `followRef` is deliberately absent. It exists so a later rebuild can re-resolve a moving base, and
|
|
2799
|
+
* this base cannot move: a commit's parent is the same commit for ever. Recording a ref here would
|
|
2800
|
+
* invite exactly the re-resolution the immutability is for.
|
|
2801
|
+
*
|
|
2802
|
+
* `exact` is set because the parent *is* the comparison point. For an ordinary commit that agrees
|
|
2803
|
+
* with `merge-base(parent, commit)` anyway; for a merge it is what pins the diff to the first parent
|
|
2804
|
+
* rather than to wherever the two histories converge, and for a root commit it is the only way the
|
|
2805
|
+
* empty tree can be a base at all.
|
|
2806
|
+
*/
|
|
2807
|
+
async function commitTarget(root, commitRef) {
|
|
2808
|
+
const commit = await resolveNamedCommit(root, commitRef);
|
|
2809
|
+
const short = commit.sha.slice(0, 7);
|
|
2810
|
+
const parent = commit.parentSha;
|
|
2811
|
+
const resolvedBase = parent ? {
|
|
2812
|
+
revision: parent,
|
|
2813
|
+
display: parent.slice(0, 7),
|
|
2814
|
+
exact: true
|
|
2815
|
+
} : {
|
|
2816
|
+
revision: await emptyTreeSha(root),
|
|
2817
|
+
display: "the empty tree",
|
|
2818
|
+
exact: true
|
|
2819
|
+
};
|
|
2820
|
+
return {
|
|
2821
|
+
originRepoPath: root,
|
|
2822
|
+
branch: short,
|
|
2823
|
+
sha: commit.sha,
|
|
2824
|
+
base: resolvedBase.display,
|
|
2825
|
+
resolvedBase,
|
|
2826
|
+
commit
|
|
2827
|
+
};
|
|
2828
|
+
}
|
|
2829
|
+
/**
|
|
2687
2830
|
* Which commit this request was cut from, decided before anything expensive happens.
|
|
2688
2831
|
*
|
|
2689
2832
|
* Strict, and it refuses rather than falling back to a local branch of the right name. The local
|
|
@@ -2798,7 +2941,7 @@ async function refuseEmptyTarget(target) {
|
|
|
2798
2941
|
async function uncommittedIn(root, target) {
|
|
2799
2942
|
if (target.pullRequest) return 0;
|
|
2800
2943
|
try {
|
|
2801
|
-
if ((await git(root, ["rev-parse", "HEAD"])).trim() !== target.sha || await branchOf(root) !== target.branch) return 0;
|
|
2944
|
+
if ((await git(root, ["rev-parse", "HEAD"])).trim() !== target.sha || await branchOf$1(root) !== target.branch) return 0;
|
|
2802
2945
|
return (await git(root, ["status", "--porcelain"])).split("\n").filter((line) => line.trim()).length;
|
|
2803
2946
|
} catch {
|
|
2804
2947
|
return 0;
|
|
@@ -2818,6 +2961,14 @@ async function uncommittedIn(root, target) {
|
|
|
2818
2961
|
* to the display name, which `resolveBase` now resolves remote-first anyway.
|
|
2819
2962
|
*/
|
|
2820
2963
|
function baseOf(changeSet) {
|
|
2964
|
+
if (changeSet.targetType === "commit" && changeSet.mergeBase) return {
|
|
2965
|
+
base: changeSet.baseRef,
|
|
2966
|
+
resolvedBase: {
|
|
2967
|
+
revision: changeSet.mergeBase,
|
|
2968
|
+
display: changeSet.baseRef,
|
|
2969
|
+
exact: true
|
|
2970
|
+
}
|
|
2971
|
+
};
|
|
2821
2972
|
if (!changeSet.baseRef) return {};
|
|
2822
2973
|
const followRef = changeSet.baseResolvedRef;
|
|
2823
2974
|
return {
|
|
@@ -2872,7 +3023,7 @@ async function repositoryRoot(repoPath) {
|
|
|
2872
3023
|
} catch {}
|
|
2873
3024
|
throw new TargetError(`${repoPath} is not inside a git repository.`);
|
|
2874
3025
|
}
|
|
2875
|
-
async function branchOf(root) {
|
|
3026
|
+
async function branchOf$1(root) {
|
|
2876
3027
|
let branch = "";
|
|
2877
3028
|
try {
|
|
2878
3029
|
branch = (await git(root, [
|
|
@@ -2885,7 +3036,7 @@ async function branchOf(root) {
|
|
|
2885
3036
|
branch = "";
|
|
2886
3037
|
}
|
|
2887
3038
|
if (branch) return branch;
|
|
2888
|
-
throw new TargetError("This checkout is on a detached HEAD, so there is no branch to file a review under.
|
|
3039
|
+
throw new TargetError("This checkout is on a detached HEAD, so there is no branch to file a review under. To review the commit you are on, run `prreviewbuddy review --commit HEAD`. Otherwise check out a branch, or name the pull request you want reviewed.");
|
|
2889
3040
|
}
|
|
2890
3041
|
/**
|
|
2891
3042
|
* The commit has to be here before a worktree can be pinned to it.
|
|
@@ -4036,9 +4187,22 @@ function reviewedRepositories() {
|
|
|
4036
4187
|
*
|
|
4037
4188
|
* A newline separates the halves because it cannot appear in any of them, so no set of values can
|
|
4038
4189
|
* collide by running together.
|
|
4190
|
+
*
|
|
4191
|
+
* **A commit review keys by the commit, not by a branch.** It follows nothing, so the ref half has
|
|
4192
|
+
* no meaning for it, and its display name is a short sha that two different commits can share.
|
|
4039
4193
|
*/
|
|
4040
4194
|
function lineageKey(review) {
|
|
4041
|
-
|
|
4195
|
+
if (review.targetType === "commit") return lineageKeyFor({
|
|
4196
|
+
kind: "commit",
|
|
4197
|
+
repoPath: review.repoPath,
|
|
4198
|
+
sha: review.commitSha ?? ""
|
|
4199
|
+
});
|
|
4200
|
+
return lineageKeyFor({
|
|
4201
|
+
kind: "branch",
|
|
4202
|
+
repoPath: review.repoPath,
|
|
4203
|
+
branch: review.branch,
|
|
4204
|
+
followedRef: review.headResolvedRef
|
|
4205
|
+
});
|
|
4042
4206
|
}
|
|
4043
4207
|
/**
|
|
4044
4208
|
* The same key, from the pieces, so a target and a stored review cannot arrive at it differently.
|
|
@@ -4047,8 +4211,19 @@ function lineageKey(review) {
|
|
|
4047
4211
|
* asks whether a target is the target of a stored review, and if it compared its own three fields
|
|
4048
4212
|
* by hand then widening the key here and widening the match there would be two changes that have
|
|
4049
4213
|
* to stay in step forever.
|
|
4214
|
+
*
|
|
4215
|
+
* **The commit arm is decided before the branch arm's defaulting, and that order is the point.** The
|
|
4216
|
+
* branch arm reads an absent ref as `refs/heads/<branch>`, so a commit reaching it would derive
|
|
4217
|
+
* `refs/heads/<sha>` and file under a ref nobody has. The middle segment is the literal `commit`,
|
|
4218
|
+
* which no branch key can produce in that position with a full sha after it, because a branch key's
|
|
4219
|
+
* last segment always starts with `refs/`.
|
|
4220
|
+
*
|
|
4221
|
+
* The branch arm is byte-for-byte the key every review on disk already has. Changing it re-files
|
|
4222
|
+
* every stored review under a new card, silently, on every machine.
|
|
4050
4223
|
*/
|
|
4051
|
-
function lineageKeyFor(
|
|
4224
|
+
function lineageKeyFor(subject) {
|
|
4225
|
+
if (subject.kind === "commit") return `${subject.repoPath}\ncommit\n${subject.sha}`;
|
|
4226
|
+
const { repoPath, branch, followedRef } = subject;
|
|
4052
4227
|
return `${repoPath}\n${branch}\n${followedRef ?? `refs/heads/${branch}`}`;
|
|
4053
4228
|
}
|
|
4054
4229
|
/**
|
|
@@ -4206,6 +4381,11 @@ function summarise(workspace) {
|
|
|
4206
4381
|
branch: workspace.changeSet.headRef,
|
|
4207
4382
|
baseRef: workspace.changeSet.baseRef ?? "",
|
|
4208
4383
|
...workspace.changeSet.headResolvedRef ? { headResolvedRef: workspace.changeSet.headResolvedRef } : {},
|
|
4384
|
+
...workspace.changeSet.targetType === "commit" ? {
|
|
4385
|
+
targetType: "commit",
|
|
4386
|
+
parentSha: workspace.changeSet.parentSha ?? null,
|
|
4387
|
+
...workspace.changeSet.parentCount !== void 0 ? { parentCount: workspace.changeSet.parentCount } : {}
|
|
4388
|
+
} : {},
|
|
4209
4389
|
repoPath: workspace.repoPath,
|
|
4210
4390
|
changedFiles: totalChangedFiles(workspace.changeSet.data),
|
|
4211
4391
|
additions: files.reduce((total, file) => total + (file.additions ?? 0), 0),
|
|
@@ -4224,7 +4404,7 @@ function summarise(workspace) {
|
|
|
4224
4404
|
pinned: workspace.pinned === true,
|
|
4225
4405
|
paths: files.slice(0, MAX_INDEXED_PATHS).map((file) => file.name),
|
|
4226
4406
|
prNumber: workspace.changeSet.data.pullRequest?.number || workspace.prContext?.pullRequest.number,
|
|
4227
|
-
commitSha: workspace.session.metadata?.commitSha || void 0,
|
|
4407
|
+
commitSha: workspace.session.metadata?.commitSha || (workspace.changeSet.targetType === "commit" ? workspace.changeSet.data.pullRequest.head.sha : "") || void 0,
|
|
4228
4408
|
author: describeAuthorship(workspace.changeSet, workspace.prContext?.pullRequest)?.label,
|
|
4229
4409
|
authorHandle: workspace.prContext?.pullRequest.author?.handle || void 0
|
|
4230
4410
|
};
|
|
@@ -4586,6 +4766,18 @@ function isClaimed(jobId, now = Date.now()) {
|
|
|
4586
4766
|
return liveClaim(jobId, now) !== null;
|
|
4587
4767
|
}
|
|
4588
4768
|
/**
|
|
4769
|
+
* Whether a driver took this job and is no longer there to finish it.
|
|
4770
|
+
*
|
|
4771
|
+
* The claim file is the evidence. A job minted a moment ago and not yet driven has no claim at all,
|
|
4772
|
+
* and a driver that finished released its claim on the way out, so a claim that is still on disk
|
|
4773
|
+
* with nobody live behind it can only belong to a run that ended without finishing: a terminal that
|
|
4774
|
+
* closed, a process that was killed. Answered at once rather than after the sweep's half hour,
|
|
4775
|
+
* because the reviewer asking is usually the one who just closed that terminal.
|
|
4776
|
+
*/
|
|
4777
|
+
function driverGone(jobId, now = Date.now()) {
|
|
4778
|
+
return readClaim(jobId) !== null && liveClaim(jobId, now) === null;
|
|
4779
|
+
}
|
|
4780
|
+
/**
|
|
4589
4781
|
* Take the claim on a job, or return null because another driver holds it.
|
|
4590
4782
|
*
|
|
4591
4783
|
* Two retries fired together, a retry racing the run `startJob` already kicked off, or a retry
|
|
@@ -4854,7 +5046,7 @@ function deleteJob(id) {
|
|
|
4854
5046
|
}
|
|
4855
5047
|
function advance(job, phase, progress) {
|
|
4856
5048
|
const leaving = job.phase;
|
|
4857
|
-
const completed = WORK_PHASES.includes(leaving) && !job.completed.includes(leaving) ? [...job.completed, leaving] : job.completed;
|
|
5049
|
+
const completed = WORK_PHASES.includes(leaving) && leaving !== phase && !job.completed.includes(leaving) ? [...job.completed, leaving] : job.completed;
|
|
4858
5050
|
return {
|
|
4859
5051
|
...job,
|
|
4860
5052
|
phase,
|
|
@@ -5110,18 +5302,47 @@ async function refSha(repoPath, ref) {
|
|
|
5110
5302
|
* - **Describe, do not re-analyse.** The workspace says what changed. Deciding what it means for
|
|
5111
5303
|
* the findings is the analyser's job, and the analyser is Claude Code.
|
|
5112
5304
|
*/
|
|
5113
|
-
|
|
5305
|
+
/**
|
|
5306
|
+
* Freshness, and for a commit review whether its commit is still here, in one pass.
|
|
5307
|
+
*
|
|
5308
|
+
* **`latest()` is never asked about a commit review.** Its null already means "the branch is not in
|
|
5309
|
+
* this repository", a real answer with two documented causes. A commit review has no branch to look
|
|
5310
|
+
* for at all, and teaching that function to return null for this too would make one value mean two
|
|
5311
|
+
* unrelated things, told apart only by knowing the target type: the ambiguity a commit target exists
|
|
5312
|
+
* to remove. So this path does not reach it.
|
|
5313
|
+
*/
|
|
5314
|
+
async function checkReview(workspace) {
|
|
5114
5315
|
const reviewedSha = workspace.session.metadata?.commitSha || workspace.changeSet.data.pullRequest.head.sha || null;
|
|
5115
5316
|
const headSha = await readHead(workspace.repoPath);
|
|
5317
|
+
const commitReview = isCommitReview(workspace.changeSet);
|
|
5116
5318
|
return {
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5319
|
+
freshness: {
|
|
5320
|
+
checkedAt: Date.now(),
|
|
5321
|
+
code: commitReview ? fixedCode(reviewedSha) : await codeFreshness(workspace, reviewedSha, headSha, await latest(workspace)),
|
|
5322
|
+
checkout: await checkoutFreshness(workspace, headSha),
|
|
5323
|
+
conversation: workspace.prContext ? {
|
|
5324
|
+
attached: true,
|
|
5325
|
+
state: "unknown",
|
|
5326
|
+
importedAt: workspace.prContext.discussion.fetchedAt
|
|
5327
|
+
} : { attached: false }
|
|
5328
|
+
},
|
|
5329
|
+
availability: commitReview && reviewedSha ? {
|
|
5330
|
+
state: await commitExists(workspace.repoPath, reviewedSha) ? "available" : "missing",
|
|
5331
|
+
sha: reviewedSha
|
|
5332
|
+
} : null
|
|
5333
|
+
};
|
|
5334
|
+
}
|
|
5335
|
+
/** The code answer for a commit review. Nothing is compared, so every count is absent rather than zero. */
|
|
5336
|
+
function fixedCode(reviewedSha) {
|
|
5337
|
+
return {
|
|
5338
|
+
state: "fixed",
|
|
5339
|
+
reviewedSha,
|
|
5340
|
+
latestSha: null,
|
|
5341
|
+
comparison: null,
|
|
5342
|
+
newCommits: null,
|
|
5343
|
+
changedFiles: null,
|
|
5344
|
+
rewritten: false,
|
|
5345
|
+
reason: null
|
|
5125
5346
|
};
|
|
5126
5347
|
}
|
|
5127
5348
|
/**
|
|
@@ -5139,6 +5360,7 @@ async function checkFreshness(workspace) {
|
|
|
5139
5360
|
* a whole `FreshnessReport` around a single run would imply it had its own answer to both.
|
|
5140
5361
|
*/
|
|
5141
5362
|
async function checkCodeFreshness(workspace, reviewedSha) {
|
|
5363
|
+
if (isCommitReview(workspace.changeSet)) return fixedCode(reviewedSha);
|
|
5142
5364
|
return codeFreshness(workspace, reviewedSha, await readHead(workspace.repoPath), await latest(workspace));
|
|
5143
5365
|
}
|
|
5144
5366
|
/**
|
|
@@ -9283,6 +9505,229 @@ async function withUsageRecorded(workspaceId, kind, env, body, runId, requestedM
|
|
|
9283
9505
|
}
|
|
9284
9506
|
}
|
|
9285
9507
|
//#endregion
|
|
9508
|
+
//#region ../../packages/review-harness/src/workspace/notifications.ts
|
|
9509
|
+
/**
|
|
9510
|
+
* What happened while the reviewer was looking at something else, kept until they have seen it.
|
|
9511
|
+
*
|
|
9512
|
+
* **One file per notification, not one list.** Notifications are written by whichever process
|
|
9513
|
+
* drove the work to its end: the CLI for a review typed at a terminal, the workspace server for one
|
|
9514
|
+
* started from the page, the sweep for one whose driver died. A single `notifications.json` read,
|
|
9515
|
+
* modified and written by several processes loses whichever entry was written second, and the lost
|
|
9516
|
+
* one is by definition something the reviewer was not watching. `jobs/` is laid out the same way for
|
|
9517
|
+
* the same reason.
|
|
9518
|
+
*
|
|
9519
|
+
* **Created once, with `wx`.** The id is derived from the transition it records, so a second attempt
|
|
9520
|
+
* to record the same transition fails at the filesystem and returns null, and a caller that only
|
|
9521
|
+
* delivers what it created cannot deliver twice. Nothing that reads a job ever creates one: a page
|
|
9522
|
+
* reloading, a client polling or a review being reopened cannot announce anything.
|
|
9523
|
+
*
|
|
9524
|
+
* Imports only `store_root`, for the cycle `queue.ts` describes.
|
|
9525
|
+
*/
|
|
9526
|
+
var NOTIFICATIONS_DIR = join(STORE_ROOT, "notifications");
|
|
9527
|
+
/** As long as a review is kept, so the list never names a review nobody can open for that reason. */
|
|
9528
|
+
var NOTIFICATION_RETENTION_MS = 2592e6;
|
|
9529
|
+
var OPERATION_NAMES = {
|
|
9530
|
+
review: "Review",
|
|
9531
|
+
reanalysis: "Reanalysis",
|
|
9532
|
+
update: "Review update",
|
|
9533
|
+
kiss: "KISS analysis"
|
|
9534
|
+
};
|
|
9535
|
+
/** What happened, in the words the list uses: `Review ready`, `Review update failed`. */
|
|
9536
|
+
function eventName(notification) {
|
|
9537
|
+
return `${OPERATION_NAMES[notification.operation]} ${notification.outcome}`;
|
|
9538
|
+
}
|
|
9539
|
+
/** The whole line, for anything that shows one event on its own: what happened, then to what. */
|
|
9540
|
+
function headline(notification) {
|
|
9541
|
+
return `${eventName(notification)} · ${notification.subject}`;
|
|
9542
|
+
}
|
|
9543
|
+
var OPERATIONS = [
|
|
9544
|
+
"review",
|
|
9545
|
+
"reanalysis",
|
|
9546
|
+
"update",
|
|
9547
|
+
"kiss"
|
|
9548
|
+
];
|
|
9549
|
+
var OUTCOMES = ["ready", "failed"];
|
|
9550
|
+
/** An id is a file name, so it is held to a shape that cannot leave the directory. */
|
|
9551
|
+
var ID = /^[A-Za-z0-9-]{1,200}$/;
|
|
9552
|
+
function pathOf(id) {
|
|
9553
|
+
return join(NOTIFICATIONS_DIR, `${id}.json`);
|
|
9554
|
+
}
|
|
9555
|
+
/**
|
|
9556
|
+
* Record one event, or return null because it is already recorded or could not be written.
|
|
9557
|
+
*
|
|
9558
|
+
* Never throws. A notification that could not be written is a notification the reviewer does not
|
|
9559
|
+
* get, which is a smaller loss than the review job that was being announced.
|
|
9560
|
+
*/
|
|
9561
|
+
function createNotification(input, now = Date.now()) {
|
|
9562
|
+
if (!ID.test(input.id)) return null;
|
|
9563
|
+
const record = {
|
|
9564
|
+
version: 1,
|
|
9565
|
+
kind: "analysis",
|
|
9566
|
+
...input,
|
|
9567
|
+
createdAt: now
|
|
9568
|
+
};
|
|
9569
|
+
try {
|
|
9570
|
+
mkdirSync(NOTIFICATIONS_DIR, { recursive: true });
|
|
9571
|
+
writeFileSync(pathOf(input.id), JSON.stringify(record), {
|
|
9572
|
+
flag: "wx",
|
|
9573
|
+
mode: 384
|
|
9574
|
+
});
|
|
9575
|
+
return record;
|
|
9576
|
+
} catch {
|
|
9577
|
+
return null;
|
|
9578
|
+
}
|
|
9579
|
+
}
|
|
9580
|
+
/** Every notification, newest first. Unreadable and malformed files are skipped, not reported. */
|
|
9581
|
+
function listNotifications() {
|
|
9582
|
+
let names;
|
|
9583
|
+
try {
|
|
9584
|
+
names = readdirSync(NOTIFICATIONS_DIR);
|
|
9585
|
+
} catch {
|
|
9586
|
+
return [];
|
|
9587
|
+
}
|
|
9588
|
+
const found = [];
|
|
9589
|
+
for (const name of names) {
|
|
9590
|
+
if (!name.endsWith(".json")) continue;
|
|
9591
|
+
const record = readNotification(name.slice(0, -5));
|
|
9592
|
+
if (record) found.push(record);
|
|
9593
|
+
}
|
|
9594
|
+
return found.sort((a, b) => b.createdAt - a.createdAt || (a.id < b.id ? -1 : 1));
|
|
9595
|
+
}
|
|
9596
|
+
function readNotification(id) {
|
|
9597
|
+
if (!ID.test(id)) return null;
|
|
9598
|
+
try {
|
|
9599
|
+
const parsed = JSON.parse(readFileSync(pathOf(id), "utf8"));
|
|
9600
|
+
return isNotification(parsed) && parsed.id === id ? parsed : null;
|
|
9601
|
+
} catch {
|
|
9602
|
+
return null;
|
|
9603
|
+
}
|
|
9604
|
+
}
|
|
9605
|
+
function unreadCount() {
|
|
9606
|
+
return listNotifications().filter((notification) => notification.readAt === void 0).length;
|
|
9607
|
+
}
|
|
9608
|
+
/**
|
|
9609
|
+
* Mark these read, and say how many changed.
|
|
9610
|
+
*
|
|
9611
|
+
* Only ever by something the reviewer did to the notification itself: opening it, or one of the two
|
|
9612
|
+
* mark buttons. Opening the review it points at is not the same act and does not come through here.
|
|
9613
|
+
*/
|
|
9614
|
+
function markRead(ids, now = Date.now()) {
|
|
9615
|
+
let changed = 0;
|
|
9616
|
+
for (const id of ids) {
|
|
9617
|
+
const record = readNotification(id);
|
|
9618
|
+
if (!record || record.readAt !== void 0) continue;
|
|
9619
|
+
try {
|
|
9620
|
+
writeFileSync(pathOf(id), JSON.stringify({
|
|
9621
|
+
...record,
|
|
9622
|
+
readAt: now
|
|
9623
|
+
}), { mode: 384 });
|
|
9624
|
+
changed += 1;
|
|
9625
|
+
} catch {}
|
|
9626
|
+
}
|
|
9627
|
+
return changed;
|
|
9628
|
+
}
|
|
9629
|
+
function markAllRead(now = Date.now()) {
|
|
9630
|
+
return markRead(listNotifications().filter((notification) => notification.readAt === void 0).map((notification) => notification.id), now);
|
|
9631
|
+
}
|
|
9632
|
+
/**
|
|
9633
|
+
* Forget what is older than the retention, and anything past the cap, oldest first.
|
|
9634
|
+
*
|
|
9635
|
+
* Files that do not parse are removed once they are as old as the retention, judged by their
|
|
9636
|
+
* modification time, so a stray file neither lingers for ever nor disappears the moment it is seen.
|
|
9637
|
+
*/
|
|
9638
|
+
function pruneNotifications(now = Date.now()) {
|
|
9639
|
+
let names;
|
|
9640
|
+
try {
|
|
9641
|
+
names = readdirSync(NOTIFICATIONS_DIR);
|
|
9642
|
+
} catch {
|
|
9643
|
+
return 0;
|
|
9644
|
+
}
|
|
9645
|
+
const kept = listNotifications();
|
|
9646
|
+
const keep = new Set(kept.filter((notification) => now - notification.createdAt < NOTIFICATION_RETENTION_MS).slice(0, 200).map((notification) => `${notification.id}.json`));
|
|
9647
|
+
const valid = new Set(kept.map((notification) => `${notification.id}.json`));
|
|
9648
|
+
let removed = 0;
|
|
9649
|
+
for (const name of names) {
|
|
9650
|
+
if (!name.endsWith(".json") || keep.has(name)) continue;
|
|
9651
|
+
const path = join(NOTIFICATIONS_DIR, name);
|
|
9652
|
+
try {
|
|
9653
|
+
if (!valid.has(name) && now - statSync(path).mtimeMs < 2592e6) continue;
|
|
9654
|
+
unlinkSync(path);
|
|
9655
|
+
removed += 1;
|
|
9656
|
+
} catch {}
|
|
9657
|
+
}
|
|
9658
|
+
return removed;
|
|
9659
|
+
}
|
|
9660
|
+
function isNotification(value) {
|
|
9661
|
+
if (!value || typeof value !== "object") return false;
|
|
9662
|
+
const record = value;
|
|
9663
|
+
return record.version === 1 && record.kind === "analysis" && typeof record.id === "string" && OPERATIONS.includes(record.operation) && OUTCOMES.includes(record.outcome) && typeof record.reviewId === "string" && typeof record.subject === "string" && (record.branch === void 0 || typeof record.branch === "string") && typeof record.repo === "string" && typeof record.createdAt === "number" && (record.readAt === void 0 || typeof record.readAt === "number");
|
|
9664
|
+
}
|
|
9665
|
+
//#endregion
|
|
9666
|
+
//#region ../../packages/review-harness/src/workspace/announce.ts
|
|
9667
|
+
/**
|
|
9668
|
+
* Tell the reviewer a job ended, once, after the ending has been written.
|
|
9669
|
+
*
|
|
9670
|
+
* Called by the three places that write an ending: `drive` in `review_job.ts`, `updateReview`, and
|
|
9671
|
+
* the sweep for a job whose driver died. Each calls it with the record it has just saved, in the
|
|
9672
|
+
* process that saved it, so the answer never depends on who happens to be looking.
|
|
9673
|
+
*
|
|
9674
|
+
* The id is the job and the moment it ended, so two calls about the same ending make one record,
|
|
9675
|
+
* and a job picked up again that ends later is a new event with its own.
|
|
9676
|
+
*
|
|
9677
|
+
* The record is the event. The Notifications page and the workspace's toasts both read it. Any
|
|
9678
|
+
* delivery that has to be pushed rather than read belongs here, after `createNotification`, and only
|
|
9679
|
+
* for a record this call created, which carries the once-only guarantee over to it for free. Desktop
|
|
9680
|
+
* delivery is written that way and parked outside the product: see `desktop_preference.ts`.
|
|
9681
|
+
*
|
|
9682
|
+
* Never throws. This runs on the success path of a review that has already been saved, and the
|
|
9683
|
+
* accounting must not break the thing it accounts for.
|
|
9684
|
+
*/
|
|
9685
|
+
function announceJob(job) {
|
|
9686
|
+
try {
|
|
9687
|
+
if (!isTerminal(job) || job.failure?.stopped || purposeOf(job) === "ask") return;
|
|
9688
|
+
const workspace = loadWorkspace(job.workspaceId);
|
|
9689
|
+
if (!workspace) return;
|
|
9690
|
+
const branch = branchOf(workspace);
|
|
9691
|
+
createNotification({
|
|
9692
|
+
id: `${job.id}-${endedAt(job)}`,
|
|
9693
|
+
operation: operationOf(job),
|
|
9694
|
+
outcome: job.phase === "done" ? "ready" : "failed",
|
|
9695
|
+
reviewId: workspace.id,
|
|
9696
|
+
subject: subjectOf(workspace),
|
|
9697
|
+
...branch ? { branch } : {},
|
|
9698
|
+
repo: basename(workspace.repoPath)
|
|
9699
|
+
});
|
|
9700
|
+
} catch {}
|
|
9701
|
+
}
|
|
9702
|
+
function operationOf(job) {
|
|
9703
|
+
if (job.reviewType === "kiss") return "kiss";
|
|
9704
|
+
if (purposeOf(job) === "update") return "update";
|
|
9705
|
+
return job.reanalysis ? "reanalysis" : "review";
|
|
9706
|
+
}
|
|
9707
|
+
/**
|
|
9708
|
+
* What was reviewed, as the reviewer would name it, and nothing that locates it on disk.
|
|
9709
|
+
*
|
|
9710
|
+
* A pull request by its number, spelled the way its forge spells it; a commit by its short sha; a
|
|
9711
|
+
* branch by the ref it follows, which is the same rule the reviews list and the CLI use.
|
|
9712
|
+
*/
|
|
9713
|
+
function subjectOf(workspace) {
|
|
9714
|
+
const changeSet = workspace.changeSet;
|
|
9715
|
+
if (changeSet.targetType === "commit") return `commit ${changeSet.data.pullRequest.head.sha.slice(0, 7)}`;
|
|
9716
|
+
const number = changeSet.data.pullRequest?.number || workspace.prContext?.pullRequest.number;
|
|
9717
|
+
if (number) return /\/merge_requests\//.test(workspace.prContext?.pullRequest.url ?? "") ? `MR !${number}` : `PR #${number}`;
|
|
9718
|
+
return followedRefName(changeSet.headRef, changeSet.headResolvedRef) ?? changeSet.headRef;
|
|
9719
|
+
}
|
|
9720
|
+
/**
|
|
9721
|
+
* The branch beside a pull request's number, which alone does not say what the change is. Nothing
|
|
9722
|
+
* for a branch review, whose subject already is the branch, or for a commit, which has none.
|
|
9723
|
+
*/
|
|
9724
|
+
function branchOf(workspace) {
|
|
9725
|
+
const changeSet = workspace.changeSet;
|
|
9726
|
+
if (changeSet.targetType === "commit") return void 0;
|
|
9727
|
+
if (!(changeSet.data.pullRequest?.number || workspace.prContext?.pullRequest.number)) return void 0;
|
|
9728
|
+
return (followedRefName(changeSet.headRef, changeSet.headResolvedRef) ?? changeSet.headRef) || void 0;
|
|
9729
|
+
}
|
|
9730
|
+
//#endregion
|
|
9286
9731
|
//#region ../../packages/review-harness/src/telemetry/consent.ts
|
|
9287
9732
|
/**
|
|
9288
9733
|
* Who this machine is counted as, and what it has agreed to.
|
|
@@ -9605,6 +10050,7 @@ async function reanalyseReview(workspaceId, agentId, onStarted, requestedModel)
|
|
|
9605
10050
|
target,
|
|
9606
10051
|
env: agent.captureEnv(),
|
|
9607
10052
|
purpose: "review",
|
|
10053
|
+
reanalysis: true,
|
|
9608
10054
|
...model ? { requestedModel: model } : {}
|
|
9609
10055
|
});
|
|
9610
10056
|
saveJob(job);
|
|
@@ -9748,6 +10194,7 @@ async function drive(jobId, workspaceId, runners, driver, signal) {
|
|
|
9748
10194
|
repoPath: job.target.originRepoPath,
|
|
9749
10195
|
headRef: job.target.branch,
|
|
9750
10196
|
...job.target.ref ? { headResolvedRef: job.target.ref } : {},
|
|
10197
|
+
...commitIdentity(job.target, stored.changeSet),
|
|
9751
10198
|
checkoutName,
|
|
9752
10199
|
isLinkedWorktree,
|
|
9753
10200
|
data: {
|
|
@@ -9819,6 +10266,7 @@ async function drive(jobId, workspaceId, runners, driver, signal) {
|
|
|
9819
10266
|
else await applyGuide(job, workspaceId, payload, observedModel);
|
|
9820
10267
|
job = update(job, "done");
|
|
9821
10268
|
finished(job, workspaceId, startedAt, true);
|
|
10269
|
+
announceJob(job);
|
|
9822
10270
|
return job;
|
|
9823
10271
|
} catch (error) {
|
|
9824
10272
|
if (error instanceof AgentCancelledError || signal?.aborted) {
|
|
@@ -9832,6 +10280,7 @@ async function drive(jobId, workspaceId, runners, driver, signal) {
|
|
|
9832
10280
|
job = recordFailure(job, explain(job, raw), driver, kind, raw);
|
|
9833
10281
|
if (job.reviewType === "kiss") failCurrentKissRun(workspaceId, job, job.failure?.message ?? raw, kind, error instanceof KissRejected ? error.answer : void 0);
|
|
9834
10282
|
finished(job, workspaceId, startedAt, false);
|
|
10283
|
+
announceJob(job);
|
|
9835
10284
|
return job;
|
|
9836
10285
|
}
|
|
9837
10286
|
}
|
|
@@ -9916,7 +10365,7 @@ function pendingChangeSet(target) {
|
|
|
9916
10365
|
data: {
|
|
9917
10366
|
pullRequest: {
|
|
9918
10367
|
number: target.pullRequest?.number ?? 0,
|
|
9919
|
-
title: target.branch,
|
|
10368
|
+
title: target.commit?.subject || target.branch,
|
|
9920
10369
|
body: null,
|
|
9921
10370
|
base: {
|
|
9922
10371
|
ref: target.base ?? "",
|
|
@@ -9935,6 +10384,7 @@ function pendingChangeSet(target) {
|
|
|
9935
10384
|
repoPath: target.originRepoPath,
|
|
9936
10385
|
headRef: target.branch,
|
|
9937
10386
|
...target.ref ? { headResolvedRef: target.ref } : {},
|
|
10387
|
+
...commitIdentity(target, void 0),
|
|
9938
10388
|
baseRef: target.base ?? "",
|
|
9939
10389
|
mergeBase: "",
|
|
9940
10390
|
includesUncommitted: false,
|
|
@@ -9943,6 +10393,27 @@ function pendingChangeSet(target) {
|
|
|
9943
10393
|
};
|
|
9944
10394
|
}
|
|
9945
10395
|
/**
|
|
10396
|
+
* What makes a stored change set a commit review, carried from wherever it is known.
|
|
10397
|
+
*
|
|
10398
|
+
* Two sources, because a target is rebuilt from a stored review in three places (`reanalyseReview`,
|
|
10399
|
+
* `pinned_checkout.ts`, `kiss_run.ts`) and none of them has the `NamedCommit` the original command
|
|
10400
|
+
* resolved. They do not need it: the stored record already says what it is, and that answer is the
|
|
10401
|
+
* one to keep. A fresh target wins when there is one, since it is where the answer was decided.
|
|
10402
|
+
*/
|
|
10403
|
+
function commitIdentity(target, stored) {
|
|
10404
|
+
if (target.commit) return {
|
|
10405
|
+
targetType: "commit",
|
|
10406
|
+
parentSha: target.commit.parentSha,
|
|
10407
|
+
parentCount: target.commit.parentCount
|
|
10408
|
+
};
|
|
10409
|
+
if (stored?.targetType === "commit") return {
|
|
10410
|
+
targetType: "commit",
|
|
10411
|
+
parentSha: stored.parentSha ?? null,
|
|
10412
|
+
...stored.parentCount !== void 0 ? { parentCount: stored.parentCount } : {}
|
|
10413
|
+
};
|
|
10414
|
+
return {};
|
|
10415
|
+
}
|
|
10416
|
+
/**
|
|
9946
10417
|
* Rebuild the prompt a phase needs, from what the job and the workspace already hold.
|
|
9947
10418
|
*
|
|
9948
10419
|
* Not cached in memory: the daemon can restart between `context` finishing and `analysing`
|
|
@@ -10965,6 +11436,8 @@ function explainUpdateFailure(error, env, requestedModel) {
|
|
|
10965
11436
|
var RefreshUnavailableError = class extends Error {};
|
|
10966
11437
|
/** A fetch reaches the network, so it gets a ceiling. Nothing else here can hang. */
|
|
10967
11438
|
var FETCH_TIMEOUT_MS = 6e4;
|
|
11439
|
+
/** Said wherever an update is asked of a commit review. One sentence, so both surfaces agree. */
|
|
11440
|
+
var COMMIT_REVIEW_CANNOT_UPDATE = "This review is of one commit, and a commit cannot move, so there is nothing to bring it up to date with. To analyse the same commit again, reanalyse it instead.";
|
|
10968
11441
|
/**
|
|
10969
11442
|
* Computes and mutates the workspace in memory; does not persist.
|
|
10970
11443
|
*
|
|
@@ -10973,6 +11446,7 @@ var FETCH_TIMEOUT_MS = 6e4;
|
|
|
10973
11446
|
*/
|
|
10974
11447
|
async function updateReview(workspace, runners = {}) {
|
|
10975
11448
|
const reviewedSha = workspace.session.metadata?.commitSha || null;
|
|
11449
|
+
if (workspace.changeSet.targetType === "commit") return refuse(reviewedSha, false, COMMIT_REVIEW_CANNOT_UPDATE);
|
|
10976
11450
|
const held = await claimCheckoutJob(workspace, reviewedSha ?? "", {
|
|
10977
11451
|
purpose: "update",
|
|
10978
11452
|
...workspace.prContext ? { conversation: {
|
|
@@ -10985,10 +11459,14 @@ async function updateReview(workspace, runners = {}) {
|
|
|
10985
11459
|
const step = (phase) => saveJob(advance(current(), phase));
|
|
10986
11460
|
try {
|
|
10987
11461
|
const outcome = await bringForward(workspace, runners, reviewedSha, held, step);
|
|
10988
|
-
|
|
11462
|
+
const ended = outcome.stop === "analysis-failed" ? fail(current(), outcome.message) : advance(current(), "done", outcome.message);
|
|
11463
|
+
saveJob(ended);
|
|
11464
|
+
announceJob(ended);
|
|
10989
11465
|
return outcome;
|
|
10990
11466
|
} catch (error) {
|
|
10991
|
-
|
|
11467
|
+
const failed = fail(current(), error instanceof Error ? error.message : String(error));
|
|
11468
|
+
saveJob(failed);
|
|
11469
|
+
announceJob(failed);
|
|
10992
11470
|
throw error;
|
|
10993
11471
|
} finally {
|
|
10994
11472
|
held.claim.release();
|
|
@@ -11578,4 +12056,4 @@ function lineageIds(id) {
|
|
|
11578
12056
|
return [.../* @__PURE__ */ new Set([id, ...lineage.map((review) => review.id)])];
|
|
11579
12057
|
}
|
|
11580
12058
|
//#endregion
|
|
11581
|
-
export {
|
|
12059
|
+
export { feedbackUrl as $, reviewerDispositions as $t, markRead as A, loadWorkspace as At, modelHelpLines as B, workspaceRevision as Bt, recordTelemetryUploadConsent as C, isClaimed as Ct, headline as D, latestKissRun as Dt, eventName as E, indexReviewGroups as Et, EXPLAIN_SIMPLY_PROMPT as F, reviewedCommit as Ft, ensureServer as G, queueStamp as Gt, readIndexToken as H, dequeueOnComplete as Ht, ReviewBeingDeletedError as I, reviewedRepositories as It, stopServer as J, doneVerb as Jt, readServerRecord as K, readQueue as Kt, MANAGED_ROOT as L, saveWorkspace as Lt, readNotification as M, positionInLineage as Mt, unreadCount as N, previousKissResult as Nt, listNotifications as O, lineageKeyFor as Ot, withUsageRecorded as P, recentWorkspaces as Pt, PACKAGE_NAME as Q, questionsOutstanding as Qt, readMarker as R, summarise as Rt, record as S, driverGone as St, announceJob as T, groupByLineage as Tt, bootstrapUrl as U, queueAdd as Ut, relativeTime as V, workspaceStamps as Vt, clearServerRecord as W, queueRemove as Wt, writeServerRecord as X, isQuestionOutstanding as Xt, workspaceUrl as Y, isIssueOutstanding as Yt, BUILD_VERSION as Z, issuesOutstanding as Zt, startKissJob as _, AgentCancelledError as _n, PHASES as _t, wasBlocked as a, resolveAgent as an, statedWorkspacePort as at, startJob as b, purposeOf as bt, updateReview as c, readAgentPreference as cn, fillFileUrlTemplate as ct, askCheckout as d, STORE_ROOT as dn, allJobs as dt, processDiscussion as en, DEFAULT_WORKSPACE_PORT as et, UpdateAlreadyRunningError as f, AGENT_IDS as fn, endedAt as ft, recordKissRun as g, detectAgents as gn, saveJob as gt, agentEnvOf as h, agentFor as hn, loadJob as ht, liveJobsFor as i, git as in, resolveWorkspacePort as it, pruneNotifications as j, matchingWorkspaceIds as jt, markAllRead as k, lineagePosition as kt, refreshPrContext as l, writeAgentPreference as ln, checkCodeFreshness as lt, runningJobs as m, agentById as mn, isTerminal as mt, deleteReview as n, resolveTarget as nn, MIN_WORKSPACE_PORT as nt, discardJob as o, resolveModel as on, writePortPreference as ot, liveUpdateFor as p, DEFAULT_AGENT_ID as pn, fail as pt, reviewsUrl as q, writeQueue as qt, lineageIds as r, displayRef as rn, parseWorkspacePort as rt, RefreshUnavailableError as s, clearAgentPreference as sn, forgeResolver as st, deleteLineage as t, describeAuthorship as tn, MAX_WORKSPACE_PORT as tt, isUnchanged as u, CONFIG_PATH as un, checkReview as ut, reanalyseReview as v, AgentUnavailableError as vn, phasesFor as vt, telemetryUploadConsent as w, followedRefName as wt, readEvents as x, clearClaim as xt, runJob as y, progressSteps as yt, removeWorktree as z, touchWorkspace as zt };
|