@mcarvin/smart-diff 1.0.4 → 1.1.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/dist/index.umd.js CHANGED
@@ -38,7 +38,7 @@
38
38
 
39
39
  function resolveLlmBaseUrl() {
40
40
  var _a, _b, _c;
41
- return (_b = (_a = process.env.LLM_BASE_URL) === null || _a === void 0 ? void 0 : _a.trim()) !== null && _b !== void 0 ? _b : (_c = process.env.OPENAI_BASE_URL) === null || _c === void 0 ? void 0 : _c.trim();
41
+ return ((_b = (_a = process.env.LLM_BASE_URL) === null || _a === void 0 ? void 0 : _a.trim()) !== null && _b !== void 0 ? _b : (_c = process.env.OPENAI_BASE_URL) === null || _c === void 0 ? void 0 : _c.trim());
42
42
  }
43
43
  function parseHeaderJsonObject(raw) {
44
44
  const trimmed = raw === null || raw === void 0 ? void 0 : raw.trim();
@@ -46,12 +46,14 @@
46
46
  return {};
47
47
  try {
48
48
  const parsed = JSON.parse(trimmed);
49
- if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
49
+ if (typeof parsed !== "object" ||
50
+ parsed === null ||
51
+ Array.isArray(parsed)) {
50
52
  return {};
51
53
  }
52
54
  const out = {};
53
55
  for (const [key, value] of Object.entries(parsed)) {
54
- if (typeof value === 'string' && value.length > 0) {
56
+ if (typeof value === "string" && value.length > 0) {
55
57
  out[key] = value;
56
58
  }
57
59
  }
@@ -68,7 +70,7 @@
68
70
  return Object.keys(merged).length > 0 ? merged : undefined;
69
71
  }
70
72
  function findAuthorizationHeaderName(headers) {
71
- return Object.keys(headers).find((k) => k.toLowerCase() === 'authorization');
73
+ return Object.keys(headers).find((k) => k.toLowerCase() === "authorization");
72
74
  }
73
75
  function stripBearerPrefix(value) {
74
76
  var _a;
@@ -111,7 +113,7 @@
111
113
  var _a, _b, _c, _d, _e;
112
114
  const baseURL = resolveLlmBaseUrl();
113
115
  const mergedHeaders = (_a = parseLlmDefaultHeadersFromEnv()) !== null && _a !== void 0 ? _a : {};
114
- const envApiKey = (_e = (_c = (_b = process.env.LLM_API_KEY) === null || _b === void 0 ? void 0 : _b.trim()) !== null && _c !== void 0 ? _c : (_d = process.env.OPENAI_API_KEY) === null || _d === void 0 ? void 0 : _d.trim()) !== null && _e !== void 0 ? _e : '';
116
+ const envApiKey = (_e = (_c = (_b = process.env.LLM_API_KEY) === null || _b === void 0 ? void 0 : _b.trim()) !== null && _c !== void 0 ? _c : (_d = process.env.OPENAI_API_KEY) === null || _d === void 0 ? void 0 : _d.trim()) !== null && _e !== void 0 ? _e : "";
115
117
  let defaultHeaders;
116
118
  let apiKey = envApiKey;
117
119
  if (apiKey.length === 0) {
@@ -119,12 +121,16 @@
119
121
  if (split.apiKeyFromAuthHeader) {
120
122
  apiKey = split.apiKeyFromAuthHeader;
121
123
  }
122
- defaultHeaders = Object.keys(split.defaultHeaders).length > 0 ? split.defaultHeaders : undefined;
124
+ defaultHeaders =
125
+ Object.keys(split.defaultHeaders).length > 0
126
+ ? split.defaultHeaders
127
+ : undefined;
123
128
  }
124
129
  else {
125
- defaultHeaders = Object.keys(mergedHeaders).length > 0 ? mergedHeaders : undefined;
130
+ defaultHeaders =
131
+ Object.keys(mergedHeaders).length > 0 ? mergedHeaders : undefined;
126
132
  }
127
- return Object.assign(Object.assign({ apiKey: apiKey.length > 0 ? apiKey : 'unused' }, (baseURL ? { baseURL } : {})), (defaultHeaders ? { defaultHeaders } : {}));
133
+ return Object.assign(Object.assign({ apiKey: apiKey.length > 0 ? apiKey : "unused" }, (baseURL ? { baseURL } : {})), (defaultHeaders ? { defaultHeaders } : {}));
128
134
  }
129
135
  function createOpenAiLikeClient() {
130
136
  return __awaiter(this, void 0, void 0, function* () {
@@ -134,9 +140,22 @@
134
140
  }
135
141
 
136
142
  const DEFAULT_LLM_MAX_DIFF_CHARS = 120000;
143
+ const DEFAULT_GIT_DIFF_SYSTEM_PROMPT = `You are a senior software engineer helping developers understand code and configuration changes from the git context they supplied.
144
+ You receive: commit subject lines (when available), changed file paths, and unified git patch(es)—either one range diff or concatenated per-commit patches, depending on how the diff was produced. Patches may be truncated mid-section with an explicit marker—do not infer changes beyond visible lines.
145
+ Explain what changed in terms of behavior, APIs, data, configuration, security, and operational risk. Tie claims to the patch when possible.
146
+ Produce a concise, developer-focused summary in Markdown.
147
+ Use sections that fit the change (for example: Highlights, Breaking or risky changes, API / contract changes, Data & schema, Configuration & infra, Security & auth, Tests & quality). Omit empty sections.
148
+ Group related changes; do not list every individual file. When multiple commits appear in the context, briefly separate notable themes by commit when helpful.
149
+ If the user message includes a Team line, use that exact team name in the summary title (for example: "## <Team> – Change summary" or similar).`;
150
+ const LLM_GATEWAY_REQUIRED_MESSAGE = "No LLM gateway configured. Set OPENAI_API_KEY or LLM_API_KEY, and/or LLM_BASE_URL or OPENAI_BASE_URL, " +
151
+ "and/or JSON in OPENAI_DEFAULT_HEADERS or LLM_DEFAULT_HEADERS. " +
152
+ "Alternatively pass openAiClientProvider to generateSummary or summarizeGitDiff.";
153
+
137
154
  function resolveLlmMaxDiffChars(cliOverride) {
138
155
  var _a;
139
- if (cliOverride !== undefined && Number.isFinite(cliOverride) && cliOverride > 0) {
156
+ if (cliOverride !== undefined &&
157
+ Number.isFinite(cliOverride) &&
158
+ cliOverride > 0) {
140
159
  return Math.trunc(cliOverride);
141
160
  }
142
161
  const raw = (_a = process.env.LLM_MAX_DIFF_CHARS) === null || _a === void 0 ? void 0 : _a.trim();
@@ -155,70 +174,75 @@
155
174
  const marker = `\n\n--- TRUNCATED: unified diff was ${diffText.length} characters; only the first ${maxChars} were sent. Narrow the ref range, adjust commit/path filters, or raise maxDiffChars / LLM_MAX_DIFF_CHARS only if your model context allows. ---\n`;
156
175
  return diffText.slice(0, maxChars) + marker;
157
176
  }
158
- const DEFAULT_GIT_DIFF_SYSTEM_PROMPT = `You are a senior software engineer helping developers understand code and configuration changes from the git context they supplied.
159
- You receive: commit subject lines (when available), changed file paths, and unified git patch(es)—either one range diff or concatenated per-commit patches, depending on how the diff was produced. Patches may be truncated mid-section with an explicit marker—do not infer changes beyond visible lines.
160
- Explain what changed in terms of behavior, APIs, data, configuration, security, and operational risk. Tie claims to the patch when possible.
161
- Produce a concise, developer-focused summary in Markdown.
162
- Use sections that fit the change (for example: Highlights, Breaking or risky changes, API / contract changes, Data & schema, Configuration & infra, Security & auth, Tests & quality). Omit empty sections.
163
- Group related changes; do not list every individual file. When multiple commits appear in the context, briefly separate notable themes by commit when helpful.
164
- If the user message includes a Team line, use that exact team name in the summary title (for example: "## <Team> – Change summary" or similar).`;
165
- const LLM_GATEWAY_REQUIRED_MESSAGE = 'No LLM gateway configured. Set OPENAI_API_KEY or LLM_API_KEY, and/or LLM_BASE_URL or OPENAI_BASE_URL, ' +
166
- 'and/or JSON in OPENAI_DEFAULT_HEADERS or LLM_DEFAULT_HEADERS. ' +
167
- 'Alternatively pass openAiClientProvider to generateSummary or summarizeGitDiff.';
168
- function generateSummary(diffText, fileNames, commits, flags, openAiClientProvider, diffSummary) {
177
+ function markdownDiffTruncationNotice(originalChars, maxChars) {
178
+ return `> **Truncated diff:** The unified diff was ${originalChars} characters; only the first ${maxChars} were sent to the model. The summary may not reflect the full change set. Narrow the ref range, adjust path filters, or raise \`maxDiffChars\` / \`LLM_MAX_DIFF_CHARS\`—often together with switching to a model whose context window can fit a larger prompt.\n\n`;
179
+ }
180
+ function generateSummary(input) {
169
181
  return __awaiter(this, void 0, void 0, function* () {
170
182
  var _a, _b;
183
+ const { diffText, fileNames, commits, flags, openAiClientProvider, diffSummary, } = input;
171
184
  if (!shouldUseLlmGateway() && openAiClientProvider === undefined) {
172
185
  throw new Error(LLM_GATEWAY_REQUIRED_MESSAGE);
173
186
  }
174
187
  const maxDiffChars = resolveLlmMaxDiffChars(flags.maxDiffChars);
188
+ const diffTruncated = diffText.length > maxDiffChars;
175
189
  const diffForLlm = truncateUnifiedDiffForLlm(diffText, maxDiffChars);
176
190
  const userContent = buildOpenAiUserContent(flags, commits, fileNames, diffForLlm, diffSummary);
177
- return callOpenAi(userContent, (_a = flags.model) !== null && _a !== void 0 ? _a : 'gpt-4o-mini', (_b = flags.systemPrompt) !== null && _b !== void 0 ? _b : DEFAULT_GIT_DIFF_SYSTEM_PROMPT, openAiClientProvider !== null && openAiClientProvider !== void 0 ? openAiClientProvider : (() => __awaiter(this, void 0, void 0, function* () { return createOpenAiLikeClient(); })));
191
+ const summary = yield callOpenAi(userContent, (_a = flags.model) !== null && _a !== void 0 ? _a : "gpt-4o-mini", (_b = flags.systemPrompt) !== null && _b !== void 0 ? _b : DEFAULT_GIT_DIFF_SYSTEM_PROMPT, openAiClientProvider !== null && openAiClientProvider !== void 0 ? openAiClientProvider : (() => __awaiter(this, void 0, void 0, function* () { return createOpenAiLikeClient(); })));
192
+ if (!diffTruncated) {
193
+ return summary;
194
+ }
195
+ return markdownDiffTruncationNotice(diffText.length, maxDiffChars) + summary;
178
196
  });
179
197
  }
180
198
  function formatRegexFilterLines(flags) {
181
199
  var _a, _b;
182
- const includes = ((_a = flags.commitMessageIncludeRegexes) !== null && _a !== void 0 ? _a : []).map((s) => s.trim()).filter(Boolean);
183
- const excludes = ((_b = flags.commitMessageExcludeRegexes) !== null && _b !== void 0 ? _b : []).map((s) => s.trim()).filter(Boolean);
200
+ const includes = ((_a = flags.commitMessageIncludeRegexes) !== null && _a !== void 0 ? _a : [])
201
+ .map((s) => s.trim())
202
+ .filter(Boolean);
203
+ const excludes = ((_b = flags.commitMessageExcludeRegexes) !== null && _b !== void 0 ? _b : [])
204
+ .map((s) => s.trim())
205
+ .filter(Boolean);
184
206
  const incLine = includes.length > 0
185
- ? `Commit message include regexes (OR): ${includes.map((r) => JSON.stringify(r)).join(', ')}\n`
186
- : '';
207
+ ? `Commit message include regexes (OR): ${includes.map((r) => JSON.stringify(r)).join(", ")}\n`
208
+ : "";
187
209
  const excLine = excludes.length > 0
188
- ? `Commit message exclude regexes: ${excludes.map((r) => JSON.stringify(r)).join(', ')}\n`
189
- : '';
210
+ ? `Commit message exclude regexes: ${excludes.map((r) => JSON.stringify(r)).join(", ")}\n`
211
+ : "";
190
212
  if (!incLine && !excLine) {
191
- return 'Commit message filters: none.\nGit context shape: single unified diff for the full ref range.\n';
213
+ return "Commit message filters: none.\nGit context shape: single unified diff for the full ref range.\n";
192
214
  }
193
215
  return (`${incLine}${excLine}` +
194
- 'Git context shape: concatenated per-commit unified patches for commits that pass the message filters.\n');
216
+ "Git context shape: concatenated per-commit unified patches for commits that pass the message filters.\n");
195
217
  }
196
218
  function buildOpenAiUserContent(flags, commits, fileNames, diffText, diffSummary) {
197
219
  var _a, _b;
198
220
  const from = flags.from;
199
- const to = (_a = flags.to) !== null && _a !== void 0 ? _a : 'HEAD';
221
+ const to = (_a = flags.to) !== null && _a !== void 0 ? _a : "HEAD";
200
222
  const team = (_b = flags.team) === null || _b === void 0 ? void 0 : _b.trim();
201
223
  const ts = new Date().toISOString();
202
- const teamLine = team ? `Team: ${team}\n` : '';
224
+ const teamLine = team ? `Team: ${team}\n` : "";
203
225
  const filterBlock = formatRegexFilterLines(flags);
204
226
  const commitBlock = commits.length > 0
205
- ? commits.map((c) => `- ${c.hash.slice(0, 7)} ${c.message.replace(/\r?\n/g, ' ')}`).join('\n')
206
- : '- (no commits in range after filtering)';
207
- const pathsBlock = fileNames.length > 0 ? fileNames.join('\n') : '(no paths in diff scope)';
227
+ ? commits
228
+ .map((c) => `- ${c.hash.slice(0, 7)} ${c.message.replace(/\r?\n/g, " ")}`)
229
+ .join("\n")
230
+ : "- (no commits in range after filtering)";
231
+ const pathsBlock = fileNames.length > 0 ? fileNames.join("\n") : "(no paths in diff scope)";
208
232
  const structuredDiffSection = diffSummary
209
233
  ? `=== Structured git context (JSON summary) ===\n${JSON.stringify(diffSummary, null, 2)}\n\n`
210
- : '';
234
+ : "";
211
235
  return (`${teamLine}` +
212
236
  `Date: ${ts}\n\n` +
213
237
  `Git refs: ${from}..${to}\n` +
214
238
  filterBlock +
215
- '\n' +
216
- '=== Included commits (subject lines) ===\n' +
239
+ "\n" +
240
+ "=== Included commits (subject lines) ===\n" +
217
241
  `${commitBlock}\n\n` +
218
- '=== Changed paths ===\n' +
242
+ "=== Changed paths ===\n" +
219
243
  `${pathsBlock}\n\n` +
220
244
  structuredDiffSection +
221
- '=== Git context (unified diff(s); patches may be truncated with an explicit marker) ===\n' +
245
+ "=== Git context (unified diff(s); patches may be truncated with an explicit marker) ===\n" +
222
246
  diffText);
223
247
  }
224
248
  function callOpenAi(userContent, model, systemPrompt, openAiClientProvider) {
@@ -232,11 +256,11 @@ If the user message includes a Team line, use that exact team name in the summar
232
256
  model,
233
257
  messages: [
234
258
  {
235
- role: 'system',
259
+ role: "system",
236
260
  content: systemPrompt,
237
261
  },
238
262
  {
239
- role: 'user',
263
+ role: "user",
240
264
  content: userContent,
241
265
  },
242
266
  ],
@@ -244,64 +268,25 @@ If the user message includes a Team line, use that exact team name in the summar
244
268
  max_tokens: maxTokens,
245
269
  });
246
270
  const typedResponse = response;
247
- const text = (_f = (_e = (_d = (_c = (_b = typedResponse.choices) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.message) === null || _d === void 0 ? void 0 : _d.content) === null || _e === void 0 ? void 0 : _e.trim()) !== null && _f !== void 0 ? _f : '';
248
- return text.length > 0 ? text : 'No summary generated by OpenAI.';
271
+ const text = (_f = (_e = (_d = (_c = (_b = typedResponse.choices) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.message) === null || _d === void 0 ? void 0 : _d.content) === null || _e === void 0 ? void 0 : _e.trim()) !== null && _f !== void 0 ? _f : "";
272
+ return text.length > 0 ? text : "No summary generated by OpenAI.";
249
273
  });
250
274
  }
251
275
 
252
- function createGitClient(cwd = process.cwd()) {
253
- return simpleGit.simpleGit(cwd);
254
- }
255
- function getCommits(git, from, to) {
256
- return __awaiter(this, void 0, void 0, function* () {
257
- const logResult = yield git.log({ from, to });
258
- return logResult.all;
259
- });
260
- }
261
- function compileRegex(pattern, label) {
262
- try {
263
- return new RegExp(pattern, 'i');
264
- }
265
- catch (_a) {
266
- throw new Error(`Invalid ${label} regular expression: ${JSON.stringify(pattern)}`);
267
- }
268
- }
269
- function commitMessagePassesFilters(message, includeRes, excludeRes) {
270
- for (const ex of excludeRes) {
271
- if (ex.test(message))
272
- return false;
273
- }
274
- if (includeRes.length > 0 && !includeRes.some((r) => r.test(message)))
275
- return false;
276
- return true;
277
- }
278
- function filterCommitsByMessageRegexes(commits, includePatterns, excludePatterns) {
279
- const includes = (includePatterns !== null && includePatterns !== void 0 ? includePatterns : []).map((p) => p.trim()).filter((p) => p.length > 0);
280
- const excludes = (excludePatterns !== null && excludePatterns !== void 0 ? excludePatterns : []).map((p) => p.trim()).filter((p) => p.length > 0);
281
- const includeRes = includes.map((p, i) => compileRegex(p, `commit message include pattern[${i}]`));
282
- const excludeRes = excludes.map((p, i) => compileRegex(p, `commit message exclude pattern[${i}]`));
283
- return commits.filter((c) => commitMessagePassesFilters(c.message, includeRes, excludeRes));
284
- }
285
- function getRepoRoot(git) {
286
- return __awaiter(this, void 0, void 0, function* () {
287
- const root = yield git.revparse(['--show-toplevel']);
288
- return root.trim();
289
- });
290
- }
291
276
  function normalizeRepoRelativePath(p) {
292
- const trimmed = p.trim().replace(/\\/g, '/');
293
- const noLeading = trimmed.replace(/^\/+/, '');
294
- const noTrailingSlash = noLeading.replace(/\/+$/, '');
295
- return noTrailingSlash.length > 0 ? noTrailingSlash : '.';
277
+ const trimmed = p.trim().replace(/\\/g, "/");
278
+ const noLeading = trimmed.replace(/^\/+/, "");
279
+ const noTrailingSlash = noLeading.replace(/\/+$/, "");
280
+ return noTrailingSlash.length > 0 ? noTrailingSlash : ".";
296
281
  }
297
282
  function assertPathUnderRepo(repoRoot, userPath) {
298
283
  const abs = node_path.resolve(repoRoot, userPath);
299
284
  const rel = node_path.relative(repoRoot, abs);
300
- if (rel === '..') {
285
+ if (rel === "..") {
301
286
  throw new Error(`Path escapes repository root: ${JSON.stringify(userPath)}`);
302
287
  }
303
288
  const segments = rel.split(/[/\\]/);
304
- if (segments.includes('..')) {
289
+ if (segments.includes("..")) {
305
290
  throw new Error(`Path escapes repository root: ${JSON.stringify(userPath)}`);
306
291
  }
307
292
  }
@@ -309,9 +294,13 @@ If the user message includes a Team line, use that exact team name in the summar
309
294
  var _a, _b, _c, _d;
310
295
  const includeRaw = (_b = (_a = pathFilter === null || pathFilter === void 0 ? void 0 : pathFilter.includeFolders) === null || _a === void 0 ? void 0 : _a.filter((p) => p.trim().length > 0)) !== null && _b !== void 0 ? _b : [];
311
296
  const excludeRaw = (_d = (_c = pathFilter === null || pathFilter === void 0 ? void 0 : pathFilter.excludeFolders) === null || _c === void 0 ? void 0 : _c.filter((p) => p.trim().length > 0)) !== null && _d !== void 0 ? _d : [];
312
- const includes = includeRaw.map(normalizeRepoRelativePath).filter((p) => p !== '.' && p !== '');
313
- const excludes = excludeRaw.map(normalizeRepoRelativePath).filter((p) => p !== '.' && p !== '');
314
- const toValidate = includes.length > 0 ? includes : ['.'];
297
+ const includes = includeRaw
298
+ .map(normalizeRepoRelativePath)
299
+ .filter((p) => p !== "." && p !== "");
300
+ const excludes = excludeRaw
301
+ .map(normalizeRepoRelativePath)
302
+ .filter((p) => p !== "." && p !== "");
303
+ const toValidate = includes.length > 0 ? includes : ["."];
315
304
  for (const inc of toValidate) {
316
305
  assertPathUnderRepo(repoRoot, inc);
317
306
  }
@@ -320,7 +309,7 @@ If the user message includes a Team line, use that exact team name in the summar
320
309
  }
321
310
  const specs = [];
322
311
  if (includes.length === 0) {
323
- specs.push('.');
312
+ specs.push(".");
324
313
  }
325
314
  else {
326
315
  for (const inc of includes) {
@@ -332,100 +321,73 @@ If the user message includes a Team line, use that exact team name in the summar
332
321
  }
333
322
  return specs;
334
323
  }
335
- function getDiffPathContext(git, pathFilter, repoRootOverride) {
336
- return __awaiter(this, void 0, void 0, function* () {
337
- const repoRoot = repoRootOverride !== null && repoRootOverride !== void 0 ? repoRootOverride : (yield getRepoRoot(git));
338
- const specs = buildDiffPathspecs(repoRoot, pathFilter);
339
- return { repoRoot, specs };
340
- });
341
- }
342
- function getDiff(git, from, to, commits, filterByCommits, pathFilter, repoRootOverride) {
343
- return __awaiter(this, void 0, void 0, function* () {
344
- const { specs } = yield getDiffPathContext(git, pathFilter, repoRootOverride);
345
- if (!filterByCommits) {
346
- return git.diff([`${from}..${to}`, '--', ...specs]);
347
- }
348
- const patches = yield Promise.all(commits.map((c) => git.diff([`${c.hash}^!`, '--', ...specs])));
349
- return patches.filter(Boolean).join('\n');
350
- });
324
+
325
+ function compileRegex(pattern, label) {
326
+ try {
327
+ return new RegExp(pattern, "i");
328
+ }
329
+ catch (_a) {
330
+ throw new Error(`Invalid ${label} regular expression: ${JSON.stringify(pattern)}`);
331
+ }
351
332
  }
352
- function getDiffSummary(git, from, to, commits, filterByCommits, pathFilter, repoRootOverride) {
353
- return __awaiter(this, void 0, void 0, function* () {
354
- const { specs } = yield getDiffPathContext(git, pathFilter, repoRootOverride);
355
- if (!filterByCommits) {
356
- const [numOutput, nameOutput] = yield Promise.all([
357
- git.diff(['--numstat', `${from}..${to}`, '--', ...specs]),
358
- git.diff(['--name-status', `${from}..${to}`, '--', ...specs]),
359
- ]);
360
- return buildDiffSummaryFromGitOutputs(nameOutput, numOutput);
361
- }
362
- const pairs = yield Promise.all(commits.map((c) => __awaiter(this, void 0, void 0, function* () {
363
- const range = `${c.hash}^!`;
364
- const [numOutput, nameOutput] = yield Promise.all([
365
- git.diff(['--numstat', range, '--', ...specs]),
366
- git.diff(['--name-status', range, '--', ...specs]),
367
- ]);
368
- return { numOutput, nameOutput };
369
- })));
370
- const nameJoined = pairs
371
- .map((p) => p.nameOutput)
372
- .filter(Boolean)
373
- .join('\n');
374
- const numJoined = pairs
375
- .map((p) => p.numOutput)
376
- .filter(Boolean)
377
- .join('\n');
378
- return buildDiffSummaryFromGitOutputs(nameJoined, numJoined);
379
- });
333
+ function commitMessagePassesFilters(message, includeRes, excludeRes) {
334
+ for (const ex of excludeRes) {
335
+ if (ex.test(message))
336
+ return false;
337
+ }
338
+ if (includeRes.length > 0 && !includeRes.some((r) => r.test(message)))
339
+ return false;
340
+ return true;
380
341
  }
381
- function getChangedFiles(git, from, to, commits, filterByCommits, pathFilter, repoRootOverride) {
382
- return __awaiter(this, void 0, void 0, function* () {
383
- const { specs } = yield getDiffPathContext(git, pathFilter, repoRootOverride);
384
- if (!filterByCommits) {
385
- const output = yield git.diff(['--name-only', `${from}..${to}`, '--', ...specs]);
386
- return output
387
- .split(/\r?\n/)
388
- .map((f) => f.trim())
389
- .filter(Boolean);
390
- }
391
- const fileSet = new Set();
392
- yield Promise.all(commits.map((c) => __awaiter(this, void 0, void 0, function* () {
393
- const output = yield git.show(['--name-only', '--pretty=format:', c.hash, '--', ...specs]);
394
- output
395
- .split(/\r?\n/)
396
- .map((f) => f.trim())
397
- .filter(Boolean)
398
- .forEach((f) => fileSet.add(f));
399
- })));
400
- return Array.from(fileSet);
401
- });
342
+ function filterCommitsByMessageRegexes(commits, includePatterns, excludePatterns) {
343
+ const includes = (includePatterns !== null && includePatterns !== void 0 ? includePatterns : [])
344
+ .map((p) => p.trim())
345
+ .filter((p) => p.length > 0);
346
+ const excludes = (excludePatterns !== null && excludePatterns !== void 0 ? excludePatterns : [])
347
+ .map((p) => p.trim())
348
+ .filter((p) => p.length > 0);
349
+ const includeRes = includes.map((p, i) => compileRegex(p, `commit message include pattern[${i}]`));
350
+ const excludeRes = excludes.map((p, i) => compileRegex(p, `commit message exclude pattern[${i}]`));
351
+ return commits.filter((c) => commitMessagePassesFilters(c.message, includeRes, excludeRes));
402
352
  }
353
+
403
354
  const GIT_STATUS_BY_FIRST_CHAR = {
404
- A: 'added',
405
- D: 'deleted',
406
- R: 'renamed',
407
- C: 'copied',
408
- T: 'type-changed',
409
- M: 'modified',
355
+ A: "added",
356
+ D: "deleted",
357
+ R: "renamed",
358
+ C: "copied",
359
+ T: "type-changed",
360
+ M: "modified",
410
361
  };
411
362
  function mapGitStatus(statusCode) {
412
363
  var _a;
413
- return (_a = GIT_STATUS_BY_FIRST_CHAR[statusCode.charAt(0)]) !== null && _a !== void 0 ? _a : 'unknown';
364
+ return (_a = GIT_STATUS_BY_FIRST_CHAR[statusCode.charAt(0)]) !== null && _a !== void 0 ? _a : "unknown";
414
365
  }
415
366
  function mergeStatus(existing, next) {
416
367
  if (existing === next)
417
368
  return existing;
418
- const precedence = ['deleted', 'added', 'renamed', 'copied', 'type-changed', 'modified', 'unknown'];
419
- return precedence.indexOf(existing) <= precedence.indexOf(next) ? existing : next;
369
+ const precedence = [
370
+ "deleted",
371
+ "added",
372
+ "renamed",
373
+ "copied",
374
+ "type-changed",
375
+ "modified",
376
+ "unknown",
377
+ ];
378
+ return precedence.indexOf(existing) <= precedence.indexOf(next)
379
+ ? existing
380
+ : next;
420
381
  }
382
+
421
383
  function parseNameStatusLine(line) {
422
384
  var _a;
423
- const parts = line.split('\t');
385
+ const parts = line.split("\t");
424
386
  let entry = null;
425
387
  if (parts.length >= 2) {
426
- const statusToken = (_a = parts[0]) !== null && _a !== void 0 ? _a : '';
388
+ const statusToken = (_a = parts[0]) !== null && _a !== void 0 ? _a : "";
427
389
  const status = mapGitStatus(statusToken);
428
- const isRenameOrCopy = statusToken.startsWith('R') || statusToken.startsWith('C');
390
+ const isRenameOrCopy = statusToken.startsWith("R") || statusToken.startsWith("C");
429
391
  if (isRenameOrCopy && parts.length >= 3) {
430
392
  const oldPath = parts[1];
431
393
  const newPath = parts[2];
@@ -471,6 +433,7 @@ If the user message includes a Team line, use that exact team name in the summar
471
433
  }
472
434
  return byPath;
473
435
  }
436
+
474
437
  function numStatPathToLookupKey(pathField) {
475
438
  const brace = /^(.*)\{(.+) => (.+)\}$/.exec(pathField);
476
439
  if (!brace) {
@@ -482,14 +445,14 @@ If the user message includes a Team line, use that exact team name in the summar
482
445
  }
483
446
  function parseNumStatLine(line) {
484
447
  var _a, _b;
485
- const parts = line.split('\t');
448
+ const parts = line.split("\t");
486
449
  if (parts.length < 3)
487
450
  return null;
488
- const addStr = (_a = parts[0]) !== null && _a !== void 0 ? _a : '';
489
- const delStr = (_b = parts[1]) !== null && _b !== void 0 ? _b : '';
490
- const pathField = parts.slice(2).join('\t');
491
- const additions = addStr !== '-' ? Number.parseInt(addStr, 10) || 0 : 0;
492
- const deletions = delStr !== '-' ? Number.parseInt(delStr, 10) || 0 : 0;
451
+ const addStr = (_a = parts[0]) !== null && _a !== void 0 ? _a : "";
452
+ const delStr = (_b = parts[1]) !== null && _b !== void 0 ? _b : "";
453
+ const pathField = parts.slice(2).join("\t");
454
+ const additions = addStr !== "-" ? Number.parseInt(addStr, 10) || 0 : 0;
455
+ const deletions = delStr !== "-" ? Number.parseInt(delStr, 10) || 0 : 0;
493
456
  const key = numStatPathToLookupKey(pathField);
494
457
  return { key, additions, deletions };
495
458
  }
@@ -503,56 +466,35 @@ If the user message includes a Team line, use that exact team name in the summar
503
466
  if (!parsed)
504
467
  continue;
505
468
  const prev = (_a = into.get(parsed.key)) !== null && _a !== void 0 ? _a : { additions: 0, deletions: 0 };
506
- into.set(parsed.key, { additions: prev.additions + parsed.additions, deletions: prev.deletions + parsed.deletions });
507
- }
508
- }
509
- const STATUS_TO_SYNTHETIC_PREFIX = {
510
- added: 'A',
511
- deleted: 'D',
512
- renamed: 'R100',
513
- copied: 'C100',
514
- 'type-changed': 'T',
515
- modified: 'M',
516
- unknown: 'X',
517
- };
518
- function diffStatusToSyntheticPrefix(status) {
519
- return STATUS_TO_SYNTHETIC_PREFIX[status];
520
- }
521
- function buildSyntheticDiffLine(meta, counts) {
522
- const prefix = diffStatusToSyntheticPrefix(meta.status);
523
- if (meta.oldPath) {
524
- return `${prefix}\t${counts.additions}\t${counts.deletions}\t${meta.oldPath}\t${meta.path}`;
525
- }
526
- return `${prefix}\t${counts.additions}\t${counts.deletions}\t${meta.path}`;
527
- }
528
- function buildDiffSummaryFromGitOutputs(nameStatusOutput, numStatOutput) {
529
- var _a;
530
- const numMap = new Map();
531
- accumulateNumStat(numStatOutput, numMap);
532
- const mergedName = mergeNameEntriesByPath(parseNameStatusLines(nameStatusOutput));
533
- const syntheticLines = [];
534
- for (const [path, meta] of mergedName) {
535
- const counts = (_a = numMap.get(path)) !== null && _a !== void 0 ? _a : { additions: 0, deletions: 0 };
536
- syntheticLines.push(buildSyntheticDiffLine(meta, counts));
469
+ into.set(parsed.key, {
470
+ additions: prev.additions + parsed.additions,
471
+ deletions: prev.deletions + parsed.deletions,
472
+ });
537
473
  }
538
- return parseDiffSummary(syntheticLines.join('\n'));
539
474
  }
475
+
540
476
  function parseTabDiffSummaryLine(line) {
541
477
  var _a;
542
- const parts = line.split('\t');
478
+ const parts = line.split("\t");
543
479
  if (parts.length < 3)
544
480
  return null;
545
- const statusToken = (_a = parts.shift()) !== null && _a !== void 0 ? _a : '';
481
+ const statusToken = (_a = parts.shift()) !== null && _a !== void 0 ? _a : "";
546
482
  const status = mapGitStatus(statusToken);
547
483
  const add0 = parts[0];
548
484
  const del0 = parts[1];
549
- const additions = add0 && add0 !== '-' ? Number.parseInt(add0, 10) || 0 : 0;
550
- const deletions = del0 && del0 !== '-' ? Number.parseInt(del0, 10) || 0 : 0;
485
+ const additions = add0 && add0 !== "-" ? Number.parseInt(add0, 10) || 0 : 0;
486
+ const deletions = del0 && del0 !== "-" ? Number.parseInt(del0, 10) || 0 : 0;
551
487
  if (parts.length === 3) {
552
488
  return { status, additions, deletions, newPath: parts[2] };
553
489
  }
554
490
  if (parts.length === 4) {
555
- return { status, additions, deletions, oldPath: parts[2], newPath: parts[3] };
491
+ return {
492
+ status,
493
+ additions,
494
+ deletions,
495
+ oldPath: parts[2],
496
+ newPath: parts[3],
497
+ };
556
498
  }
557
499
  return null;
558
500
  }
@@ -598,11 +540,142 @@ If the user message includes a Team line, use that exact team name in the summar
598
540
  };
599
541
  }
600
542
 
543
+ const STATUS_TO_SYNTHETIC_PREFIX = {
544
+ added: "A",
545
+ deleted: "D",
546
+ renamed: "R100",
547
+ copied: "C100",
548
+ "type-changed": "T",
549
+ modified: "M",
550
+ unknown: "X",
551
+ };
552
+ function diffStatusToSyntheticPrefix(status) {
553
+ return STATUS_TO_SYNTHETIC_PREFIX[status];
554
+ }
555
+ function buildSyntheticDiffLine(meta, counts) {
556
+ const prefix = diffStatusToSyntheticPrefix(meta.status);
557
+ if (meta.oldPath) {
558
+ return `${prefix}\t${counts.additions}\t${counts.deletions}\t${meta.oldPath}\t${meta.path}`;
559
+ }
560
+ return `${prefix}\t${counts.additions}\t${counts.deletions}\t${meta.path}`;
561
+ }
562
+ function buildDiffSummaryFromGitOutputs(nameStatusOutput, numStatOutput) {
563
+ var _a;
564
+ const numMap = new Map();
565
+ accumulateNumStat(numStatOutput, numMap);
566
+ const mergedName = mergeNameEntriesByPath(parseNameStatusLines(nameStatusOutput));
567
+ const syntheticLines = [];
568
+ for (const [path, meta] of mergedName) {
569
+ const counts = (_a = numMap.get(path)) !== null && _a !== void 0 ? _a : { additions: 0, deletions: 0 };
570
+ syntheticLines.push(buildSyntheticDiffLine(meta, counts));
571
+ }
572
+ return parseDiffSummary(syntheticLines.join("\n"));
573
+ }
574
+
575
+ function createGitClient(cwd = process.cwd()) {
576
+ return simpleGit.simpleGit(cwd);
577
+ }
578
+ function getCommits(git, from, to) {
579
+ return __awaiter(this, void 0, void 0, function* () {
580
+ const logResult = yield git.log({ from, to });
581
+ return logResult.all;
582
+ });
583
+ }
584
+ function getRepoRoot(git) {
585
+ return __awaiter(this, void 0, void 0, function* () {
586
+ const root = yield git.revparse(["--show-toplevel"]);
587
+ return root.trim();
588
+ });
589
+ }
590
+ function getDiffPathContext(git, pathFilter, repoRootOverride) {
591
+ return __awaiter(this, void 0, void 0, function* () {
592
+ const repoRoot = repoRootOverride !== null && repoRootOverride !== void 0 ? repoRootOverride : (yield getRepoRoot(git));
593
+ const specs = buildDiffPathspecs(repoRoot, pathFilter);
594
+ return { repoRoot, specs };
595
+ });
596
+ }
597
+ function getDiff(git, query) {
598
+ return __awaiter(this, void 0, void 0, function* () {
599
+ const { from, to, commits, filterByCommits, pathFilter, repoRootOverride } = query;
600
+ const { specs } = yield getDiffPathContext(git, pathFilter, repoRootOverride);
601
+ if (!filterByCommits) {
602
+ return git.diff([`${from}..${to}`, "--", ...specs]);
603
+ }
604
+ const patches = yield Promise.all(commits.map((c) => git.diff([`${c.hash}^!`, "--", ...specs])));
605
+ return patches.filter(Boolean).join("\n");
606
+ });
607
+ }
608
+ function getDiffSummary(git, query) {
609
+ return __awaiter(this, void 0, void 0, function* () {
610
+ const { from, to, commits, filterByCommits, pathFilter, repoRootOverride } = query;
611
+ const { specs } = yield getDiffPathContext(git, pathFilter, repoRootOverride);
612
+ if (!filterByCommits) {
613
+ const [numOutput, nameOutput] = yield Promise.all([
614
+ git.diff(["--numstat", `${from}..${to}`, "--", ...specs]),
615
+ git.diff(["--name-status", `${from}..${to}`, "--", ...specs]),
616
+ ]);
617
+ return buildDiffSummaryFromGitOutputs(nameOutput, numOutput);
618
+ }
619
+ const pairs = yield Promise.all(commits.map((c) => __awaiter(this, void 0, void 0, function* () {
620
+ const range = `${c.hash}^!`;
621
+ const [numOutput, nameOutput] = yield Promise.all([
622
+ git.diff(["--numstat", range, "--", ...specs]),
623
+ git.diff(["--name-status", range, "--", ...specs]),
624
+ ]);
625
+ return { numOutput, nameOutput };
626
+ })));
627
+ const nameJoined = pairs
628
+ .map((p) => p.nameOutput)
629
+ .filter(Boolean)
630
+ .join("\n");
631
+ const numJoined = pairs
632
+ .map((p) => p.numOutput)
633
+ .filter(Boolean)
634
+ .join("\n");
635
+ return buildDiffSummaryFromGitOutputs(nameJoined, numJoined);
636
+ });
637
+ }
638
+ function getChangedFiles(git, query) {
639
+ return __awaiter(this, void 0, void 0, function* () {
640
+ const { from, to, commits, filterByCommits, pathFilter, repoRootOverride } = query;
641
+ const { specs } = yield getDiffPathContext(git, pathFilter, repoRootOverride);
642
+ if (!filterByCommits) {
643
+ const output = yield git.diff([
644
+ "--name-only",
645
+ `${from}..${to}`,
646
+ "--",
647
+ ...specs,
648
+ ]);
649
+ return output
650
+ .split(/\r?\n/)
651
+ .map((f) => f.trim())
652
+ .filter(Boolean);
653
+ }
654
+ const fileSet = new Set();
655
+ yield Promise.all(commits.map((c) => __awaiter(this, void 0, void 0, function* () {
656
+ const output = yield git.show([
657
+ "--name-only",
658
+ "--pretty=format:",
659
+ c.hash,
660
+ "--",
661
+ ...specs,
662
+ ]);
663
+ output
664
+ .split(/\r?\n/)
665
+ .map((f) => f.trim())
666
+ .filter(Boolean)
667
+ .forEach((f) => fileSet.add(f));
668
+ })));
669
+ return Array.from(fileSet);
670
+ });
671
+ }
672
+
601
673
  function hasNonEmptyTrimmed(arr) {
602
674
  return (arr !== null && arr !== void 0 ? arr : []).some((s) => s.trim().length > 0);
603
675
  }
604
676
  function shouldFilterByCommits(allCommits, filtered, opts) {
605
- if (hasNonEmptyTrimmed(opts.commitMessageIncludeRegexes) || hasNonEmptyTrimmed(opts.commitMessageExcludeRegexes)) {
677
+ if (hasNonEmptyTrimmed(opts.commitMessageIncludeRegexes) ||
678
+ hasNonEmptyTrimmed(opts.commitMessageExcludeRegexes)) {
606
679
  return true;
607
680
  }
608
681
  return filtered.length !== allCommits.length;
@@ -612,8 +685,9 @@ If the user message includes a Team line, use that exact team name in the summar
612
685
  var _a, _b;
613
686
  const git = (_a = options.git) !== null && _a !== void 0 ? _a : createGitClient(options.cwd);
614
687
  const from = options.from;
615
- const to = (_b = options.to) !== null && _b !== void 0 ? _b : 'HEAD';
616
- const pathFilter = hasNonEmptyTrimmed(options.includeFolders) || hasNonEmptyTrimmed(options.excludeFolders)
688
+ const to = (_b = options.to) !== null && _b !== void 0 ? _b : "HEAD";
689
+ const pathFilter = hasNonEmptyTrimmed(options.includeFolders) ||
690
+ hasNonEmptyTrimmed(options.excludeFolders)
617
691
  ? {
618
692
  includeFolders: options.includeFolders,
619
693
  excludeFolders: options.excludeFolders,
@@ -622,10 +696,17 @@ If the user message includes a Team line, use that exact team name in the summar
622
696
  const allCommits = yield getCommits(git, from, to);
623
697
  const filteredCommits = filterCommitsByMessageRegexes(allCommits, options.commitMessageIncludeRegexes, options.commitMessageExcludeRegexes);
624
698
  const filterByCommits = shouldFilterByCommits(allCommits, filteredCommits, options);
699
+ const rangeQuery = {
700
+ from,
701
+ to,
702
+ commits: filteredCommits,
703
+ filterByCommits,
704
+ pathFilter,
705
+ };
625
706
  const [diffText, fileNames, diffSummary] = yield Promise.all([
626
- getDiff(git, from, to, filteredCommits, filterByCommits, pathFilter),
627
- getChangedFiles(git, from, to, filteredCommits, filterByCommits, pathFilter),
628
- getDiffSummary(git, from, to, filteredCommits, filterByCommits, pathFilter),
707
+ getDiff(git, rangeQuery),
708
+ getChangedFiles(git, rangeQuery),
709
+ getDiffSummary(git, rangeQuery),
629
710
  ]);
630
711
  const summarizeFlags = {
631
712
  from,
@@ -637,7 +718,14 @@ If the user message includes a Team line, use that exact team name in the summar
637
718
  commitMessageIncludeRegexes: options.commitMessageIncludeRegexes,
638
719
  commitMessageExcludeRegexes: options.commitMessageExcludeRegexes,
639
720
  };
640
- return generateSummary(diffText, fileNames, filteredCommits, summarizeFlags, options.openAiClientProvider, diffSummary);
721
+ return generateSummary({
722
+ diffText,
723
+ fileNames,
724
+ commits: filteredCommits,
725
+ flags: summarizeFlags,
726
+ openAiClientProvider: options.openAiClientProvider,
727
+ diffSummary,
728
+ });
641
729
  });
642
730
  }
643
731