@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 [refRange]` — review the working tree (or a
4
- * ref range) without GitHub and without piping a diff through stdin.
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
- * @param _ config (unused in this source)
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`. When omitted, diffs the working tree against the index (plain `git diff`).
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 content).
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(_: ProviderConfig | null, refRange: string | undefined): Promise<string | null>;
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` can emit multi-megabyte output for large changes; the node default (1 MiB)
4
- * would truncate-and-fail such runs.
5
- */
6
- const MAX_DIFF_BUFFER = 32 * 1024 * 1024;
7
- /**
8
- * Gets a local diff via `git --no-pager diff [refRange]` — review the working tree (or a
9
- * ref range) without GitHub and without piping a diff through stdin.
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
- * @param _ config (unused in this source)
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`. When omitted, diffs the working tree against the index (plain `git diff`).
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 content).
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(_, refRange) {
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 the ref range is valid.`);
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":"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"}
1
+ {"version":3,"file":"gitDiffSource.js","sourceRoot":"","sources":["../../src/sources/gitDiffSource.ts"],"names":[],"mappings":"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"}
@@ -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.16",
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.16"
41
+ "@gaunt-sloth/core": "2.0.0-beta.17"
42
42
  },
43
43
  "files": [
44
44
  "./dist/*",