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