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