@mcarvin/smart-diff 1.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/LICENSE.md +9 -0
- package/README.md +91 -0
- package/dist/index.cjs +652 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.min.cjs +2 -0
- package/dist/index.min.cjs.map +1 -0
- package/dist/index.min.mjs +2 -0
- package/dist/index.min.mjs.map +1 -0
- package/dist/index.mjs +631 -0
- package/dist/index.mjs.map +1 -0
- package/dist/typings/ai/aiSummary.d.ts +18 -0
- package/dist/typings/ai/openAIConfig.d.ts +21 -0
- package/dist/typings/git/gitDiff.d.ts +33 -0
- package/dist/typings/index.d.ts +24 -0
- package/dist/typings/src/ai/aiSummary.d.ts +19 -0
- package/dist/typings/src/ai/openAIConfig.d.ts +21 -0
- package/dist/typings/src/git/gitDiff.d.ts +33 -0
- package/dist/typings/src/index.d.ts +24 -0
- package/dist/typings/test/aiSummary.spec.d.ts +1 -0
- package/dist/typings/test/gitDiff.async.spec.d.ts +1 -0
- package/dist/typings/test/gitDiff.spec.d.ts +1 -0
- package/dist/typings/test/index.spec.d.ts +1 -0
- package/dist/typings/test/openAIConfig.spec.d.ts +1 -0
- package/dist/typings/test/openAiSdk.spec.d.ts +1 -0
- package/dist/typings/test/summarizeGitDiff.spec.d.ts +1 -0
- package/package.json +72 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,652 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var node_path = require('node:path');
|
|
4
|
+
var simpleGit = require('simple-git');
|
|
5
|
+
|
|
6
|
+
/******************************************************************************
|
|
7
|
+
Copyright (c) Microsoft Corporation.
|
|
8
|
+
|
|
9
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
10
|
+
purpose with or without fee is hereby granted.
|
|
11
|
+
|
|
12
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
13
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
14
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
15
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
16
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
17
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
18
|
+
PERFORMANCE OF THIS SOFTWARE.
|
|
19
|
+
***************************************************************************** */
|
|
20
|
+
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
function __awaiter(thisArg, _arguments, P, generator) {
|
|
24
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
25
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
26
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
27
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
28
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
29
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
34
|
+
var e = new Error(message);
|
|
35
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function resolveLlmBaseUrl() {
|
|
39
|
+
var _a, _b, _c;
|
|
40
|
+
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
|
+
}
|
|
42
|
+
function parseHeaderJsonObject(raw) {
|
|
43
|
+
const trimmed = raw === null || raw === void 0 ? void 0 : raw.trim();
|
|
44
|
+
if (!trimmed)
|
|
45
|
+
return {};
|
|
46
|
+
try {
|
|
47
|
+
const parsed = JSON.parse(trimmed);
|
|
48
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
const out = {};
|
|
52
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
53
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
54
|
+
out[key] = value;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
catch (_a) {
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function parseLlmDefaultHeadersFromEnv() {
|
|
64
|
+
const base = parseHeaderJsonObject(process.env.OPENAI_DEFAULT_HEADERS);
|
|
65
|
+
const override = parseHeaderJsonObject(process.env.LLM_DEFAULT_HEADERS);
|
|
66
|
+
const merged = Object.assign(Object.assign({}, base), override);
|
|
67
|
+
return Object.keys(merged).length > 0 ? merged : undefined;
|
|
68
|
+
}
|
|
69
|
+
function findAuthorizationHeaderName(headers) {
|
|
70
|
+
return Object.keys(headers).find((k) => k.toLowerCase() === 'authorization');
|
|
71
|
+
}
|
|
72
|
+
function stripBearerPrefix(value) {
|
|
73
|
+
var _a;
|
|
74
|
+
const trimmed = value.trim();
|
|
75
|
+
const match = /^Bearer\s+(\S+)/i.exec(trimmed);
|
|
76
|
+
return (_a = match === null || match === void 0 ? void 0 : match[1]) !== null && _a !== void 0 ? _a : trimmed;
|
|
77
|
+
}
|
|
78
|
+
function splitPromotableAuthorizationFromHeaders(headers) {
|
|
79
|
+
const authName = findAuthorizationHeaderName(headers);
|
|
80
|
+
if (!authName) {
|
|
81
|
+
return { defaultHeaders: headers };
|
|
82
|
+
}
|
|
83
|
+
const raw = headers[authName];
|
|
84
|
+
if (!raw) {
|
|
85
|
+
return { defaultHeaders: headers };
|
|
86
|
+
}
|
|
87
|
+
const token = stripBearerPrefix(raw);
|
|
88
|
+
const looksBearer = /^Bearer\s+\S+/i.test(raw.trim());
|
|
89
|
+
const looksOpenAiKey = /^sk-/i.test(token);
|
|
90
|
+
if (!looksBearer && !looksOpenAiKey) {
|
|
91
|
+
return { defaultHeaders: headers };
|
|
92
|
+
}
|
|
93
|
+
const next = Object.assign({}, headers);
|
|
94
|
+
delete next[authName];
|
|
95
|
+
return { defaultHeaders: next, apiKeyFromAuthHeader: token };
|
|
96
|
+
}
|
|
97
|
+
function shouldUseLlmGateway() {
|
|
98
|
+
var _a, _b, _c;
|
|
99
|
+
const apiKey = (_b = (_a = process.env.LLM_API_KEY) === null || _a === void 0 ? void 0 : _a.trim()) !== null && _b !== void 0 ? _b : (_c = process.env.OPENAI_API_KEY) === null || _c === void 0 ? void 0 : _c.trim();
|
|
100
|
+
if (apiKey)
|
|
101
|
+
return true;
|
|
102
|
+
if (resolveLlmBaseUrl())
|
|
103
|
+
return true;
|
|
104
|
+
const jsonHeaders = parseLlmDefaultHeadersFromEnv();
|
|
105
|
+
if (jsonHeaders && Object.keys(jsonHeaders).length > 0)
|
|
106
|
+
return true;
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
function resolveOpenAiLikeClientInit() {
|
|
110
|
+
var _a, _b, _c, _d, _e;
|
|
111
|
+
const baseURL = resolveLlmBaseUrl();
|
|
112
|
+
const mergedHeaders = (_a = parseLlmDefaultHeadersFromEnv()) !== null && _a !== void 0 ? _a : {};
|
|
113
|
+
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 : '';
|
|
114
|
+
let defaultHeaders;
|
|
115
|
+
let apiKey = envApiKey;
|
|
116
|
+
if (apiKey.length === 0) {
|
|
117
|
+
const split = splitPromotableAuthorizationFromHeaders(mergedHeaders);
|
|
118
|
+
if (split.apiKeyFromAuthHeader) {
|
|
119
|
+
apiKey = split.apiKeyFromAuthHeader;
|
|
120
|
+
}
|
|
121
|
+
defaultHeaders = Object.keys(split.defaultHeaders).length > 0 ? split.defaultHeaders : undefined;
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
defaultHeaders = Object.keys(mergedHeaders).length > 0 ? mergedHeaders : undefined;
|
|
125
|
+
}
|
|
126
|
+
return Object.assign(Object.assign({ apiKey: apiKey.length > 0 ? apiKey : 'unused' }, (baseURL ? { baseURL } : {})), (defaultHeaders ? { defaultHeaders } : {}));
|
|
127
|
+
}
|
|
128
|
+
function createOpenAiLikeClient() {
|
|
129
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
130
|
+
const { default: OpenAI } = yield import('openai');
|
|
131
|
+
return new OpenAI(resolveOpenAiLikeClientInit());
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const DEFAULT_LLM_MAX_DIFF_CHARS = 120000;
|
|
136
|
+
function resolveLlmMaxDiffChars(cliOverride) {
|
|
137
|
+
var _a;
|
|
138
|
+
if (cliOverride !== undefined && Number.isFinite(cliOverride) && cliOverride > 0) {
|
|
139
|
+
return Math.trunc(cliOverride);
|
|
140
|
+
}
|
|
141
|
+
const raw = (_a = process.env.LLM_MAX_DIFF_CHARS) === null || _a === void 0 ? void 0 : _a.trim();
|
|
142
|
+
if (raw) {
|
|
143
|
+
const parsed = Number.parseInt(raw, 10);
|
|
144
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
145
|
+
return parsed;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return DEFAULT_LLM_MAX_DIFF_CHARS;
|
|
149
|
+
}
|
|
150
|
+
function truncateUnifiedDiffForLlm(diffText, maxChars) {
|
|
151
|
+
if (diffText.length <= maxChars) {
|
|
152
|
+
return diffText;
|
|
153
|
+
}
|
|
154
|
+
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`;
|
|
155
|
+
return diffText.slice(0, maxChars) + marker;
|
|
156
|
+
}
|
|
157
|
+
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.
|
|
158
|
+
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.
|
|
159
|
+
Explain what changed in terms of behavior, APIs, data, configuration, security, and operational risk. Tie claims to the patch when possible.
|
|
160
|
+
Produce a concise, developer-focused summary in Markdown.
|
|
161
|
+
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.
|
|
162
|
+
Group related changes; do not list every individual file. When multiple commits appear in the context, briefly separate notable themes by commit when helpful.
|
|
163
|
+
If the user message includes a Team line, use that exact team name in the summary title (for example: "## <Team> – Change summary" or similar).`;
|
|
164
|
+
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, ' +
|
|
165
|
+
'and/or JSON in OPENAI_DEFAULT_HEADERS or LLM_DEFAULT_HEADERS. ' +
|
|
166
|
+
'Alternatively pass openAiClientProvider to generateSummary or summarizeGitDiff.';
|
|
167
|
+
function generateSummary(diffText, fileNames, commits, flags, openAiClientProvider, diffSummary) {
|
|
168
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
169
|
+
var _a, _b;
|
|
170
|
+
if (!shouldUseLlmGateway() && openAiClientProvider === undefined) {
|
|
171
|
+
throw new Error(LLM_GATEWAY_REQUIRED_MESSAGE);
|
|
172
|
+
}
|
|
173
|
+
const maxDiffChars = resolveLlmMaxDiffChars(flags.maxDiffChars);
|
|
174
|
+
const diffForLlm = truncateUnifiedDiffForLlm(diffText, maxDiffChars);
|
|
175
|
+
const userContent = buildOpenAiUserContent(flags, commits, fileNames, diffForLlm, diffSummary);
|
|
176
|
+
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(); })));
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
function formatRegexFilterLines(flags) {
|
|
180
|
+
var _a, _b;
|
|
181
|
+
const includes = ((_a = flags.commitMessageIncludeRegexes) !== null && _a !== void 0 ? _a : []).map((s) => s.trim()).filter(Boolean);
|
|
182
|
+
const excludes = ((_b = flags.commitMessageExcludeRegexes) !== null && _b !== void 0 ? _b : []).map((s) => s.trim()).filter(Boolean);
|
|
183
|
+
const incLine = includes.length > 0
|
|
184
|
+
? `Commit message include regexes (OR): ${includes.map((r) => JSON.stringify(r)).join(', ')}\n`
|
|
185
|
+
: '';
|
|
186
|
+
const excLine = excludes.length > 0
|
|
187
|
+
? `Commit message exclude regexes: ${excludes.map((r) => JSON.stringify(r)).join(', ')}\n`
|
|
188
|
+
: '';
|
|
189
|
+
if (!incLine && !excLine) {
|
|
190
|
+
return 'Commit message filters: none.\nGit context shape: single unified diff for the full ref range.\n';
|
|
191
|
+
}
|
|
192
|
+
return (`${incLine}${excLine}` +
|
|
193
|
+
'Git context shape: concatenated per-commit unified patches for commits that pass the message filters.\n');
|
|
194
|
+
}
|
|
195
|
+
function buildOpenAiUserContent(flags, commits, fileNames, diffText, diffSummary) {
|
|
196
|
+
var _a, _b;
|
|
197
|
+
const from = flags.from;
|
|
198
|
+
const to = (_a = flags.to) !== null && _a !== void 0 ? _a : 'HEAD';
|
|
199
|
+
const team = (_b = flags.team) === null || _b === void 0 ? void 0 : _b.trim();
|
|
200
|
+
const ts = new Date().toISOString();
|
|
201
|
+
const teamLine = team ? `Team: ${team}\n` : '';
|
|
202
|
+
const filterBlock = formatRegexFilterLines(flags);
|
|
203
|
+
const commitBlock = commits.length > 0
|
|
204
|
+
? commits.map((c) => `- ${c.hash.slice(0, 7)} ${c.message.replace(/\r?\n/g, ' ')}`).join('\n')
|
|
205
|
+
: '- (no commits in range after filtering)';
|
|
206
|
+
const pathsBlock = fileNames.length > 0 ? fileNames.join('\n') : '(no paths in diff scope)';
|
|
207
|
+
const structuredDiffSection = diffSummary
|
|
208
|
+
? `=== Structured git context (JSON summary) ===\n${JSON.stringify(diffSummary, null, 2)}\n\n`
|
|
209
|
+
: '';
|
|
210
|
+
return (`${teamLine}` +
|
|
211
|
+
`Date: ${ts}\n\n` +
|
|
212
|
+
`Git refs: ${from}..${to}\n` +
|
|
213
|
+
filterBlock +
|
|
214
|
+
'\n' +
|
|
215
|
+
'=== Included commits (subject lines) ===\n' +
|
|
216
|
+
`${commitBlock}\n\n` +
|
|
217
|
+
'=== Changed paths ===\n' +
|
|
218
|
+
`${pathsBlock}\n\n` +
|
|
219
|
+
structuredDiffSection +
|
|
220
|
+
'=== Git context (unified diff(s); patches may be truncated with an explicit marker) ===\n' +
|
|
221
|
+
diffText);
|
|
222
|
+
}
|
|
223
|
+
function callOpenAi(userContent, model, systemPrompt, openAiClientProvider) {
|
|
224
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
225
|
+
var _a, _b, _c, _d, _e, _f;
|
|
226
|
+
const client = yield openAiClientProvider();
|
|
227
|
+
const maxTokensRaw = (_a = process.env.LLM_MAX_TOKENS) !== null && _a !== void 0 ? _a : process.env.OPENAI_MAX_TOKENS;
|
|
228
|
+
const parsed = maxTokensRaw !== undefined ? Number.parseInt(maxTokensRaw, 10) : 4000;
|
|
229
|
+
const maxTokens = Number.isFinite(parsed) && parsed > 0 ? parsed : 4000;
|
|
230
|
+
const response = yield client.chat.completions.create({
|
|
231
|
+
model,
|
|
232
|
+
messages: [
|
|
233
|
+
{
|
|
234
|
+
role: 'system',
|
|
235
|
+
content: systemPrompt,
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
role: 'user',
|
|
239
|
+
content: userContent,
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
temperature: 0.2,
|
|
243
|
+
max_tokens: maxTokens,
|
|
244
|
+
});
|
|
245
|
+
const typedResponse = response;
|
|
246
|
+
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 : '';
|
|
247
|
+
return text.length > 0 ? text : 'No summary generated by OpenAI.';
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function createGitClient(cwd = process.cwd()) {
|
|
252
|
+
return simpleGit.simpleGit(cwd);
|
|
253
|
+
}
|
|
254
|
+
function getCommits(git, from, to) {
|
|
255
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
256
|
+
const logResult = yield git.log({ from, to });
|
|
257
|
+
return logResult.all;
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
function compileRegex(pattern, label) {
|
|
261
|
+
try {
|
|
262
|
+
return new RegExp(pattern, 'i');
|
|
263
|
+
}
|
|
264
|
+
catch (_a) {
|
|
265
|
+
throw new Error(`Invalid ${label} regular expression: ${JSON.stringify(pattern)}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function filterCommitsByMessageRegexes(commits, includePatterns, excludePatterns) {
|
|
269
|
+
const includes = (includePatterns !== null && includePatterns !== void 0 ? includePatterns : []).map((p) => p.trim()).filter((p) => p.length > 0);
|
|
270
|
+
const excludes = (excludePatterns !== null && excludePatterns !== void 0 ? excludePatterns : []).map((p) => p.trim()).filter((p) => p.length > 0);
|
|
271
|
+
const includeRes = includes.map((p, i) => compileRegex(p, `commit message include pattern[${i}]`));
|
|
272
|
+
const excludeRes = excludes.map((p, i) => compileRegex(p, `commit message exclude pattern[${i}]`));
|
|
273
|
+
return commits.filter((c) => {
|
|
274
|
+
for (const ex of excludeRes) {
|
|
275
|
+
if (ex.test(c.message))
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
if (includeRes.length > 0 && !includeRes.some((r) => r.test(c.message)))
|
|
279
|
+
return false;
|
|
280
|
+
return true;
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
function getRepoRoot(git) {
|
|
284
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
285
|
+
const root = yield git.revparse(['--show-toplevel']);
|
|
286
|
+
return root.trim();
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
function normalizeRepoRelativePath(p) {
|
|
290
|
+
const trimmed = p.trim().replace(/\\/g, '/');
|
|
291
|
+
const noLeading = trimmed.replace(/^\/+/, '');
|
|
292
|
+
const noTrailingSlash = noLeading.replace(/\/+$/, '');
|
|
293
|
+
return noTrailingSlash.length > 0 ? noTrailingSlash : '.';
|
|
294
|
+
}
|
|
295
|
+
function assertPathUnderRepo(repoRoot, userPath) {
|
|
296
|
+
const abs = node_path.resolve(repoRoot, userPath);
|
|
297
|
+
const rel = node_path.relative(repoRoot, abs);
|
|
298
|
+
if (rel === '..') {
|
|
299
|
+
throw new Error(`Path escapes repository root: ${JSON.stringify(userPath)}`);
|
|
300
|
+
}
|
|
301
|
+
const segments = rel.split(/[/\\]/);
|
|
302
|
+
if (segments.includes('..')) {
|
|
303
|
+
throw new Error(`Path escapes repository root: ${JSON.stringify(userPath)}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function buildDiffPathspecs(repoRoot, pathFilter) {
|
|
307
|
+
var _a, _b, _c, _d;
|
|
308
|
+
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 : [];
|
|
309
|
+
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 : [];
|
|
310
|
+
const includes = includeRaw.map(normalizeRepoRelativePath).filter((p) => p !== '.' && p !== '');
|
|
311
|
+
const excludes = excludeRaw.map(normalizeRepoRelativePath).filter((p) => p !== '.' && p !== '');
|
|
312
|
+
const toValidate = includes.length > 0 ? includes : ['.'];
|
|
313
|
+
for (const inc of toValidate) {
|
|
314
|
+
assertPathUnderRepo(repoRoot, inc);
|
|
315
|
+
}
|
|
316
|
+
for (const exc of excludes) {
|
|
317
|
+
assertPathUnderRepo(repoRoot, exc);
|
|
318
|
+
}
|
|
319
|
+
const specs = [];
|
|
320
|
+
if (includes.length === 0) {
|
|
321
|
+
specs.push('.');
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
for (const inc of includes) {
|
|
325
|
+
specs.push(inc);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
for (const exc of excludes) {
|
|
329
|
+
specs.push(`:(exclude)${exc}`);
|
|
330
|
+
}
|
|
331
|
+
return specs;
|
|
332
|
+
}
|
|
333
|
+
function getDiffPathContext(git, pathFilter, repoRootOverride) {
|
|
334
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
335
|
+
const repoRoot = repoRootOverride !== null && repoRootOverride !== void 0 ? repoRootOverride : (yield getRepoRoot(git));
|
|
336
|
+
const specs = buildDiffPathspecs(repoRoot, pathFilter);
|
|
337
|
+
return { repoRoot, specs };
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
function getDiff(git, from, to, commits, filterByCommits, pathFilter, repoRootOverride) {
|
|
341
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
342
|
+
const { specs } = yield getDiffPathContext(git, pathFilter, repoRootOverride);
|
|
343
|
+
if (!filterByCommits) {
|
|
344
|
+
return git.diff([`${from}..${to}`, '--', ...specs]);
|
|
345
|
+
}
|
|
346
|
+
const patches = yield Promise.all(commits.map((c) => git.diff([`${c.hash}^!`, '--', ...specs])));
|
|
347
|
+
return patches.filter(Boolean).join('\n');
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
function getDiffSummary(git, from, to, commits, filterByCommits, pathFilter, repoRootOverride) {
|
|
351
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
352
|
+
const { specs } = yield getDiffPathContext(git, pathFilter, repoRootOverride);
|
|
353
|
+
if (!filterByCommits) {
|
|
354
|
+
const [numOutput, nameOutput] = yield Promise.all([
|
|
355
|
+
git.diff(['--numstat', `${from}..${to}`, '--', ...specs]),
|
|
356
|
+
git.diff(['--name-status', `${from}..${to}`, '--', ...specs]),
|
|
357
|
+
]);
|
|
358
|
+
return buildDiffSummaryFromGitOutputs(nameOutput, numOutput);
|
|
359
|
+
}
|
|
360
|
+
const pairs = yield Promise.all(commits.map((c) => __awaiter(this, void 0, void 0, function* () {
|
|
361
|
+
const range = `${c.hash}^!`;
|
|
362
|
+
const [numOutput, nameOutput] = yield Promise.all([
|
|
363
|
+
git.diff(['--numstat', range, '--', ...specs]),
|
|
364
|
+
git.diff(['--name-status', range, '--', ...specs]),
|
|
365
|
+
]);
|
|
366
|
+
return { numOutput, nameOutput };
|
|
367
|
+
})));
|
|
368
|
+
const nameJoined = pairs
|
|
369
|
+
.map((p) => p.nameOutput)
|
|
370
|
+
.filter(Boolean)
|
|
371
|
+
.join('\n');
|
|
372
|
+
const numJoined = pairs
|
|
373
|
+
.map((p) => p.numOutput)
|
|
374
|
+
.filter(Boolean)
|
|
375
|
+
.join('\n');
|
|
376
|
+
return buildDiffSummaryFromGitOutputs(nameJoined, numJoined);
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
function getChangedFiles(git, from, to, commits, filterByCommits, pathFilter, repoRootOverride) {
|
|
380
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
381
|
+
const { specs } = yield getDiffPathContext(git, pathFilter, repoRootOverride);
|
|
382
|
+
if (!filterByCommits) {
|
|
383
|
+
const output = yield git.diff(['--name-only', `${from}..${to}`, '--', ...specs]);
|
|
384
|
+
return output
|
|
385
|
+
.split(/\r?\n/)
|
|
386
|
+
.map((f) => f.trim())
|
|
387
|
+
.filter(Boolean);
|
|
388
|
+
}
|
|
389
|
+
const fileSet = new Set();
|
|
390
|
+
yield Promise.all(commits.map((c) => __awaiter(this, void 0, void 0, function* () {
|
|
391
|
+
const output = yield git.show(['--name-only', '--pretty=format:', c.hash, '--', ...specs]);
|
|
392
|
+
output
|
|
393
|
+
.split(/\r?\n/)
|
|
394
|
+
.map((f) => f.trim())
|
|
395
|
+
.filter(Boolean)
|
|
396
|
+
.forEach((f) => fileSet.add(f));
|
|
397
|
+
})));
|
|
398
|
+
return Array.from(fileSet);
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
function mapGitStatus(statusCode) {
|
|
402
|
+
if (statusCode.startsWith('A'))
|
|
403
|
+
return 'added';
|
|
404
|
+
if (statusCode.startsWith('D'))
|
|
405
|
+
return 'deleted';
|
|
406
|
+
if (statusCode.startsWith('R'))
|
|
407
|
+
return 'renamed';
|
|
408
|
+
if (statusCode.startsWith('C'))
|
|
409
|
+
return 'copied';
|
|
410
|
+
if (statusCode.startsWith('T'))
|
|
411
|
+
return 'type-changed';
|
|
412
|
+
if (statusCode.startsWith('M'))
|
|
413
|
+
return 'modified';
|
|
414
|
+
return 'unknown';
|
|
415
|
+
}
|
|
416
|
+
function mergeStatus(existing, next) {
|
|
417
|
+
if (existing === next)
|
|
418
|
+
return existing;
|
|
419
|
+
const precedence = ['deleted', 'added', 'renamed', 'copied', 'type-changed', 'modified', 'unknown'];
|
|
420
|
+
return precedence.indexOf(existing) <= precedence.indexOf(next) ? existing : next;
|
|
421
|
+
}
|
|
422
|
+
function parseNameStatusLines(nameStatusOutput) {
|
|
423
|
+
var _a;
|
|
424
|
+
const entries = [];
|
|
425
|
+
for (const rawLine of nameStatusOutput.split(/\r?\n/)) {
|
|
426
|
+
const line = rawLine.trim();
|
|
427
|
+
if (!line)
|
|
428
|
+
continue;
|
|
429
|
+
const parts = line.split('\t');
|
|
430
|
+
if (parts.length < 2)
|
|
431
|
+
continue;
|
|
432
|
+
const statusToken = (_a = parts[0]) !== null && _a !== void 0 ? _a : '';
|
|
433
|
+
const status = mapGitStatus(statusToken);
|
|
434
|
+
if (statusToken.startsWith('R') || statusToken.startsWith('C')) {
|
|
435
|
+
if (parts.length < 3)
|
|
436
|
+
continue;
|
|
437
|
+
const oldPath = parts[1];
|
|
438
|
+
const newPath = parts[2];
|
|
439
|
+
if (oldPath === undefined || newPath === undefined)
|
|
440
|
+
continue;
|
|
441
|
+
entries.push({ path: newPath, status, oldPath });
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
const pathOnly = parts[1];
|
|
445
|
+
if (pathOnly === undefined)
|
|
446
|
+
continue;
|
|
447
|
+
entries.push({ path: pathOnly, status });
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return entries;
|
|
451
|
+
}
|
|
452
|
+
function mergeNameEntriesByPath(entries) {
|
|
453
|
+
var _a;
|
|
454
|
+
const byPath = new Map();
|
|
455
|
+
for (const e of entries) {
|
|
456
|
+
const existing = byPath.get(e.path);
|
|
457
|
+
if (!existing) {
|
|
458
|
+
byPath.set(e.path, Object.assign({}, e));
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
existing.status = mergeStatus(existing.status, e.status);
|
|
462
|
+
if (e.oldPath) {
|
|
463
|
+
existing.oldPath = (_a = existing.oldPath) !== null && _a !== void 0 ? _a : e.oldPath;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return byPath;
|
|
468
|
+
}
|
|
469
|
+
function numStatPathToLookupKey(pathField) {
|
|
470
|
+
const brace = /^(.*)\{(.+) => (.+)\}$/.exec(pathField);
|
|
471
|
+
if (!brace) {
|
|
472
|
+
return pathField;
|
|
473
|
+
}
|
|
474
|
+
const dirRaw = brace[1];
|
|
475
|
+
const toSeg = brace[3].trim();
|
|
476
|
+
return `${dirRaw}${toSeg}`;
|
|
477
|
+
}
|
|
478
|
+
function accumulateNumStat(numStatOutput, into) {
|
|
479
|
+
var _a, _b, _c;
|
|
480
|
+
for (const rawLine of numStatOutput.split(/\r?\n/)) {
|
|
481
|
+
const line = rawLine.trim();
|
|
482
|
+
if (!line)
|
|
483
|
+
continue;
|
|
484
|
+
const parts = line.split('\t');
|
|
485
|
+
if (parts.length < 3)
|
|
486
|
+
continue;
|
|
487
|
+
const addStr = (_a = parts[0]) !== null && _a !== void 0 ? _a : '';
|
|
488
|
+
const delStr = (_b = parts[1]) !== null && _b !== void 0 ? _b : '';
|
|
489
|
+
const pathField = parts.slice(2).join('\t');
|
|
490
|
+
const additions = addStr !== '-' ? Number.parseInt(addStr, 10) || 0 : 0;
|
|
491
|
+
const deletions = delStr !== '-' ? Number.parseInt(delStr, 10) || 0 : 0;
|
|
492
|
+
const key = numStatPathToLookupKey(pathField);
|
|
493
|
+
const prev = (_c = into.get(key)) !== null && _c !== void 0 ? _c : { additions: 0, deletions: 0 };
|
|
494
|
+
into.set(key, { additions: prev.additions + additions, deletions: prev.deletions + deletions });
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
function diffStatusToSyntheticPrefix(status) {
|
|
498
|
+
switch (status) {
|
|
499
|
+
case 'added':
|
|
500
|
+
return 'A';
|
|
501
|
+
case 'deleted':
|
|
502
|
+
return 'D';
|
|
503
|
+
case 'renamed':
|
|
504
|
+
return 'R100';
|
|
505
|
+
case 'copied':
|
|
506
|
+
return 'C100';
|
|
507
|
+
case 'type-changed':
|
|
508
|
+
return 'T';
|
|
509
|
+
case 'modified':
|
|
510
|
+
return 'M';
|
|
511
|
+
default:
|
|
512
|
+
return 'X';
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
function buildDiffSummaryFromGitOutputs(nameStatusOutput, numStatOutput) {
|
|
516
|
+
var _a;
|
|
517
|
+
const numMap = new Map();
|
|
518
|
+
accumulateNumStat(numStatOutput, numMap);
|
|
519
|
+
const mergedName = mergeNameEntriesByPath(parseNameStatusLines(nameStatusOutput));
|
|
520
|
+
const syntheticLines = [];
|
|
521
|
+
for (const [path, meta] of mergedName) {
|
|
522
|
+
const counts = (_a = numMap.get(path)) !== null && _a !== void 0 ? _a : { additions: 0, deletions: 0 };
|
|
523
|
+
const prefix = diffStatusToSyntheticPrefix(meta.status);
|
|
524
|
+
if (meta.oldPath) {
|
|
525
|
+
syntheticLines.push(`${prefix}\t${counts.additions}\t${counts.deletions}\t${meta.oldPath}\t${path}`);
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
syntheticLines.push(`${prefix}\t${counts.additions}\t${counts.deletions}\t${path}`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return parseDiffSummary(syntheticLines.join('\n'));
|
|
532
|
+
}
|
|
533
|
+
function parseDiffSummary(diffOutput) {
|
|
534
|
+
var _a, _b, _c;
|
|
535
|
+
const fileMap = new Map();
|
|
536
|
+
for (const rawLine of diffOutput.split(/\r?\n/)) {
|
|
537
|
+
const line = rawLine.trim();
|
|
538
|
+
if (!line)
|
|
539
|
+
continue;
|
|
540
|
+
const parts = line.split('\t');
|
|
541
|
+
if (parts.length < 3)
|
|
542
|
+
continue;
|
|
543
|
+
const statusToken = (_a = parts.shift()) !== null && _a !== void 0 ? _a : '';
|
|
544
|
+
const status = mapGitStatus(statusToken);
|
|
545
|
+
const additions = parts[0] && parts[0] !== '-' ? Number.parseInt(parts[0], 10) || 0 : 0;
|
|
546
|
+
const deletions = parts[1] && parts[1] !== '-' ? Number.parseInt(parts[1], 10) || 0 : 0;
|
|
547
|
+
let oldPath;
|
|
548
|
+
let newPath;
|
|
549
|
+
if (parts.length === 3) {
|
|
550
|
+
newPath = parts[2];
|
|
551
|
+
}
|
|
552
|
+
else if (parts.length === 4) {
|
|
553
|
+
oldPath = parts[2];
|
|
554
|
+
newPath = parts[3];
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
const path = newPath;
|
|
560
|
+
const existing = fileMap.get(path);
|
|
561
|
+
if (existing) {
|
|
562
|
+
existing.additions += additions;
|
|
563
|
+
existing.deletions += deletions;
|
|
564
|
+
existing.status = mergeStatus(existing.status, status);
|
|
565
|
+
if (oldPath)
|
|
566
|
+
existing.oldPath = (_b = existing.oldPath) !== null && _b !== void 0 ? _b : oldPath;
|
|
567
|
+
existing.newPath = (_c = existing.newPath) !== null && _c !== void 0 ? _c : newPath;
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
fileMap.set(path, {
|
|
571
|
+
path,
|
|
572
|
+
status,
|
|
573
|
+
additions,
|
|
574
|
+
deletions,
|
|
575
|
+
oldPath,
|
|
576
|
+
newPath: oldPath ? newPath : undefined,
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
const files = Array.from(fileMap.values());
|
|
581
|
+
return {
|
|
582
|
+
files,
|
|
583
|
+
totalFiles: files.length,
|
|
584
|
+
totalAdditions: files.reduce((sum, file) => sum + file.additions, 0),
|
|
585
|
+
totalDeletions: files.reduce((sum, file) => sum + file.deletions, 0),
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function hasNonEmptyTrimmed(arr) {
|
|
590
|
+
return (arr !== null && arr !== void 0 ? arr : []).some((s) => s.trim().length > 0);
|
|
591
|
+
}
|
|
592
|
+
function shouldFilterByCommits(allCommits, filtered, opts) {
|
|
593
|
+
if (hasNonEmptyTrimmed(opts.commitMessageIncludeRegexes) || hasNonEmptyTrimmed(opts.commitMessageExcludeRegexes)) {
|
|
594
|
+
return true;
|
|
595
|
+
}
|
|
596
|
+
return filtered.length !== allCommits.length;
|
|
597
|
+
}
|
|
598
|
+
function summarizeGitDiff(options) {
|
|
599
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
600
|
+
var _a, _b;
|
|
601
|
+
const git = (_a = options.git) !== null && _a !== void 0 ? _a : createGitClient(options.cwd);
|
|
602
|
+
const from = options.from;
|
|
603
|
+
const to = (_b = options.to) !== null && _b !== void 0 ? _b : 'HEAD';
|
|
604
|
+
const pathFilter = hasNonEmptyTrimmed(options.includeFolders) || hasNonEmptyTrimmed(options.excludeFolders)
|
|
605
|
+
? {
|
|
606
|
+
includeFolders: options.includeFolders,
|
|
607
|
+
excludeFolders: options.excludeFolders,
|
|
608
|
+
}
|
|
609
|
+
: undefined;
|
|
610
|
+
const allCommits = yield getCommits(git, from, to);
|
|
611
|
+
const filteredCommits = filterCommitsByMessageRegexes(allCommits, options.commitMessageIncludeRegexes, options.commitMessageExcludeRegexes);
|
|
612
|
+
const filterByCommits = shouldFilterByCommits(allCommits, filteredCommits, options);
|
|
613
|
+
const [diffText, fileNames, diffSummary] = yield Promise.all([
|
|
614
|
+
getDiff(git, from, to, filteredCommits, filterByCommits, pathFilter),
|
|
615
|
+
getChangedFiles(git, from, to, filteredCommits, filterByCommits, pathFilter),
|
|
616
|
+
getDiffSummary(git, from, to, filteredCommits, filterByCommits, pathFilter),
|
|
617
|
+
]);
|
|
618
|
+
const summarizeFlags = {
|
|
619
|
+
from,
|
|
620
|
+
to,
|
|
621
|
+
team: options.teamName,
|
|
622
|
+
model: options.model,
|
|
623
|
+
maxDiffChars: options.maxDiffChars,
|
|
624
|
+
systemPrompt: options.systemPrompt,
|
|
625
|
+
commitMessageIncludeRegexes: options.commitMessageIncludeRegexes,
|
|
626
|
+
commitMessageExcludeRegexes: options.commitMessageExcludeRegexes,
|
|
627
|
+
};
|
|
628
|
+
return generateSummary(diffText, fileNames, filteredCommits, summarizeFlags, options.openAiClientProvider, diffSummary);
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
exports.DEFAULT_GIT_DIFF_SYSTEM_PROMPT = DEFAULT_GIT_DIFF_SYSTEM_PROMPT;
|
|
633
|
+
exports.LLM_GATEWAY_REQUIRED_MESSAGE = LLM_GATEWAY_REQUIRED_MESSAGE;
|
|
634
|
+
exports.buildDiffPathspecs = buildDiffPathspecs;
|
|
635
|
+
exports.createGitClient = createGitClient;
|
|
636
|
+
exports.createOpenAiLikeClient = createOpenAiLikeClient;
|
|
637
|
+
exports.filterCommitsByMessageRegexes = filterCommitsByMessageRegexes;
|
|
638
|
+
exports.generateSummary = generateSummary;
|
|
639
|
+
exports.getChangedFiles = getChangedFiles;
|
|
640
|
+
exports.getCommits = getCommits;
|
|
641
|
+
exports.getDiff = getDiff;
|
|
642
|
+
exports.getDiffSummary = getDiffSummary;
|
|
643
|
+
exports.getRepoRoot = getRepoRoot;
|
|
644
|
+
exports.parseLlmDefaultHeadersFromEnv = parseLlmDefaultHeadersFromEnv;
|
|
645
|
+
exports.resolveLlmBaseUrl = resolveLlmBaseUrl;
|
|
646
|
+
exports.resolveLlmMaxDiffChars = resolveLlmMaxDiffChars;
|
|
647
|
+
exports.resolveOpenAiLikeClientInit = resolveOpenAiLikeClientInit;
|
|
648
|
+
exports.shouldUseLlmGateway = shouldUseLlmGateway;
|
|
649
|
+
exports.splitPromotableAuthorizationFromHeaders = splitPromotableAuthorizationFromHeaders;
|
|
650
|
+
exports.summarizeGitDiff = summarizeGitDiff;
|
|
651
|
+
exports.truncateUnifiedDiffForLlm = truncateUnifiedDiffForLlm;
|
|
652
|
+
//# sourceMappingURL=index.cjs.map
|