@mcarvin/smart-diff 2.3.2 → 3.0.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/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  [![codecov](https://codecov.io/gh/mcarvin8/smart-diff/graph/badge.svg?token=H3ZWAGG7S9)](https://codecov.io/gh/mcarvin8/smart-diff)
8
8
  [![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fmcarvin8%2Fsmart-diff%2Fmain)](https://dashboard.stryker-mutator.io/reports/github.com/mcarvin8/smart-diff/main)
9
9
 
10
- TypeScript library that turns a **git revision range** into a **Markdown summary** using any LLM provider supported by the [Vercel AI SDK](https://sdk.vercel.ai) — OpenAI, Anthropic, Google Gemini, Amazon Bedrock, Mistral, Cohere, Groq, xAI, DeepSeek, or any OpenAI-compatible gateway. It uses [`simple-git`](https://github.com/steveukx/git-js) to read the repo, respects **path includes/excludes** and **commit message include/exclude regexes**, and sends commits, paths, structured diff stats, and unified diff text to the model.
10
+ TypeScript library that turns a **git revision range** into a **Markdown summary** using any LLM provider supported by the [Vercel AI SDK](https://sdk.vercel.ai) — OpenAI, Anthropic, Google Gemini, Amazon Bedrock, Mistral, Cohere, Groq, xAI, DeepSeek, or any OpenAI-compatible gateway. It shells out to `git` to read the repo, respects **path includes/excludes** and **commit message include/exclude regexes**, and sends commits, paths, structured diff stats, and unified diff text to the model.
11
11
 
12
12
  ## Requirements
13
13
 
@@ -21,7 +21,28 @@ TypeScript library that turns a **git revision range** into a **Markdown summary
21
21
  npm install @mcarvin/smart-diff
22
22
  ```
23
23
 
24
- `@ai-sdk/openai` and `@ai-sdk/openai-compatible` ship as direct dependencies. Every other provider (`@ai-sdk/anthropic`, `@ai-sdk/google`, `@ai-sdk/amazon-bedrock`, `@ai-sdk/mistral`, `@ai-sdk/cohere`, `@ai-sdk/groq`, `@ai-sdk/xai`, `@ai-sdk/deepseek`) is declared as an **optional peer** and only needs to be installed when you actually use that provider. If the package is missing, smart-diff throws a clear error telling you which one to install.
24
+ All provider packages are **optional** only install the one(s) you need:
25
+
26
+ ```bash
27
+ # OpenAI
28
+ npm install @ai-sdk/openai
29
+
30
+ # Anthropic
31
+ npm install @ai-sdk/anthropic
32
+
33
+ # Google Gemini
34
+ npm install @ai-sdk/google
35
+
36
+ # Amazon Bedrock
37
+ npm install @ai-sdk/amazon-bedrock
38
+
39
+ # OpenAI-compatible gateway (Azure, Ollama, Together, etc.)
40
+ npm install @ai-sdk/openai-compatible
41
+
42
+ # Others: @ai-sdk/mistral @ai-sdk/cohere @ai-sdk/groq @ai-sdk/xai @ai-sdk/deepseek
43
+ ```
44
+
45
+ If the package for the selected provider is missing at runtime, smart-diff throws a clear error telling you which one to install.
25
46
 
26
47
  ## Provider configuration
27
48
 
@@ -29,15 +50,15 @@ smart-diff is "configured" when [`isLlmProviderConfigured()`](#lower-level-api)
29
50
 
30
51
  ### Selecting a provider
31
52
 
32
- `LLM_PROVIDER` explicitly selects a provider. When unset, the resolver auto-detects in this order: `LLM_BASE_URL`/`OPENAI_BASE_URL` → `openai-compatible`, `OPENAI_API_KEY`/`LLM_API_KEY` → `openai`, then `ANTHROPIC_API_KEY`, `GOOGLE_GENERATIVE_AI_API_KEY` (or `GOOGLE_API_KEY`), `MISTRAL_API_KEY`, `COHERE_API_KEY`, `GROQ_API_KEY`, `XAI_API_KEY`, `DEEPSEEK_API_KEY`, and finally `OPENAI_DEFAULT_HEADERS`/`LLM_DEFAULT_HEADERS` → `openai`.
53
+ `LLM_PROVIDER` explicitly selects a provider. When unset, the resolver auto-detects in this order: `LLM_BASE_URL`/`OPENAI_BASE_URL` → `openai-compatible`, `OPENAI_API_KEY`/`LLM_API_KEY` → `openai`, then `ANTHROPIC_API_KEY`, `GOOGLE_GENERATIVE_AI_API_KEY` (or `GOOGLE_API_KEY`), `MISTRAL_API_KEY`, `COHERE_API_KEY`, `GROQ_API_KEY`, `XAI_API_KEY`, `DEEPSEEK_API_KEY`, `AWS_ACCESS_KEY_ID`/`AWS_PROFILE` → `bedrock`, and finally `OPENAI_DEFAULT_HEADERS`/`LLM_DEFAULT_HEADERS` → `openai`.
33
54
 
34
55
  | Provider (`LLM_PROVIDER`) | Package | Credential env vars | Default model |
35
56
  |---|---|---|---|
36
57
  | `openai` | `@ai-sdk/openai` | `OPENAI_API_KEY` or `LLM_API_KEY` | `gpt-4o-mini` |
37
58
  | `openai-compatible` | `@ai-sdk/openai-compatible` | `LLM_BASE_URL` or `OPENAI_BASE_URL` (required); `OPENAI_API_KEY`/`LLM_API_KEY` or custom headers | `gpt-4o-mini` |
38
- | `anthropic` | `@ai-sdk/anthropic` | `ANTHROPIC_API_KEY` | `claude-3-5-haiku-latest` |
59
+ | `anthropic` | `@ai-sdk/anthropic` | `ANTHROPIC_API_KEY` | `claude-haiku-4-5-20251001` |
39
60
  | `google` | `@ai-sdk/google` | `GOOGLE_GENERATIVE_AI_API_KEY` or `GOOGLE_API_KEY` | `gemini-2.0-flash` |
40
- | `bedrock` | `@ai-sdk/amazon-bedrock` | Standard AWS credential chain (env / profile / role) | `anthropic.claude-3-5-haiku-20241022-v1:0` |
61
+ | `bedrock` | `@ai-sdk/amazon-bedrock` | `AWS_ACCESS_KEY_ID` / `AWS_PROFILE` (auto-detects); full AWS credential chain supported | `anthropic.claude-3-5-haiku-20241022-v1:0` |
41
62
  | `mistral` | `@ai-sdk/mistral` | `MISTRAL_API_KEY` | `mistral-small-latest` |
42
63
  | `cohere` | `@ai-sdk/cohere` | `COHERE_API_KEY` | `command-r-08-2024` |
43
64
  | `groq` | `@ai-sdk/groq` | `GROQ_API_KEY` | `llama-3.1-8b-instant` |
@@ -57,6 +78,7 @@ smart-diff is "configured" when [`isLlmProviderConfigured()`](#lower-level-api)
57
78
  | `LLM_PROVIDER_NAME` | Display name used when `openai-compatible` is active (defaults to `openai-compatible`). |
58
79
  | `OPENAI_MAX_DIFF_CHARS` / `LLM_MAX_DIFF_CHARS` | Max size of unified diff text sent to the model (default ~120k characters). |
59
80
  | `OPENAI_MAX_TOKENS` / `LLM_MAX_TOKENS` | Max completion tokens (default 4000). |
81
+ | `LLM_TEMPERATURE` | Sampling temperature, clamped to 0–2 (default 0.2). Lower = more deterministic; higher = more varied prose. |
60
82
 
61
83
  ### Example: native OpenAI
62
84
 
@@ -113,7 +135,7 @@ const markdown = await summarizeGitDiff({
113
135
  | Option | Description |
114
136
  |--------|-------------|
115
137
  | `from` / `to` | Git refs for the range; `to` defaults to `HEAD`. |
116
- | `cwd` / `git` | Working tree for `simple-git`, or inject your own `SimpleGit` instance. |
138
+ | `cwd` / `git` | Working directory path, or inject your own `GitClient` instance. |
117
139
  | `includeFolders` | Limit diff to these paths relative to repo root (omit for full repo minus excludes). |
118
140
  | `excludeFolders` | Excluded paths (git `:(exclude)` pathspecs), e.g. `node_modules`. |
119
141
  | `commitMessageIncludeRegexes` | If any pattern is non-empty, only commits whose **full message** matches at least one pattern are kept (after excludes). Case-insensitive. |
@@ -173,10 +195,22 @@ const md = await summarizeGitDiff({
173
195
 
174
196
  The package also exports helpers for building a custom pipeline on top of the same git and LLM behavior:
175
197
 
176
- - **Git**: `createGitClient`, `getRepoRoot`, `getCommits`, `getDiff`, `getDiffSummary`, `getChangedFiles`, `filterCommitsByMessageRegexes`, `buildDiffPathspecs`, `buildDiffShapingGitArgs`, `shapeUnifiedDiff`, `DEFAULT_NOISE_EXCLUDES`
198
+ - **Git**: `createGitClient(cwd?, timeout?)`, `getRepoRoot`, `getCommits`, `getDiff`, `getDiffSummary`, `getChangedFiles`, `filterCommitsByMessageRegexes`, `buildDiffPathspecs`, `buildDiffShapingGitArgs`, `shapeUnifiedDiff`, `DEFAULT_NOISE_EXCLUDES` — `timeout` is in milliseconds, forwarded to `execFile`; omit for no timeout
177
199
  - **AI**: `generateSummary`, `resolveLlmMaxDiffChars`, `truncateUnifiedDiffForLlm`
178
200
  - **Provider resolution**: `resolveLanguageModel`, `detectLlmProvider`, `isLlmProviderConfigured`, `defaultModelForProvider`, `resolveLlmBaseUrl`, `parseLlmDefaultHeadersFromEnv`
179
- - **Constants / types**: `DEFAULT_GIT_DIFF_SYSTEM_PROMPT`, `LLM_GATEWAY_REQUIRED_MESSAGE`, `LlmProviderId`, `LlmModelProvider`, `ResolveLanguageModelOptions`, `GenerateSummaryInput`, `SummarizeFlags`
201
+ - **Constants / types**: `DEFAULT_GIT_DIFF_SYSTEM_PROMPT`, `LLM_GATEWAY_REQUIRED_MESSAGE`, `LlmProviderId`, `LlmModelProvider`, `ResolveLanguageModelOptions`, `GenerateSummaryInput`, `SummarizeFlags`, `DiffFileSummary`, `DiffSummary`, `CommitInfo`, `GitClient`, `GitDiffRangeQuery`, `DiffPathFilter`, `DiffShapingOptions` — `DiffFileSummary.binary?: boolean` is set to `true` when git reports `-` for additions/deletions (binary file); absent for text files
202
+
203
+ ## Migrating from 2.x → 3.x
204
+
205
+ `@ai-sdk/openai` and `@ai-sdk/openai-compatible` are no longer bundled as direct dependencies. If you use either, add them explicitly:
206
+
207
+ ```bash
208
+ npm install @ai-sdk/openai
209
+ # or
210
+ npm install @ai-sdk/openai-compatible
211
+ ```
212
+
213
+ Everything else — env vars, auto-detection, the public API — is unchanged.
180
214
 
181
215
  ## Migrating from 1.x → 2.x
182
216
 
package/dist/index.cjs CHANGED
@@ -2,7 +2,8 @@
2
2
 
3
3
  var ai = require('ai');
4
4
  var node_path = require('node:path');
5
- var simpleGit = require('simple-git');
5
+ var node_child_process = require('node:child_process');
6
+ var node_util = require('node:util');
6
7
 
7
8
  /******************************************************************************
8
9
  Copyright (c) Microsoft Corporation.
@@ -53,7 +54,7 @@ const LLM_GATEWAY_REQUIRED_MESSAGE = "No LLM provider configured. Set LLM_PROVID
53
54
  const DEFAULT_MODEL_BY_PROVIDER = {
54
55
  openai: "gpt-4o-mini",
55
56
  "openai-compatible": "gpt-4o-mini",
56
- anthropic: "claude-3-5-haiku-latest",
57
+ anthropic: "claude-haiku-4-5-20251001",
57
58
  google: "gemini-2.0-flash",
58
59
  bedrock: "anthropic.claude-3-5-haiku-20241022-v1:0",
59
60
  mistral: "mistral-small-latest",
@@ -120,7 +121,7 @@ function resolveOpenAiApiKey() {
120
121
  return (_a = readEnv("LLM_API_KEY")) !== null && _a !== void 0 ? _a : readEnv("OPENAI_API_KEY");
121
122
  }
122
123
  function detectLlmProvider() {
123
- var _a, _b;
124
+ var _a, _b, _c;
124
125
  const explicit = (_a = readEnv("LLM_PROVIDER")) === null || _a === void 0 ? void 0 : _a.toLowerCase();
125
126
  if (explicit && isValidProviderId(explicit)) {
126
127
  return explicit;
@@ -145,6 +146,8 @@ function detectLlmProvider() {
145
146
  return "xai";
146
147
  if (readEnv("DEEPSEEK_API_KEY"))
147
148
  return "deepseek";
149
+ if ((_c = readEnv("AWS_ACCESS_KEY_ID")) !== null && _c !== void 0 ? _c : readEnv("AWS_PROFILE"))
150
+ return "bedrock";
148
151
  if (parseLlmDefaultHeadersFromEnv())
149
152
  return "openai";
150
153
  return undefined;
@@ -157,17 +160,17 @@ function defaultModelForProvider(provider) {
157
160
  }
158
161
  function createOpenAiModel(modelId) {
159
162
  return __awaiter(this, void 0, void 0, function* () {
160
- const { createOpenAI } = yield import('@ai-sdk/openai');
163
+ const mod = yield importOptional("openai", "@ai-sdk/openai", () => import('@ai-sdk/openai'));
161
164
  const apiKey = resolveOpenAiApiKey();
162
165
  const headers = parseLlmDefaultHeadersFromEnv();
163
- const provider = createOpenAI(Object.assign(Object.assign({}, (apiKey ? { apiKey } : {})), (headers ? { headers } : {})));
166
+ const provider = mod.createOpenAI(Object.assign(Object.assign({}, (apiKey ? { apiKey } : {})), (headers ? { headers } : {})));
164
167
  return provider(modelId);
165
168
  });
166
169
  }
167
170
  function createOpenAiCompatibleModel(modelId) {
168
171
  return __awaiter(this, void 0, void 0, function* () {
169
172
  var _a;
170
- const { createOpenAICompatible } = yield import('@ai-sdk/openai-compatible');
173
+ const { createOpenAICompatible } = yield importOptional("openai-compatible", "@ai-sdk/openai-compatible", () => import('@ai-sdk/openai-compatible'));
171
174
  const baseURL = resolveLlmBaseUrl();
172
175
  if (!baseURL) {
173
176
  throw new Error("openai-compatible provider requires LLM_BASE_URL or OPENAI_BASE_URL to be set.");
@@ -330,6 +333,17 @@ function resolveMaxOutputTokens() {
330
333
  const parsed = raw !== undefined ? Number.parseInt(raw, 10) : 4000;
331
334
  return Number.isFinite(parsed) && parsed > 0 ? parsed : 4000;
332
335
  }
336
+ function resolveLlmTemperature() {
337
+ var _a;
338
+ const raw = (_a = process.env.LLM_TEMPERATURE) === null || _a === void 0 ? void 0 : _a.trim();
339
+ if (raw) {
340
+ const parsed = Number.parseFloat(raw);
341
+ if (Number.isFinite(parsed)) {
342
+ return Math.min(2, Math.max(0, parsed));
343
+ }
344
+ }
345
+ return 0.2;
346
+ }
333
347
  function generateSummary(input) {
334
348
  return __awaiter(this, void 0, void 0, function* () {
335
349
  var _a;
@@ -409,7 +423,7 @@ function callLlm(userContent, systemPrompt, maxOutputTokens, llmModelProvider, f
409
423
  model,
410
424
  system: systemPrompt,
411
425
  prompt: userContent,
412
- temperature: 0.2,
426
+ temperature: resolveLlmTemperature(),
413
427
  maxOutputTokens,
414
428
  });
415
429
  const text = result.text.trim();
@@ -424,10 +438,9 @@ function normalizeRepoRelativePath(p) {
424
438
  return noTrailingSlash.length > 0 ? noTrailingSlash : ".";
425
439
  }
426
440
  function assertPathUnderRepo(repoRoot, userPath) {
441
+ const absRoot = node_path.resolve(repoRoot);
427
442
  const abs = node_path.resolve(repoRoot, userPath);
428
- const rel = node_path.relative(repoRoot, abs);
429
- const segments = rel.split(/[/\\]/);
430
- if (segments.includes("..")) {
443
+ if (abs !== absRoot && !abs.startsWith(absRoot + node_path.sep)) {
431
444
  throw new Error(`Path escapes repository root: ${JSON.stringify(userPath)}`);
432
445
  }
433
446
  }
@@ -705,10 +718,11 @@ function parseNumStatLine(line) {
705
718
  const addStr = parts[0];
706
719
  const delStr = parts[1];
707
720
  const pathField = parts.slice(2).join("\t");
721
+ const binary = addStr === "-" || delStr === "-" ? true : undefined;
708
722
  const additions = addStr !== "-" ? Number.parseInt(addStr, 10) || 0 : 0;
709
723
  const deletions = delStr !== "-" ? Number.parseInt(delStr, 10) || 0 : 0;
710
724
  const key = numStatPathToLookupKey(pathField);
711
- return { key, additions, deletions };
725
+ return Object.assign({ key, additions, deletions }, (binary ? { binary } : {}));
712
726
  }
713
727
  function accumulateNumStat(numStatOutput, into) {
714
728
  var _a;
@@ -720,10 +734,8 @@ function accumulateNumStat(numStatOutput, into) {
720
734
  if (!parsed)
721
735
  continue;
722
736
  const prev = (_a = into.get(parsed.key)) !== null && _a !== void 0 ? _a : { additions: 0, deletions: 0 };
723
- into.set(parsed.key, {
724
- additions: prev.additions + parsed.additions,
725
- deletions: prev.deletions + parsed.deletions,
726
- });
737
+ const binary = parsed.binary || prev.binary || undefined;
738
+ into.set(parsed.key, Object.assign({ additions: prev.additions + parsed.additions, deletions: prev.deletions + parsed.deletions }, (binary ? { binary } : {})));
727
739
  }
728
740
  }
729
741
 
@@ -813,7 +825,7 @@ function buildSyntheticDiffLine(meta, counts) {
813
825
  return `${prefix}\t${counts.additions}\t${counts.deletions}\t${meta.path}`;
814
826
  }
815
827
  function buildDiffSummaryFromGitOutputs(nameStatusOutput, numStatOutput) {
816
- var _a;
828
+ var _a, _b;
817
829
  const numMap = new Map();
818
830
  accumulateNumStat(numStatOutput, numMap);
819
831
  const mergedName = mergeNameEntriesByPath(parseNameStatusLines(nameStatusOutput));
@@ -822,21 +834,39 @@ function buildDiffSummaryFromGitOutputs(nameStatusOutput, numStatOutput) {
822
834
  const counts = (_a = numMap.get(path)) !== null && _a !== void 0 ? _a : { additions: 0, deletions: 0 };
823
835
  syntheticLines.push(buildSyntheticDiffLine(meta, counts));
824
836
  }
825
- return parseDiffSummary(syntheticLines.join("\n"));
837
+ const summary = parseDiffSummary(syntheticLines.join("\n"));
838
+ for (const file of summary.files) {
839
+ if ((_b = numMap.get(file.path)) === null || _b === void 0 ? void 0 : _b.binary) {
840
+ file.binary = true;
841
+ }
842
+ }
843
+ return summary;
826
844
  }
827
845
 
828
- function createGitClient(cwd = process.cwd()) {
829
- return simpleGit.simpleGit(cwd);
846
+ const execFileAsync = node_util.promisify(node_child_process.execFile);
847
+ function createGitClient(cwd = process.cwd(), timeout) {
848
+ return {
849
+ run: (args) => execFileAsync("git", args, Object.assign({ cwd, maxBuffer: 100 * 1024 * 1024 }, (timeout !== undefined ? { timeout } : {}))).then(({ stdout }) => stdout),
850
+ };
830
851
  }
831
852
  function getCommits(git, from, to) {
832
853
  return __awaiter(this, void 0, void 0, function* () {
833
- const logResult = yield git.log({ from, to });
834
- return logResult.all;
854
+ const output = yield git.run(["log", "--format=%H%x1f%s", `${from}..${to}`]);
855
+ return output
856
+ .split("\n")
857
+ .filter(Boolean)
858
+ .map((line) => {
859
+ const sep = line.indexOf("\x1f");
860
+ return {
861
+ hash: sep >= 0 ? line.slice(0, sep) : line,
862
+ message: sep >= 0 ? line.slice(sep + 1) : "",
863
+ };
864
+ });
835
865
  });
836
866
  }
837
867
  function getRepoRoot(git) {
838
868
  return __awaiter(this, void 0, void 0, function* () {
839
- const root = yield git.revparse(["--show-toplevel"]);
869
+ const root = yield git.run(["rev-parse", "--show-toplevel"]);
840
870
  return root.trim();
841
871
  });
842
872
  }
@@ -853,7 +883,8 @@ function getDiff(git, query) {
853
883
  const { specs } = yield getDiffPathContext(git, pathFilter, repoRootOverride);
854
884
  const shapingArgs = buildDiffShapingGitArgs(shaping);
855
885
  if (!filterByCommits) {
856
- const raw = yield git.diff([
886
+ const raw = yield git.run([
887
+ "diff",
857
888
  ...shapingArgs,
858
889
  `${from}..${to}`,
859
890
  "--",
@@ -861,7 +892,7 @@ function getDiff(git, query) {
861
892
  ]);
862
893
  return shapeUnifiedDiff(raw, shaping);
863
894
  }
864
- const patches = yield Promise.all(commits.map((c) => git.diff([...shapingArgs, `${c.hash}^!`, "--", ...specs])));
895
+ const patches = yield Promise.all(commits.map((c) => git.run(["diff", ...shapingArgs, `${c.hash}^!`, "--", ...specs])));
865
896
  return patches
866
897
  .map((p) => shapeUnifiedDiff(p, shaping))
867
898
  .filter(Boolean)
@@ -875,14 +906,16 @@ function getDiffSummary(git, query) {
875
906
  const whitespaceArgs = (shaping === null || shaping === void 0 ? void 0 : shaping.ignoreWhitespace) ? ["-w"] : [];
876
907
  if (!filterByCommits) {
877
908
  const [numOutput, nameOutput] = yield Promise.all([
878
- git.diff([
909
+ git.run([
910
+ "diff",
879
911
  ...whitespaceArgs,
880
912
  "--numstat",
881
913
  `${from}..${to}`,
882
914
  "--",
883
915
  ...specs,
884
916
  ]),
885
- git.diff([
917
+ git.run([
918
+ "diff",
886
919
  ...whitespaceArgs,
887
920
  "--name-status",
888
921
  `${from}..${to}`,
@@ -895,8 +928,22 @@ function getDiffSummary(git, query) {
895
928
  const pairs = yield Promise.all(commits.map((c) => __awaiter(this, void 0, void 0, function* () {
896
929
  const range = `${c.hash}^!`;
897
930
  const [numOutput, nameOutput] = yield Promise.all([
898
- git.diff([...whitespaceArgs, "--numstat", range, "--", ...specs]),
899
- git.diff([...whitespaceArgs, "--name-status", range, "--", ...specs]),
931
+ git.run([
932
+ "diff",
933
+ ...whitespaceArgs,
934
+ "--numstat",
935
+ range,
936
+ "--",
937
+ ...specs,
938
+ ]),
939
+ git.run([
940
+ "diff",
941
+ ...whitespaceArgs,
942
+ "--name-status",
943
+ range,
944
+ "--",
945
+ ...specs,
946
+ ]),
900
947
  ]);
901
948
  return { numOutput, nameOutput };
902
949
  })));
@@ -916,7 +963,8 @@ function getChangedFiles(git, query) {
916
963
  const { from, to, commits, filterByCommits, pathFilter, repoRootOverride } = query;
917
964
  const { specs } = yield getDiffPathContext(git, pathFilter, repoRootOverride);
918
965
  if (!filterByCommits) {
919
- const output = yield git.diff([
966
+ const output = yield git.run([
967
+ "diff",
920
968
  "--name-only",
921
969
  `${from}..${to}`,
922
970
  "--",
@@ -925,24 +973,24 @@ function getChangedFiles(git, query) {
925
973
  return output
926
974
  .split(/\r?\n/)
927
975
  .map((f) => f.trim())
928
- .filter(Boolean);
976
+ .filter(Boolean)
977
+ .sort();
929
978
  }
930
- const fileSet = new Set();
931
- yield Promise.all(commits.map((c) => __awaiter(this, void 0, void 0, function* () {
932
- const output = yield git.show([
979
+ const results = yield Promise.all(commits.map((c) => __awaiter(this, void 0, void 0, function* () {
980
+ const output = yield git.run([
981
+ "show",
933
982
  "--name-only",
934
983
  "--pretty=format:",
935
984
  c.hash,
936
985
  "--",
937
986
  ...specs,
938
987
  ]);
939
- output
988
+ return output
940
989
  .split(/\r?\n/)
941
990
  .map((f) => f.trim())
942
- .filter(Boolean)
943
- .forEach((f) => fileSet.add(f));
991
+ .filter(Boolean);
944
992
  })));
945
- return Array.from(fileSet);
993
+ return [...new Set(results.flat())].sort();
946
994
  });
947
995
  }
948
996