prodex 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +369 -146
- package/dist/app/dependency-source-provider.d.ts +2 -0
- package/dist/app/dependency-source-provider.js +66 -0
- package/dist/app/entry-resolver.d.ts +7 -0
- package/dist/app/entry-resolver.js +26 -0
- package/dist/app/execute-run.d.ts +2 -2
- package/dist/app/execute-run.js +138 -36
- package/dist/app/git-source-provider.d.ts +4 -0
- package/dist/app/git-source-provider.js +551 -0
- package/dist/app/grep-source-provider.d.ts +2 -0
- package/dist/app/grep-source-provider.js +226 -0
- package/dist/app/planner.d.ts +12 -0
- package/dist/app/planner.js +106 -0
- package/dist/app/planners/attachment-options.d.ts +2 -0
- package/dist/app/planners/attachment-options.js +39 -0
- package/dist/app/planners/git-plan.d.ts +13 -0
- package/dist/app/planners/git-plan.js +141 -0
- package/dist/app/planners/grep-plan.d.ts +13 -0
- package/dist/app/planners/grep-plan.js +115 -0
- package/dist/app/planners/list-utils.d.ts +1 -0
- package/dist/app/planners/list-utils.js +6 -0
- package/dist/app/planners/pack-plan.d.ts +13 -0
- package/dist/app/planners/pack-plan.js +65 -0
- package/dist/app/planners/scope-plan.d.ts +17 -0
- package/dist/app/planners/scope-plan.js +143 -0
- package/dist/app/planners/trace-plan.d.ts +13 -0
- package/dist/app/planners/trace-plan.js +77 -0
- package/dist/app/source-collector.d.ts +5 -0
- package/dist/app/source-collector.js +33 -0
- package/dist/app/target-resolver.d.ts +7 -0
- package/dist/app/target-resolver.js +110 -0
- package/dist/cli/cli-input.js +70 -19
- package/dist/cli/flag-specs.d.ts +7 -3
- package/dist/cli/flag-specs.js +58 -7
- package/dist/cli/help-specs.d.ts +205 -0
- package/dist/cli/help-specs.js +136 -0
- package/dist/cli/help.js +107 -28
- package/dist/cli/report-command.js +59 -26
- package/dist/commands/{run-command.d.ts → git-command.d.ts} +3 -3
- package/dist/commands/git-command.js +7 -0
- package/dist/commands/grep-command.d.ts +11 -0
- package/dist/commands/grep-command.js +7 -0
- package/dist/commands/pack-command.d.ts +11 -0
- package/dist/commands/pack-command.js +7 -0
- package/dist/commands/scope-command.d.ts +12 -0
- package/dist/commands/scope-command.js +7 -0
- package/dist/commands/shared-runner.d.ts +12 -0
- package/dist/commands/shared-runner.js +30 -0
- package/dist/commands/trace-command.d.ts +11 -0
- package/dist/commands/trace-command.js +7 -0
- package/dist/config/default-config.js +7 -11
- package/dist/config/load.d.ts +1 -0
- package/dist/config/load.js +269 -4
- package/dist/config/migration/detect.d.ts +2 -1
- package/dist/config/migration/detect.js +10 -2
- package/dist/config/migration/messages.js +12 -11
- package/dist/config/migration/transform.js +77 -32
- package/dist/filesystem/binary.d.ts +2 -0
- package/dist/filesystem/binary.js +30 -0
- package/dist/filesystem/entry-discovery.d.ts +14 -0
- package/dist/filesystem/entry-discovery.js +78 -0
- package/dist/filesystem/file-set.d.ts +7 -0
- package/dist/filesystem/file-set.js +57 -0
- package/dist/filesystem/glob-scan.d.ts +6 -2
- package/dist/filesystem/glob-scan.js +12 -7
- package/dist/filesystem/path-patterns.d.ts +20 -0
- package/dist/filesystem/path-patterns.js +70 -0
- package/dist/index.js +51 -29
- package/dist/output/markdown-sections.d.ts +5 -0
- package/dist/output/markdown-sections.js +103 -0
- package/dist/output/markdown-toc.d.ts +31 -0
- package/dist/output/markdown-toc.js +287 -0
- package/dist/output/markdown.d.ts +5 -8
- package/dist/output/markdown.js +80 -127
- package/dist/output/produce-output.d.ts +4 -1
- package/dist/output/produce-output.js +6 -7
- package/dist/output/render-helpers.d.ts +4 -0
- package/dist/output/render-helpers.js +14 -0
- package/dist/output/text.d.ts +2 -2
- package/dist/output/text.js +47 -8
- package/dist/resolvers/js/resolve-alias.js +10 -41
- package/dist/resolvers/php/bindings.js +6 -6
- package/dist/resolvers/php/extract-imports.d.ts +19 -7
- package/dist/resolvers/php/extract-imports.js +146 -26
- package/dist/resolvers/php/php-resolver.js +55 -24
- package/dist/resolvers/php/psr4.d.ts +1 -1
- package/dist/resolvers/php/psr4.js +7 -1
- package/dist/runtime/shell-command-runner.d.ts +5 -0
- package/dist/runtime/shell-command-runner.js +118 -0
- package/dist/tracing/collect-trace.d.ts +10 -0
- package/dist/tracing/collect-trace.js +23 -0
- package/dist/tracing/exclude.d.ts +0 -5
- package/dist/tracing/exclude.js +16 -12
- package/dist/tracing/follow-chain.js +2 -2
- package/dist/types/app.types.d.ts +38 -8
- package/dist/types/artifact.types.d.ts +50 -0
- package/dist/types/artifact.types.js +2 -0
- package/dist/types/cli.types.d.ts +52 -4
- package/dist/types/config.types.d.ts +37 -20
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.js +2 -0
- package/dist/types/output.types.d.ts +12 -3
- package/dist/types/planner.types.d.ts +76 -0
- package/dist/types/planner.types.js +2 -0
- package/dist/types/resolver.types.d.ts +1 -1
- package/dist/types/tracing.types.d.ts +0 -11
- package/package.json +2 -2
- package/schema/prodex.schema.json +59 -41
- package/dist/app/run-plans.d.ts +0 -11
- package/dist/app/run-plans.js +0 -61
- package/dist/commands/profiles-command.d.ts +0 -6
- package/dist/commands/profiles-command.js +0 -16
- package/dist/commands/run-command.js +0 -17
- package/dist/config/build-config.d.ts +0 -13
- package/dist/config/build-config.js +0 -127
- package/dist/tracing/include-files.d.ts +0 -2
- package/dist/tracing/include-files.js +0 -36
- package/dist/tracing/trace-run.d.ts +0 -2
- package/dist/tracing/trace-run.js +0 -21
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runGit = runGit;
|
|
7
|
+
exports.runGitBuffer = runGitBuffer;
|
|
8
|
+
exports.collectGitSources = collectGitSources;
|
|
9
|
+
const child_process_1 = require("child_process");
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const binary_1 = require("../filesystem/binary");
|
|
13
|
+
const file_set_1 = require("../filesystem/file-set");
|
|
14
|
+
const path_2 = require("../filesystem/path");
|
|
15
|
+
const exclude_1 = require("../tracing/exclude");
|
|
16
|
+
function runGit(args, cwd, warnings, errors, maxBuffer = 10 * 1024 * 1024) {
|
|
17
|
+
try {
|
|
18
|
+
const res = (0, child_process_1.spawnSync)("git", args, {
|
|
19
|
+
cwd,
|
|
20
|
+
shell: false,
|
|
21
|
+
encoding: "utf8",
|
|
22
|
+
maxBuffer,
|
|
23
|
+
});
|
|
24
|
+
if (res.error) {
|
|
25
|
+
const err = res.error;
|
|
26
|
+
if (err.code === "ENOBUFS") {
|
|
27
|
+
warnings.push("Git output exceeded limit and was truncated.");
|
|
28
|
+
return (res.stdout || "").slice(0, maxBuffer);
|
|
29
|
+
}
|
|
30
|
+
if (err.code === "ENOENT") {
|
|
31
|
+
errors.push("Git executable not found in PATH.");
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
errors.push(`Git command error: ${err.message || String(err)}`);
|
|
35
|
+
}
|
|
36
|
+
return "";
|
|
37
|
+
}
|
|
38
|
+
if (res.status !== 0) {
|
|
39
|
+
errors.push(`Git command failed (exit code ${res.status}): ${res.stderr || "unknown error"}`);
|
|
40
|
+
return "";
|
|
41
|
+
}
|
|
42
|
+
if (res.stderr) {
|
|
43
|
+
warnings.push(`Git command stderr: ${res.stderr}`);
|
|
44
|
+
}
|
|
45
|
+
return res.stdout || "";
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
errors.push(`Git execution error: ${err.message || String(err)}`);
|
|
49
|
+
return "";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function runGitBuffer(args, cwd, warnings, errors, maxBuffer = 10 * 1024 * 1024) {
|
|
53
|
+
try {
|
|
54
|
+
const res = (0, child_process_1.spawnSync)("git", args, {
|
|
55
|
+
cwd,
|
|
56
|
+
shell: false,
|
|
57
|
+
encoding: "buffer",
|
|
58
|
+
maxBuffer,
|
|
59
|
+
});
|
|
60
|
+
if (res.error) {
|
|
61
|
+
const err = res.error;
|
|
62
|
+
if (err.code === "ENOBUFS") {
|
|
63
|
+
warnings.push("Git output exceeded limit and was truncated.");
|
|
64
|
+
return (res.stdout || Buffer.alloc(0)).subarray(0, maxBuffer);
|
|
65
|
+
}
|
|
66
|
+
if (err.code === "ENOENT") {
|
|
67
|
+
errors.push("Git executable not found in PATH.");
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
errors.push(`Git command error: ${err.message || String(err)}`);
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
if (res.status !== 0) {
|
|
75
|
+
errors.push(`Git command failed (exit code ${res.status}): ${res.stderr ? res.stderr.toString("utf8") : "unknown error"}`);
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
return res.stdout || Buffer.alloc(0);
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
errors.push(`Git execution error: ${err.message || String(err)}`);
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function readHistoricalFile(rev, gitRelPath, gitRoot, warnings, errors) {
|
|
86
|
+
const initialErrorCount = errors.length;
|
|
87
|
+
const buf = runGitBuffer(["show", `${rev}:${gitRelPath}`], gitRoot, warnings, errors);
|
|
88
|
+
if (errors.length > initialErrorCount) {
|
|
89
|
+
const lastError = errors[errors.length - 1];
|
|
90
|
+
if (lastError.includes("does not exist in") ||
|
|
91
|
+
lastError.includes("exists on disk but not in") ||
|
|
92
|
+
lastError.includes("Path '") ||
|
|
93
|
+
lastError.includes("exists on disk, but not in")) {
|
|
94
|
+
errors.splice(initialErrorCount); // clear new errors
|
|
95
|
+
return { content: null, isBinary: false, isDeleted: true };
|
|
96
|
+
}
|
|
97
|
+
return { content: null, isBinary: false, isDeleted: false };
|
|
98
|
+
}
|
|
99
|
+
if (!buf) {
|
|
100
|
+
return { content: null, isBinary: false, isDeleted: false };
|
|
101
|
+
}
|
|
102
|
+
const isBinary = (0, binary_1.isBinaryBuffer)(buf);
|
|
103
|
+
if (isBinary) {
|
|
104
|
+
return { content: null, isBinary: true, isDeleted: false };
|
|
105
|
+
}
|
|
106
|
+
return { content: buf.toString("utf8"), isBinary: false, isDeleted: false };
|
|
107
|
+
}
|
|
108
|
+
function buildHistoricalSections(gitOptions, gitRoot, changesSummaryList, warnings, errors) {
|
|
109
|
+
const sections = [];
|
|
110
|
+
// 1. Git Mode section
|
|
111
|
+
let modeContent = "";
|
|
112
|
+
if (gitOptions.mode === "commit") {
|
|
113
|
+
const commitInfo = runGit(["show", "-s", "--format=Revision: %H%nAuthor: %an <%ae>%nDate: %ad%nMessage: %s", gitOptions.rev], gitRoot, warnings, errors).trim();
|
|
114
|
+
modeContent = `Mode: commit\n${commitInfo || `Revision: ${gitOptions.rev}`}`;
|
|
115
|
+
}
|
|
116
|
+
else if (gitOptions.mode === "range") {
|
|
117
|
+
const baseSha = runGit(["rev-parse", gitOptions.base], gitRoot, warnings, errors).trim();
|
|
118
|
+
const headSha = runGit(["rev-parse", gitOptions.head], gitRoot, warnings, errors).trim();
|
|
119
|
+
modeContent = `Mode: range\nSpec: ${gitOptions.spec}\nBase SHA: ${baseSha}\nHead SHA: ${headSha}`;
|
|
120
|
+
}
|
|
121
|
+
else if (gitOptions.mode === "against") {
|
|
122
|
+
const baseSha = runGit(["rev-parse", gitOptions.base], gitRoot, warnings, errors).trim();
|
|
123
|
+
const headSha = runGit(["rev-parse", "HEAD"], gitRoot, warnings, errors).trim();
|
|
124
|
+
modeContent = `Mode: against\nAgainst: ${gitOptions.base} (${baseSha})\nMerge Base: ${gitOptions.mergeBase}\nHEAD SHA: ${headSha}`;
|
|
125
|
+
}
|
|
126
|
+
sections.push({
|
|
127
|
+
id: "git-mode",
|
|
128
|
+
title: "Git Mode",
|
|
129
|
+
kind: "code",
|
|
130
|
+
language: "txt",
|
|
131
|
+
content: modeContent || "(unknown mode)",
|
|
132
|
+
});
|
|
133
|
+
// 2. Diff Stat section
|
|
134
|
+
let statTitle = "Git Diff Stat";
|
|
135
|
+
let statContent = "";
|
|
136
|
+
if (gitOptions.mode === "commit") {
|
|
137
|
+
statTitle = "Commit Diff Stat";
|
|
138
|
+
statContent = runGit(["show", "--stat", "--oneline", gitOptions.rev], gitRoot, warnings, errors);
|
|
139
|
+
}
|
|
140
|
+
else if (gitOptions.mode === "range") {
|
|
141
|
+
statTitle = "Range Diff Stat";
|
|
142
|
+
statContent = runGit(["diff", "--stat", gitOptions.spec], gitRoot, warnings, errors);
|
|
143
|
+
}
|
|
144
|
+
else if (gitOptions.mode === "against") {
|
|
145
|
+
statTitle = "Branch Diff Stat";
|
|
146
|
+
statContent = runGit(["diff", "--stat", gitOptions.mergeBase, "HEAD"], gitRoot, warnings, errors);
|
|
147
|
+
}
|
|
148
|
+
sections.push({
|
|
149
|
+
id: "git-diff-stat",
|
|
150
|
+
title: statTitle,
|
|
151
|
+
kind: "code",
|
|
152
|
+
language: "txt",
|
|
153
|
+
content: statContent || "(no diff stat)",
|
|
154
|
+
});
|
|
155
|
+
// 3. File Notes section
|
|
156
|
+
const filteredSummary = [...changesSummaryList];
|
|
157
|
+
if (filteredSummary.length === 0) {
|
|
158
|
+
filteredSummary.push("(none)");
|
|
159
|
+
}
|
|
160
|
+
sections.push({
|
|
161
|
+
id: "file-notes",
|
|
162
|
+
title: "File Notes",
|
|
163
|
+
kind: "text",
|
|
164
|
+
content: filteredSummary.join("\n"),
|
|
165
|
+
});
|
|
166
|
+
// 4. Full Diff (when includeDiff is passed)
|
|
167
|
+
if (gitOptions.includeDiff) {
|
|
168
|
+
let fullDiffTitle = "Full Diff";
|
|
169
|
+
let fullDiffContent = "";
|
|
170
|
+
if (gitOptions.mode === "commit") {
|
|
171
|
+
fullDiffTitle = "Commit Diff";
|
|
172
|
+
fullDiffContent = runGit(["show", gitOptions.rev], gitRoot, warnings, errors, 5 * 1024 * 1024);
|
|
173
|
+
}
|
|
174
|
+
else if (gitOptions.mode === "range") {
|
|
175
|
+
fullDiffTitle = "Range Diff";
|
|
176
|
+
fullDiffContent = runGit(["diff", gitOptions.spec], gitRoot, warnings, errors, 5 * 1024 * 1024);
|
|
177
|
+
}
|
|
178
|
+
else if (gitOptions.mode === "against") {
|
|
179
|
+
fullDiffTitle = "Branch Diff";
|
|
180
|
+
fullDiffContent = runGit(["diff", gitOptions.mergeBase, "HEAD"], gitRoot, warnings, errors, 5 * 1024 * 1024);
|
|
181
|
+
}
|
|
182
|
+
sections.push({
|
|
183
|
+
id: "full-diff",
|
|
184
|
+
title: fullDiffTitle,
|
|
185
|
+
kind: "code",
|
|
186
|
+
language: "diff",
|
|
187
|
+
content: fullDiffContent || "(no changes)",
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
return sections;
|
|
191
|
+
}
|
|
192
|
+
async function collectHistoricalSources(plan, gitOptions, gitRoot, warnings, errors) {
|
|
193
|
+
// 1. Resolve snapshot revision and file listing parameters
|
|
194
|
+
let snapshotRev = "";
|
|
195
|
+
let diffOut = "";
|
|
196
|
+
if (gitOptions.mode === "commit") {
|
|
197
|
+
// Verify commit exists
|
|
198
|
+
const revParse = runGit(["rev-parse", "--verify", gitOptions.rev], gitRoot, warnings, errors).trim();
|
|
199
|
+
if (errors.length || !revParse) {
|
|
200
|
+
return { files: [], entries: [], includes: plan.include ?? [], mode: "git", warnings, errors };
|
|
201
|
+
}
|
|
202
|
+
snapshotRev = gitOptions.rev;
|
|
203
|
+
// Run git diff-tree -M --root --no-commit-id -r --name-status -z <rev>
|
|
204
|
+
diffOut = runGit(["diff-tree", "-M", "--root", "--no-commit-id", "-r", "--name-status", "-z", snapshotRev], gitRoot, warnings, errors);
|
|
205
|
+
}
|
|
206
|
+
else if (gitOptions.mode === "range") {
|
|
207
|
+
// Warn if using three-dot range
|
|
208
|
+
if (gitOptions.spec.includes("...")) {
|
|
209
|
+
warnings.push(`Using three-dot range: comparing merge-base(${gitOptions.base}, ${gitOptions.head}) to ${gitOptions.head}.`);
|
|
210
|
+
}
|
|
211
|
+
// Verify endpoints
|
|
212
|
+
const baseParse = runGit(["rev-parse", "--verify", gitOptions.base], gitRoot, warnings, errors).trim();
|
|
213
|
+
const headParse = runGit(["rev-parse", "--verify", gitOptions.head], gitRoot, warnings, errors).trim();
|
|
214
|
+
if (errors.length || !baseParse || !headParse) {
|
|
215
|
+
return { files: [], entries: [], includes: plan.include ?? [], mode: "git", warnings, errors };
|
|
216
|
+
}
|
|
217
|
+
snapshotRev = gitOptions.head;
|
|
218
|
+
// Run git diff -M --name-status -z <spec>
|
|
219
|
+
diffOut = runGit(["diff", "-M", "--name-status", "-z", gitOptions.spec], gitRoot, warnings, errors);
|
|
220
|
+
}
|
|
221
|
+
else if (gitOptions.mode === "against") {
|
|
222
|
+
// Resolve merge-base
|
|
223
|
+
const mergeBase = runGit(["merge-base", gitOptions.base, "HEAD"], gitRoot, warnings, errors).trim();
|
|
224
|
+
if (errors.length || !mergeBase) {
|
|
225
|
+
errors.push(`Failed to find merge-base between ${gitOptions.base} and HEAD.`);
|
|
226
|
+
return { files: [], entries: [], includes: plan.include ?? [], mode: "git", warnings, errors };
|
|
227
|
+
}
|
|
228
|
+
gitOptions.mergeBase = mergeBase;
|
|
229
|
+
snapshotRev = "HEAD";
|
|
230
|
+
// Run git diff -M --name-status -z <mergeBase> HEAD
|
|
231
|
+
diffOut = runGit(["diff", "-M", "--name-status", "-z", mergeBase, "HEAD"], gitRoot, warnings, errors);
|
|
232
|
+
}
|
|
233
|
+
if (errors.length) {
|
|
234
|
+
return { files: [], entries: [], includes: plan.include ?? [], mode: "git", warnings, errors };
|
|
235
|
+
}
|
|
236
|
+
// 2. Parse NUL-separated name-status output
|
|
237
|
+
const tokens = diffOut.split("\0");
|
|
238
|
+
const parsedFiles = [];
|
|
239
|
+
let i = 0;
|
|
240
|
+
while (i < tokens.length) {
|
|
241
|
+
const status = tokens[i];
|
|
242
|
+
if (!status) {
|
|
243
|
+
i++;
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (status.startsWith("R") || status.startsWith("C")) {
|
|
247
|
+
const oldPath = tokens[i + 1];
|
|
248
|
+
const newPath = tokens[i + 2];
|
|
249
|
+
if (oldPath && newPath) {
|
|
250
|
+
parsedFiles.push({ status, path: newPath, oldPath });
|
|
251
|
+
}
|
|
252
|
+
i += 3;
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
const filePath = tokens[i + 1];
|
|
256
|
+
if (filePath) {
|
|
257
|
+
parsedFiles.push({ status, path: filePath });
|
|
258
|
+
}
|
|
259
|
+
i += 2;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// 3. Apply excludes to notes too! Consistent exclude filtering
|
|
263
|
+
const filteredParsedFiles = parsedFiles.filter((file) => {
|
|
264
|
+
const absPath = (0, path_2.normalizePath)(path_1.default.resolve(gitRoot, file.path));
|
|
265
|
+
return !(0, exclude_1.isExcluded)(absPath, plan.exclude, plan.root);
|
|
266
|
+
});
|
|
267
|
+
// 4. Resolve file listing and snapshots
|
|
268
|
+
const gitSelectedPaths = [];
|
|
269
|
+
const snapshots = [];
|
|
270
|
+
const changesSummaryList = [];
|
|
271
|
+
const relPath = (p) => path_1.default.relative(plan.root, p).replaceAll("\\", "/");
|
|
272
|
+
for (const file of filteredParsedFiles) {
|
|
273
|
+
const absPath = (0, path_2.normalizePath)(path_1.default.resolve(gitRoot, file.path));
|
|
274
|
+
const isDeleted = file.status.startsWith("D");
|
|
275
|
+
if (isDeleted) {
|
|
276
|
+
changesSummaryList.push(`- Deleted: ${relPath(absPath)}`);
|
|
277
|
+
gitSelectedPaths.push(absPath);
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (file.status.startsWith("R")) {
|
|
281
|
+
const oldAbsPath = (0, path_2.normalizePath)(path_1.default.resolve(gitRoot, file.oldPath));
|
|
282
|
+
changesSummaryList.push(`- Renamed: ${relPath(oldAbsPath)} -> ${relPath(absPath)}`);
|
|
283
|
+
}
|
|
284
|
+
else if (file.status.startsWith("C")) {
|
|
285
|
+
const oldAbsPath = (0, path_2.normalizePath)(path_1.default.resolve(gitRoot, file.oldPath));
|
|
286
|
+
changesSummaryList.push(`- Copied: ${relPath(oldAbsPath)} -> ${relPath(absPath)}`);
|
|
287
|
+
}
|
|
288
|
+
// Read content from git history
|
|
289
|
+
const res = readHistoricalFile(snapshotRev, file.path, gitRoot, warnings, errors);
|
|
290
|
+
if (res.isDeleted) {
|
|
291
|
+
changesSummaryList.push(`- Deleted: ${relPath(absPath)}`);
|
|
292
|
+
gitSelectedPaths.push(absPath);
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (res.isBinary) {
|
|
296
|
+
changesSummaryList.push(`- Binary: ${relPath(absPath)}`);
|
|
297
|
+
gitSelectedPaths.push(absPath);
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if (res.content !== null) {
|
|
301
|
+
gitSelectedPaths.push(absPath);
|
|
302
|
+
snapshots.push({
|
|
303
|
+
path: absPath,
|
|
304
|
+
content: res.content,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
// readError
|
|
309
|
+
gitSelectedPaths.push(absPath);
|
|
310
|
+
snapshots.push({
|
|
311
|
+
path: absPath,
|
|
312
|
+
content: "",
|
|
313
|
+
readError: "Failed to read file from Git history.",
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
// 5. Merge, resolve includes, apply excludes
|
|
318
|
+
const filteredPaths = await (0, file_set_1.buildFinalFileSet)({
|
|
319
|
+
root: plan.root,
|
|
320
|
+
sources: gitSelectedPaths,
|
|
321
|
+
include: plan.include ?? [],
|
|
322
|
+
exclude: plan.exclude,
|
|
323
|
+
});
|
|
324
|
+
// 6. Classify final path set
|
|
325
|
+
const finalFiles = [];
|
|
326
|
+
for (const p of filteredPaths) {
|
|
327
|
+
const isHistorical = gitSelectedPaths.includes(p);
|
|
328
|
+
if (isHistorical) {
|
|
329
|
+
const hasSnapshot = snapshots.some((s) => s.path === p);
|
|
330
|
+
if (hasSnapshot) {
|
|
331
|
+
finalFiles.push(p);
|
|
332
|
+
}
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
// Otherwise, it came from includes, so read from disk
|
|
336
|
+
const isDeleted = !fs_1.default.existsSync(p);
|
|
337
|
+
if (isDeleted) {
|
|
338
|
+
changesSummaryList.push(`- Deleted: ${relPath(p)}`);
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
if ((0, binary_1.isBinaryFile)(p)) {
|
|
342
|
+
changesSummaryList.push(`- Binary: ${relPath(p)}`);
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
finalFiles.push(p);
|
|
346
|
+
}
|
|
347
|
+
// 7. Validation error for empty results
|
|
348
|
+
const hasGitMatches = gitSelectedPaths.length > 0;
|
|
349
|
+
const hasIncludes = plan.include && plan.include.length > 0;
|
|
350
|
+
if (!hasGitMatches && !hasIncludes) {
|
|
351
|
+
errors.push("No files matched the Git revision/range options and no include patterns were provided.");
|
|
352
|
+
return { files: [], entries: [], includes: plan.include ?? [], mode: "git", warnings, errors };
|
|
353
|
+
}
|
|
354
|
+
// 8. Generate Git metadata sections (Commit 3)
|
|
355
|
+
const sections = buildHistoricalSections(gitOptions, gitRoot, changesSummaryList, warnings, errors);
|
|
356
|
+
return {
|
|
357
|
+
files: finalFiles,
|
|
358
|
+
entries: [],
|
|
359
|
+
includes: plan.include ?? [],
|
|
360
|
+
mode: "git",
|
|
361
|
+
warnings,
|
|
362
|
+
errors,
|
|
363
|
+
sections,
|
|
364
|
+
snapshots,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
async function collectGitSources(plan) {
|
|
368
|
+
const warnings = [];
|
|
369
|
+
const errors = [];
|
|
370
|
+
// 1. Verify if we are inside a Git repository and get the top level directory
|
|
371
|
+
const isInside = runGit(["rev-parse", "--is-inside-work-tree"], plan.root, warnings, errors);
|
|
372
|
+
if (errors.length) {
|
|
373
|
+
return { files: [], entries: [], includes: plan.include ?? [], mode: "git", warnings, errors };
|
|
374
|
+
}
|
|
375
|
+
if (isInside.trim() !== "true") {
|
|
376
|
+
errors.push("Not a git repository (or any of the parent directories).");
|
|
377
|
+
return { files: [], entries: [], includes: plan.include ?? [], mode: "git", warnings, errors };
|
|
378
|
+
}
|
|
379
|
+
const gitRoot = runGit(["rev-parse", "--show-toplevel"], plan.root, warnings, errors).trim();
|
|
380
|
+
if (errors.length || !gitRoot) {
|
|
381
|
+
errors.push("Failed to resolve git top-level directory.");
|
|
382
|
+
return { files: [], entries: [], includes: plan.include ?? [], mode: "git", warnings, errors };
|
|
383
|
+
}
|
|
384
|
+
const gitOptions = plan.gitOptions || { mode: "working-tree", changed: true, staged: true, unstaged: true, untracked: true, includeDiff: false };
|
|
385
|
+
if (gitOptions.mode !== "working-tree" && gitOptions.mode !== undefined) {
|
|
386
|
+
return collectHistoricalSources(plan, gitOptions, gitRoot, warnings, errors);
|
|
387
|
+
}
|
|
388
|
+
// 2. Query git status --porcelain -z -uall relative to Git root
|
|
389
|
+
const statusOut = runGit(["status", "--porcelain", "-z", "-uall"], gitRoot, warnings, errors);
|
|
390
|
+
if (errors.length) {
|
|
391
|
+
return { files: [], entries: [], includes: plan.include ?? [], mode: "git", warnings, errors };
|
|
392
|
+
}
|
|
393
|
+
// Determine active source options (already parsed above as working-tree)
|
|
394
|
+
// 3. Parse porcelain entries
|
|
395
|
+
const tokens = statusOut.split("\0");
|
|
396
|
+
const gitSelectedPaths = [];
|
|
397
|
+
const deletedList = [];
|
|
398
|
+
const renamedList = [];
|
|
399
|
+
let i = 0;
|
|
400
|
+
while (i < tokens.length) {
|
|
401
|
+
const token = tokens[i];
|
|
402
|
+
if (!token) {
|
|
403
|
+
i++;
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
const xy = token.slice(0, 2);
|
|
407
|
+
const path1 = token.slice(3);
|
|
408
|
+
const isRename = xy[0] === "R" || xy[0] === "C" || xy[1] === "R" || xy[1] === "C";
|
|
409
|
+
let fileGitRel = "";
|
|
410
|
+
let oldFileGitRel = "";
|
|
411
|
+
if (isRename) {
|
|
412
|
+
fileGitRel = path1; // new path / destination
|
|
413
|
+
oldFileGitRel = tokens[i + 1] || ""; // old path / source
|
|
414
|
+
i += 2;
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
fileGitRel = path1;
|
|
418
|
+
i++;
|
|
419
|
+
}
|
|
420
|
+
if (!fileGitRel)
|
|
421
|
+
continue;
|
|
422
|
+
// Classify entry using X and Y status codes
|
|
423
|
+
const X = xy[0];
|
|
424
|
+
const Y = xy[1];
|
|
425
|
+
const isUntracked = (X === "?" && Y === "?");
|
|
426
|
+
const isStaged = !isUntracked && X !== " " && X !== "!" && X !== "?";
|
|
427
|
+
const isUnstaged = !isUntracked && Y !== " " && Y !== "!" && Y !== "?";
|
|
428
|
+
let isSelected = false;
|
|
429
|
+
if (gitOptions.staged && isStaged)
|
|
430
|
+
isSelected = true;
|
|
431
|
+
if (gitOptions.unstaged && isUnstaged)
|
|
432
|
+
isSelected = true;
|
|
433
|
+
if (gitOptions.untracked && isUntracked)
|
|
434
|
+
isSelected = true;
|
|
435
|
+
if (isSelected) {
|
|
436
|
+
const absolutePath = (0, path_2.normalizePath)(path_1.default.resolve(gitRoot, fileGitRel));
|
|
437
|
+
gitSelectedPaths.push(absolutePath);
|
|
438
|
+
if (X === "D" || Y === "D") {
|
|
439
|
+
deletedList.push(absolutePath);
|
|
440
|
+
}
|
|
441
|
+
if (isRename) {
|
|
442
|
+
const oldAbsolutePath = (0, path_2.normalizePath)(path_1.default.resolve(gitRoot, oldFileGitRel));
|
|
443
|
+
renamedList.push({ oldPath: oldAbsolutePath, newPath: absolutePath });
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
// 4. Merge, resolve includes, apply excludes, and sort deterministically
|
|
448
|
+
const filteredPaths = await (0, file_set_1.buildFinalFileSet)({
|
|
449
|
+
root: plan.root,
|
|
450
|
+
sources: gitSelectedPaths,
|
|
451
|
+
include: plan.include ?? [],
|
|
452
|
+
exclude: plan.exclude,
|
|
453
|
+
});
|
|
454
|
+
// 7. Classify final path set
|
|
455
|
+
const finalFiles = [];
|
|
456
|
+
const changesSummaryList = [];
|
|
457
|
+
// Map paths back to plan-root-relative or just project relative for summary display
|
|
458
|
+
const relPath = (p) => path_1.default.relative(plan.root, p).replaceAll("\\", "/");
|
|
459
|
+
for (const p of filteredPaths) {
|
|
460
|
+
const isDeleted = deletedList.includes(p) || !fs_1.default.existsSync(p);
|
|
461
|
+
if (isDeleted) {
|
|
462
|
+
changesSummaryList.push(`- Deleted: ${relPath(p)}`);
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
const renameMatch = renamedList.find(r => r.newPath === p);
|
|
466
|
+
if (renameMatch) {
|
|
467
|
+
changesSummaryList.push(`- Renamed: ${relPath(renameMatch.oldPath)} -> ${relPath(renameMatch.newPath)}`);
|
|
468
|
+
}
|
|
469
|
+
if ((0, binary_1.isBinaryFile)(p)) {
|
|
470
|
+
changesSummaryList.push(`- Binary: ${relPath(p)}`);
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
// It is a normal readable file or a readable file that might fail on snapshot
|
|
474
|
+
finalFiles.push(p);
|
|
475
|
+
}
|
|
476
|
+
// Double check validation:
|
|
477
|
+
// If no git selected files and no includes, fail clearly.
|
|
478
|
+
const hasGitMatches = gitSelectedPaths.length > 0;
|
|
479
|
+
const hasIncludes = plan.include && plan.include.length > 0;
|
|
480
|
+
if (!hasGitMatches && !hasIncludes) {
|
|
481
|
+
errors.push("No Git working-state files matched the active source flags and no include patterns were provided.");
|
|
482
|
+
return { files: [], entries: [], includes: plan.include ?? [], mode: "git", warnings, errors };
|
|
483
|
+
}
|
|
484
|
+
// 8. Generate Git metadata sections
|
|
485
|
+
const sections = [];
|
|
486
|
+
// Git Status short
|
|
487
|
+
const statusShort = runGit(["status", "--short"], plan.root, warnings, errors);
|
|
488
|
+
sections.push({
|
|
489
|
+
id: "git-status",
|
|
490
|
+
title: "Git Status",
|
|
491
|
+
kind: "code",
|
|
492
|
+
language: "txt",
|
|
493
|
+
content: statusShort || "(empty status)",
|
|
494
|
+
});
|
|
495
|
+
// Git Diff Stat
|
|
496
|
+
const diffStat = runGit(["diff", "--stat"], plan.root, warnings, errors);
|
|
497
|
+
sections.push({
|
|
498
|
+
id: "git-diff-stat",
|
|
499
|
+
title: "Git Diff Stat",
|
|
500
|
+
kind: "code",
|
|
501
|
+
language: "txt",
|
|
502
|
+
content: diffStat || "(no diff stat)",
|
|
503
|
+
});
|
|
504
|
+
// Git Cached Diff Stat
|
|
505
|
+
const diffCachedStat = runGit(["diff", "--cached", "--stat"], plan.root, warnings, errors);
|
|
506
|
+
sections.push({
|
|
507
|
+
id: "git-diff-cached-stat",
|
|
508
|
+
title: "Git Cached Diff Stat",
|
|
509
|
+
kind: "code",
|
|
510
|
+
language: "txt",
|
|
511
|
+
content: diffCachedStat || "(no cached diff stat)",
|
|
512
|
+
});
|
|
513
|
+
// File Notes
|
|
514
|
+
if (changesSummaryList.length === 0) {
|
|
515
|
+
changesSummaryList.push("(none)");
|
|
516
|
+
}
|
|
517
|
+
sections.push({
|
|
518
|
+
id: "file-notes",
|
|
519
|
+
title: "File Notes",
|
|
520
|
+
kind: "text",
|
|
521
|
+
content: changesSummaryList.join("\n"),
|
|
522
|
+
});
|
|
523
|
+
// Optional full diff sections
|
|
524
|
+
if (gitOptions.includeDiff) {
|
|
525
|
+
const diffText = runGit(["diff"], plan.root, warnings, errors, 5 * 1024 * 1024);
|
|
526
|
+
sections.push({
|
|
527
|
+
id: "full-diff",
|
|
528
|
+
title: "Full Diff",
|
|
529
|
+
kind: "code",
|
|
530
|
+
language: "diff",
|
|
531
|
+
content: diffText || "(no changes)",
|
|
532
|
+
});
|
|
533
|
+
const diffCachedText = runGit(["diff", "--cached"], plan.root, warnings, errors, 5 * 1024 * 1024);
|
|
534
|
+
sections.push({
|
|
535
|
+
id: "cached-full-diff",
|
|
536
|
+
title: "Cached Full Diff",
|
|
537
|
+
kind: "code",
|
|
538
|
+
language: "diff",
|
|
539
|
+
content: diffCachedText || "(no changes)",
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
return {
|
|
543
|
+
files: finalFiles,
|
|
544
|
+
entries: [],
|
|
545
|
+
includes: plan.include ?? [],
|
|
546
|
+
mode: "git",
|
|
547
|
+
warnings,
|
|
548
|
+
errors,
|
|
549
|
+
sections,
|
|
550
|
+
};
|
|
551
|
+
}
|