@gaunt-sloth/review 2.0.0-beta.16 → 2.0.0-beta.17
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.
|
@@ -1,12 +1,33 @@
|
|
|
1
1
|
import type { ProviderConfig } from './types.js';
|
|
2
2
|
/**
|
|
3
|
-
* Gets a local diff via `git --no-pager diff
|
|
4
|
-
*
|
|
3
|
+
* Gets a local diff via `git --no-pager diff` — review the working tree, a ref range, or a
|
|
4
|
+
* branch against its merge base, without GitHub and without piping a diff through stdin.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* Which diff, in order:
|
|
7
|
+
*
|
|
8
|
+
* 1. An explicit `refRange` always wins and keeps its plain meaning, `git diff <refRange>`,
|
|
9
|
+
* whatever `mergeBase` says. It is the more specific request: the user typed it for this run,
|
|
10
|
+
* while `mergeBase` is a standing default from config.
|
|
11
|
+
* 2. Otherwise, with `mergeBase` set, the working tree against the merge base of that ref and
|
|
12
|
+
* `HEAD` — what the branch's pull request would show, plus uncommitted edits to tracked files.
|
|
13
|
+
* The base is resolved to a sha by {@link resolveMergeBase} (see there for why a sha, and why
|
|
14
|
+
* an unresolvable base fails instead of falling back), and the diff is
|
|
15
|
+
* `git diff <sha> --`: the `--` stops git reading the sha as a path when a file of that name
|
|
16
|
+
* exists. Untracked files are not in it, as in any `git diff`.
|
|
17
|
+
* 3. Otherwise the working tree against the index (plain `git diff`).
|
|
18
|
+
*
|
|
19
|
+
* `mergeBase` is validated here rather than in core's config schema, which keeps every
|
|
20
|
+
* content source's block opaque (`z.record(z.string(), z.unknown())`) and leaves each source to
|
|
21
|
+
* own its keys. It is validated on every call, even when an explicit `refRange` means it will not
|
|
22
|
+
* be used: a malformed value is a config error that should surface on the first run, not on the
|
|
23
|
+
* first run that happens to omit the id.
|
|
24
|
+
*
|
|
25
|
+
* @param config `contentSourceConfig.git`, if any. Recognised key: `mergeBase` (string); any
|
|
26
|
+
* other key is ignored with a warning, so a typo such as `mergebase` is reported.
|
|
7
27
|
* @param refRange optional revision selection passed to `git diff`, e.g. `origin/main...HEAD`
|
|
8
|
-
* or `HEAD~3`.
|
|
28
|
+
* or `HEAD~3`.
|
|
9
29
|
* @returns the diff content; throws with a clear message outside a git repository, on a bad
|
|
10
|
-
* ref, or when the diff is empty (an empty review would otherwise run against no
|
|
30
|
+
* ref or merge base, or when the diff is empty (an empty review would otherwise run against no
|
|
31
|
+
* content).
|
|
11
32
|
*/
|
|
12
|
-
export declare function get(
|
|
33
|
+
export declare function get(config: ProviderConfig | null, refRange: string | undefined): Promise<string | null>;
|
|
@@ -1,28 +1,103 @@
|
|
|
1
1
|
import { ProgressIndicator } from '@gaunt-sloth/core/utils/ProgressIndicator.js';
|
|
2
|
+
import { displayWarning } from '@gaunt-sloth/core/utils/consoleUtils.js';
|
|
3
|
+
import { MERGE_BASE_SETTING, resolveMergeBase, runGit } from '#src/utils/git.js';
|
|
4
|
+
/** The keys this source reads from `contentSourceConfig.git`. */
|
|
5
|
+
const KNOWN_GIT_CONFIG_KEYS = ['mergeBase'];
|
|
2
6
|
/**
|
|
3
|
-
* `git diff`
|
|
4
|
-
*
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* Gets a local diff via `git --no-pager diff` — review the working tree, a ref range, or a
|
|
8
|
+
* branch against its merge base, without GitHub and without piping a diff through stdin.
|
|
9
|
+
*
|
|
10
|
+
* Which diff, in order:
|
|
11
|
+
*
|
|
12
|
+
* 1. An explicit `refRange` always wins and keeps its plain meaning, `git diff <refRange>`,
|
|
13
|
+
* whatever `mergeBase` says. It is the more specific request: the user typed it for this run,
|
|
14
|
+
* while `mergeBase` is a standing default from config.
|
|
15
|
+
* 2. Otherwise, with `mergeBase` set, the working tree against the merge base of that ref and
|
|
16
|
+
* `HEAD` — what the branch's pull request would show, plus uncommitted edits to tracked files.
|
|
17
|
+
* The base is resolved to a sha by {@link resolveMergeBase} (see there for why a sha, and why
|
|
18
|
+
* an unresolvable base fails instead of falling back), and the diff is
|
|
19
|
+
* `git diff <sha> --`: the `--` stops git reading the sha as a path when a file of that name
|
|
20
|
+
* exists. Untracked files are not in it, as in any `git diff`.
|
|
21
|
+
* 3. Otherwise the working tree against the index (plain `git diff`).
|
|
10
22
|
*
|
|
11
|
-
*
|
|
23
|
+
* `mergeBase` is validated here rather than in core's config schema, which keeps every
|
|
24
|
+
* content source's block opaque (`z.record(z.string(), z.unknown())`) and leaves each source to
|
|
25
|
+
* own its keys. It is validated on every call, even when an explicit `refRange` means it will not
|
|
26
|
+
* be used: a malformed value is a config error that should surface on the first run, not on the
|
|
27
|
+
* first run that happens to omit the id.
|
|
28
|
+
*
|
|
29
|
+
* @param config `contentSourceConfig.git`, if any. Recognised key: `mergeBase` (string); any
|
|
30
|
+
* other key is ignored with a warning, so a typo such as `mergebase` is reported.
|
|
12
31
|
* @param refRange optional revision selection passed to `git diff`, e.g. `origin/main...HEAD`
|
|
13
|
-
* or `HEAD~3`.
|
|
32
|
+
* or `HEAD~3`.
|
|
14
33
|
* @returns the diff content; throws with a clear message outside a git repository, on a bad
|
|
15
|
-
* ref, or when the diff is empty (an empty review would otherwise run against no
|
|
34
|
+
* ref or merge base, or when the diff is empty (an empty review would otherwise run against no
|
|
35
|
+
* content).
|
|
16
36
|
*/
|
|
17
|
-
export async function get(
|
|
37
|
+
export async function get(config, refRange) {
|
|
18
38
|
// Args go to execFile (no shell), so shell metacharacters are inert; still reject
|
|
19
39
|
// option-shaped input so an id can never become a git flag (e.g. `--output=<file>`).
|
|
20
40
|
if (refRange && refRange.startsWith('-')) {
|
|
21
41
|
throw new Error(`Invalid git diff argument "${refRange}"; expected a ref or ref range (e.g. "origin/main...HEAD"), not an option.`);
|
|
22
42
|
}
|
|
43
|
+
const mergeBase = readMergeBase(config);
|
|
44
|
+
if (!refRange && mergeBase !== undefined) {
|
|
45
|
+
return getMergeBaseDiff(mergeBase);
|
|
46
|
+
}
|
|
23
47
|
const label = refRange ? `for "${refRange}"` : 'for the working tree';
|
|
24
48
|
const gitArgs = ['--no-pager', 'diff', ...(refRange ? [refRange] : [])];
|
|
25
49
|
const progress = new ProgressIndicator(`Getting local git diff ${label}`);
|
|
50
|
+
return runDiff(progress, label, gitArgs, 'the ref range is valid');
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Read `mergeBase` from the git source's config block. `undefined` when unset; throws, naming
|
|
54
|
+
* the setting, for anything that is set but is not a non-empty string (and for a block that is
|
|
55
|
+
* not an object), so a mistyped value is reported instead of silently ignored.
|
|
56
|
+
*
|
|
57
|
+
* Any other key in the block gets a warning, not an error. A mistyped key such as `mergebase`
|
|
58
|
+
* would otherwise review the plain working-tree diff with nothing said. It is not rejected
|
|
59
|
+
* because a project config is shared by people on different gth versions: a key a later release
|
|
60
|
+
* adds must not break an older one's reviews. That matches the top-level rule in core's config
|
|
61
|
+
* schema, where unknown keys warn and only known deprecated names fail.
|
|
62
|
+
*/
|
|
63
|
+
function readMergeBase(config) {
|
|
64
|
+
if (config === null || config === undefined) {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
if (typeof config !== 'object' || Array.isArray(config)) {
|
|
68
|
+
throw new Error(`Invalid contentSourceConfig.git; expected an object such as { "mergeBase": "origin/main" }.`);
|
|
69
|
+
}
|
|
70
|
+
const unknownKeys = Object.keys(config).filter((key) => !KNOWN_GIT_CONFIG_KEYS.includes(key));
|
|
71
|
+
if (unknownKeys.length > 0) {
|
|
72
|
+
const names = unknownKeys.map((key) => `"${key}"`).join(', ');
|
|
73
|
+
const noun = unknownKeys.length === 1 ? 'key' : 'keys';
|
|
74
|
+
const verb = unknownKeys.length === 1 ? 'is' : 'are';
|
|
75
|
+
displayWarning(`contentSourceConfig.git: unknown ${noun} ${names} ${verb} ignored; known keys: ${KNOWN_GIT_CONFIG_KEYS.join(', ')}.`);
|
|
76
|
+
}
|
|
77
|
+
const value = config.mergeBase;
|
|
78
|
+
if (value === undefined) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
82
|
+
throw new Error(`Invalid ${MERGE_BASE_SETTING} ${JSON.stringify(value) ?? String(value)}; expected a non-empty string naming a ref, e.g. "origin/main".`);
|
|
83
|
+
}
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
/** The working tree against the merge base of `mergeBase` and `HEAD`. */
|
|
87
|
+
async function getMergeBaseDiff(mergeBase) {
|
|
88
|
+
const target = `the merge base of "${mergeBase}" and HEAD`;
|
|
89
|
+
const progress = new ProgressIndicator(`Getting local git diff against ${target}`);
|
|
90
|
+
let sha;
|
|
91
|
+
try {
|
|
92
|
+
sha = await resolveMergeBase(mergeBase);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
progress.stop();
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
return runDiff(progress, `against ${target} (${sha})`, ['--no-pager', 'diff', sha, '--'], `${MERGE_BASE_SETTING} names a ref in this repository`);
|
|
99
|
+
}
|
|
100
|
+
async function runDiff(progress, label, gitArgs, hint) {
|
|
26
101
|
try {
|
|
27
102
|
const diffContent = await runGit(gitArgs);
|
|
28
103
|
progress.stop();
|
|
@@ -37,39 +112,7 @@ export async function get(_, refRange) {
|
|
|
37
112
|
if (reason.startsWith('No changes found')) {
|
|
38
113
|
throw new Error(reason);
|
|
39
114
|
}
|
|
40
|
-
throw new Error(`Failed to get git diff ${label}: ${reason}\nConsider checking that you are inside a git repository and
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Run git with an args array (execFile, no shell). Rejects on a non-zero exit or spawn
|
|
45
|
-
* failure with git's stderr as the reason; benign stderr chatter on a zero exit is ignored
|
|
46
|
-
* (unlike systemUtils.execAsync, which rejects on any stderr output).
|
|
47
|
-
*/
|
|
48
|
-
async function runGit(args) {
|
|
49
|
-
const { execFile } = await import('node:child_process');
|
|
50
|
-
return new Promise((resolve, reject) => {
|
|
51
|
-
execFile('git', args, { maxBuffer: MAX_DIFF_BUFFER }, (error, stdout, stderr) => {
|
|
52
|
-
if (error) {
|
|
53
|
-
reject(new Error(extractGitError(stderr ?? '', error.message)));
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
resolve(stdout);
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Reduce git's stderr to the one meaningful line. Outside a repository `git diff` appends
|
|
62
|
-
* its entire `--no-index` usage screen after the warning line; that wall of text is noise
|
|
63
|
-
* in a CLI error. Prefer the `fatal:` line when present, else the first non-empty line.
|
|
64
|
-
*/
|
|
65
|
-
function extractGitError(stderr, fallback) {
|
|
66
|
-
const lines = stderr
|
|
67
|
-
.split('\n')
|
|
68
|
-
.map((line) => line.trim())
|
|
69
|
-
.filter(Boolean);
|
|
70
|
-
if (lines.length === 0) {
|
|
71
|
-
return fallback;
|
|
115
|
+
throw new Error(`Failed to get git diff ${label}: ${reason}\nConsider checking that you are inside a git repository and ${hint}.`);
|
|
72
116
|
}
|
|
73
|
-
return lines.find((line) => line.startsWith('fatal:')) ?? lines[0];
|
|
74
117
|
}
|
|
75
118
|
//# sourceMappingURL=gitDiffSource.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gitDiffSource.js","sourceRoot":"","sources":["../../src/sources/gitDiffSource.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,8CAA8C,CAAC;
|
|
1
|
+
{"version":3,"file":"gitDiffSource.js","sourceRoot":"","sources":["../../src/sources/gitDiffSource.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,8CAA8C,CAAC;AACjF,OAAO,EAAE,cAAc,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAEjF,iEAAiE;AACjE,MAAM,qBAAqB,GAAG,CAAC,WAAW,CAAU,CAAC;AAErD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,CAAC,KAAK,UAAU,GAAG,CACvB,MAA6B,EAC7B,QAA4B;IAE5B,kFAAkF;IAClF,qFAAqF;IACrF,IAAI,QAAQ,IAAI,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACzC,MAAM,IAAI,KAAK,CACb,8BAA8B,QAAQ,4EAA4E,CACnH,CAAC;IACJ,CAAC;IAED,MAAM,SAAS,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;IAExC,IAAI,CAAC,QAAQ,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;QACzC,OAAO,gBAAgB,CAAC,SAAS,CAAC,CAAC;IACrC,CAAC;IAED,MAAM,KAAK,GAAG,QAAQ,CAAC,CAAC,CAAC,QAAQ,QAAQ,GAAG,CAAC,CAAC,CAAC,sBAAsB,CAAC;IACtE,MAAM,OAAO,GAAG,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAExE,MAAM,QAAQ,GAAG,IAAI,iBAAiB,CAAC,0BAA0B,KAAK,EAAE,CAAC,CAAC;IAC1E,OAAO,OAAO,CAAC,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,wBAAwB,CAAC,CAAC;AACrE,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAS,aAAa,CAAC,MAAyC;IAC9D,IAAI,MAAM,KAAK,IAAI,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QAC5C,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QACxD,MAAM,IAAI,KAAK,CACb,6FAA6F,CAC9F,CAAC;IACJ,CAAC;IACD,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,CAC5C,CAAC,GAAG,EAAE,EAAE,CAAC,CAAE,qBAA2C,CAAC,QAAQ,CAAC,GAAG,CAAC,CACrE,CAAC;IACF,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC3B,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,GAAG,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC9D,MAAM,IAAI,GAAG,WAAW,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC;QACvD,MAAM,IAAI,GAAG,WAAW,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC;QACrD,cAAc,CACZ,oCAAoC,IAAI,IAAI,KAAK,IAAI,IAAI,yBAAyB,qBAAqB,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CACtH,CAAC;IACJ,CAAC;IACD,MAAM,KAAK,GAAG,MAAM,CAAC,SAAS,CAAC;IAC/B,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACpD,MAAM,IAAI,KAAK,CACb,WAAW,kBAAkB,IAAI,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,iEAAiE,CACzI,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,yEAAyE;AACzE,KAAK,UAAU,gBAAgB,CAAC,SAAiB;IAC/C,MAAM,MAAM,GAAG,sBAAsB,SAAS,YAAY,CAAC;IAC3D,MAAM,QAAQ,GAAG,IAAI,iBAAiB,CAAC,kCAAkC,MAAM,EAAE,CAAC,CAAC;IACnF,IAAI,GAAW,CAAC;IAChB,IAAI,CAAC;QACH,GAAG,GAAG,MAAM,gBAAgB,CAAC,SAAS,CAAC,CAAC;IAC1C,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,QAAQ,CAAC,IAAI,EAAE,CAAC;QAChB,MAAM,KAAK,CAAC;IACd,CAAC;IACD,OAAO,OAAO,CACZ,QAAQ,EACR,WAAW,MAAM,KAAK,GAAG,GAAG,EAC5B,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,CAAC,EACjC,GAAG,kBAAkB,iCAAiC,CACvD,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,OAAO,CACpB,QAA2B,EAC3B,KAAa,EACb,OAAiB,EACjB,IAAY;IAEZ,IAAI,CAAC;QACH,MAAM,WAAW,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,CAAC;QAC1C,QAAQ,CAAC,IAAI,EAAE,CAAC;QAEhB,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CAAC,gCAAgC,KAAK,sBAAsB,CAAC,CAAC;QAC/E,CAAC;QAED,OAAO,kBAAkB,KAAK,OAAO,WAAW,EAAE,CAAC;IACrD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,QAAQ,CAAC,IAAI,EAAE,CAAC;QAChB,MAAM,MAAM,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACtE,IAAI,MAAM,CAAC,UAAU,CAAC,kBAAkB,CAAC,EAAE,CAAC;YAC1C,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC;QAC1B,CAAC;QACD,MAAM,IAAI,KAAK,CACb,0BAA0B,KAAK,KAAK,MAAM,gEAAgE,IAAI,GAAG,CAClH,CAAC;IACJ,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** The config key that names the base a branch is reviewed against. */
|
|
2
|
+
export declare const MERGE_BASE_SETTING = "contentSourceConfig.git.mergeBase";
|
|
3
|
+
/**
|
|
4
|
+
* A git invocation that failed. `exitCode` is git's own exit status when git ran and exited
|
|
5
|
+
* non-zero, and `undefined` when it could not be spawned at all; callers use it to tell apart
|
|
6
|
+
* failures that print nothing (`git merge-base` exits 1 silently when there is no common
|
|
7
|
+
* ancestor) from the ones that print a `fatal:` line.
|
|
8
|
+
*/
|
|
9
|
+
export declare class GitCommandError extends Error {
|
|
10
|
+
readonly exitCode: number | undefined;
|
|
11
|
+
constructor(message: string, exitCode: number | undefined);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Run git with an args array (execFile, no shell) in the current working directory. Rejects
|
|
15
|
+
* with a {@link GitCommandError} on a non-zero exit or spawn failure, with git's stderr reduced
|
|
16
|
+
* to its meaningful line as the message; benign stderr chatter on a zero exit is ignored (unlike
|
|
17
|
+
* systemUtils.execAsync, which rejects on any stderr output).
|
|
18
|
+
*/
|
|
19
|
+
export declare function runGit(args: string[]): Promise<string>;
|
|
20
|
+
/**
|
|
21
|
+
* Resolve the merge base of `base` and `HEAD` to a commit sha, with `git merge-base <base> HEAD`.
|
|
22
|
+
*
|
|
23
|
+
* This is the ONE place the merge base is computed, and it is shared on purpose: the git content
|
|
24
|
+
* source diffs against it, and a later reader that lists the branch's commits (as requirements
|
|
25
|
+
* evidence) must compute its range from this same function. Two resolvers are how a reviewed
|
|
26
|
+
* diff and the evidence beside it would come to describe different branches.
|
|
27
|
+
*
|
|
28
|
+
* Why a sha rather than `git diff --merge-base <base>`: that flag needs git 2.30+, and it would
|
|
29
|
+
* put a flag on git's command line that came from the caller's side of the seam. Resolving first
|
|
30
|
+
* works on any git, and what reaches `git diff` afterwards is a validated object name, never
|
|
31
|
+
* something option-shaped.
|
|
32
|
+
*
|
|
33
|
+
* An unresolvable base throws; it never falls back to some other diff. A fallback (the plain
|
|
34
|
+
* working-tree diff, say) would silently review LESS than the user configured, which is worse
|
|
35
|
+
* than stopping. The messages name `origin` (by default the config key) so the user knows which
|
|
36
|
+
* setting to fix.
|
|
37
|
+
*
|
|
38
|
+
* @param base the configured ref, e.g. `origin/main`; rejected if it starts with `-`, so a value
|
|
39
|
+
* from config can never become a git flag.
|
|
40
|
+
* @param origin what to call the value in error messages.
|
|
41
|
+
* @returns the full merge-base commit sha.
|
|
42
|
+
*/
|
|
43
|
+
export declare function resolveMergeBase(base: string, origin?: string): Promise<string>;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `git diff` can emit multi-megabyte output for large changes; the node default (1 MiB)
|
|
3
|
+
* would truncate-and-fail such runs.
|
|
4
|
+
*/
|
|
5
|
+
const MAX_GIT_BUFFER = 32 * 1024 * 1024;
|
|
6
|
+
/** The config key that names the base a branch is reviewed against. */
|
|
7
|
+
export const MERGE_BASE_SETTING = 'contentSourceConfig.git.mergeBase';
|
|
8
|
+
/**
|
|
9
|
+
* A git invocation that failed. `exitCode` is git's own exit status when git ran and exited
|
|
10
|
+
* non-zero, and `undefined` when it could not be spawned at all; callers use it to tell apart
|
|
11
|
+
* failures that print nothing (`git merge-base` exits 1 silently when there is no common
|
|
12
|
+
* ancestor) from the ones that print a `fatal:` line.
|
|
13
|
+
*/
|
|
14
|
+
export class GitCommandError extends Error {
|
|
15
|
+
exitCode;
|
|
16
|
+
constructor(message, exitCode) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = 'GitCommandError';
|
|
19
|
+
this.exitCode = exitCode;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Run git with an args array (execFile, no shell) in the current working directory. Rejects
|
|
24
|
+
* with a {@link GitCommandError} on a non-zero exit or spawn failure, with git's stderr reduced
|
|
25
|
+
* to its meaningful line as the message; benign stderr chatter on a zero exit is ignored (unlike
|
|
26
|
+
* systemUtils.execAsync, which rejects on any stderr output).
|
|
27
|
+
*/
|
|
28
|
+
export async function runGit(args) {
|
|
29
|
+
const { execFile } = await import('node:child_process');
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
execFile('git', args, { maxBuffer: MAX_GIT_BUFFER }, (error, stdout, stderr) => {
|
|
32
|
+
if (error) {
|
|
33
|
+
const code = error.code;
|
|
34
|
+
reject(new GitCommandError(extractGitError(stderr ?? '', error.message), typeof code === 'number' ? code : undefined));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
resolve(stdout);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Reduce git's stderr to the one meaningful line. Outside a repository `git diff` appends
|
|
43
|
+
* its entire `--no-index` usage screen after the warning line; that wall of text is noise
|
|
44
|
+
* in a CLI error. Prefer the `fatal:` line when present, else the first non-empty line.
|
|
45
|
+
*/
|
|
46
|
+
function extractGitError(stderr, fallback) {
|
|
47
|
+
const lines = stderr
|
|
48
|
+
.split('\n')
|
|
49
|
+
.map((line) => line.trim())
|
|
50
|
+
.filter(Boolean);
|
|
51
|
+
if (lines.length === 0) {
|
|
52
|
+
return fallback;
|
|
53
|
+
}
|
|
54
|
+
return lines.find((line) => line.startsWith('fatal:')) ?? lines[0];
|
|
55
|
+
}
|
|
56
|
+
/** A full object name as `git merge-base` prints it: SHA-1 (40) or SHA-256 (64) hex. */
|
|
57
|
+
const OBJECT_NAME = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
58
|
+
/**
|
|
59
|
+
* Resolve the merge base of `base` and `HEAD` to a commit sha, with `git merge-base <base> HEAD`.
|
|
60
|
+
*
|
|
61
|
+
* This is the ONE place the merge base is computed, and it is shared on purpose: the git content
|
|
62
|
+
* source diffs against it, and a later reader that lists the branch's commits (as requirements
|
|
63
|
+
* evidence) must compute its range from this same function. Two resolvers are how a reviewed
|
|
64
|
+
* diff and the evidence beside it would come to describe different branches.
|
|
65
|
+
*
|
|
66
|
+
* Why a sha rather than `git diff --merge-base <base>`: that flag needs git 2.30+, and it would
|
|
67
|
+
* put a flag on git's command line that came from the caller's side of the seam. Resolving first
|
|
68
|
+
* works on any git, and what reaches `git diff` afterwards is a validated object name, never
|
|
69
|
+
* something option-shaped.
|
|
70
|
+
*
|
|
71
|
+
* An unresolvable base throws; it never falls back to some other diff. A fallback (the plain
|
|
72
|
+
* working-tree diff, say) would silently review LESS than the user configured, which is worse
|
|
73
|
+
* than stopping. The messages name `origin` (by default the config key) so the user knows which
|
|
74
|
+
* setting to fix.
|
|
75
|
+
*
|
|
76
|
+
* @param base the configured ref, e.g. `origin/main`; rejected if it starts with `-`, so a value
|
|
77
|
+
* from config can never become a git flag.
|
|
78
|
+
* @param origin what to call the value in error messages.
|
|
79
|
+
* @returns the full merge-base commit sha.
|
|
80
|
+
*/
|
|
81
|
+
export async function resolveMergeBase(base, origin = MERGE_BASE_SETTING) {
|
|
82
|
+
if (base.startsWith('-')) {
|
|
83
|
+
throw new Error(`Invalid ${origin} "${base}"; expected a ref (e.g. "origin/main"), not an option.`);
|
|
84
|
+
}
|
|
85
|
+
let output;
|
|
86
|
+
try {
|
|
87
|
+
output = await runGit(['merge-base', base, 'HEAD']);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
// `git merge-base` exits 1 and prints nothing when the two commits share no history.
|
|
91
|
+
if (error instanceof GitCommandError && error.exitCode === 1) {
|
|
92
|
+
throw new Error(`${origin} "${base}" has no common ancestor with HEAD, so there is no merge base to review against.`);
|
|
93
|
+
}
|
|
94
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
95
|
+
throw new Error(`${origin} "${base}" could not be resolved to a merge base: ${reason}`);
|
|
96
|
+
}
|
|
97
|
+
const sha = output.trim();
|
|
98
|
+
if (!OBJECT_NAME.test(sha)) {
|
|
99
|
+
throw new Error(`${origin} "${base}" could not be resolved to a merge base: git merge-base printed "${sha}".`);
|
|
100
|
+
}
|
|
101
|
+
return sha;
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=git.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git.js","sourceRoot":"","sources":["../../src/utils/git.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gaunt-sloth/review",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.17",
|
|
4
4
|
"description": "Review functionality for Gaunt Sloth",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Andrew Kondratev",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"@langchain/langgraph": "^1.4.16",
|
|
39
39
|
"langchain": "^1.5.11",
|
|
40
40
|
"zod": "^4.6.5",
|
|
41
|
-
"@gaunt-sloth/core": "2.0.0-beta.
|
|
41
|
+
"@gaunt-sloth/core": "2.0.0-beta.17"
|
|
42
42
|
},
|
|
43
43
|
"files": [
|
|
44
44
|
"./dist/*",
|