@davmash/bygone 0.8.13 → 0.8.16
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/CHANGELOG.md +16 -0
- package/README.md +2 -2
- package/cli/commandSpec.js +12 -2
- package/cli/completions.js +1 -1
- package/cli/present.js +42 -1
- package/cli/tour.js +13 -2
- package/cli/tourHistory.js +90 -0
- package/completions/_bygone +2 -1
- package/completions/bygone +1 -1
- package/completions/bygone.fish +1 -0
- package/docs/change-tour-format.md +46 -6
- package/docs/explore.md +36 -10
- package/docs/present.md +9 -1
- package/docs/product-surface.md +18 -14
- package/docs/releasing.md +25 -29
- package/media/diff.worker.js +3 -3
- package/media/webview.css +1 -1
- package/media/webview.js +173 -173
- package/out/changeTour.js +953 -410
- package/out/gitComparison.js +354 -0
- package/out/standalone-main.js +7678 -2124
- package/out/tourCoverage.js +992 -402
- package/package.json +1 -1
- package/schemas/change-tour-source.schema.json +58 -3
- package/skills/pr-tour-guide/SKILL.md +16 -7
- package/standalone/index.html +5 -0
- package/web/index.html +14 -0
- package/web/presenter.css +101 -0
- package/web/web-host.js +5 -5
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/gitComparison.ts
|
|
31
|
+
var gitComparison_exports = {};
|
|
32
|
+
__export(gitComparison_exports, {
|
|
33
|
+
createGitRunner: () => createGitRunner,
|
|
34
|
+
detectDefaultBaseRef: () => detectDefaultBaseRef,
|
|
35
|
+
listBranchCommits: () => listBranchCommits,
|
|
36
|
+
listChangedPaths: () => listChangedPaths,
|
|
37
|
+
materializeBranchReviewTrees: () => materializeBranchReviewTrees,
|
|
38
|
+
materializeGitTree: () => materializeGitTree,
|
|
39
|
+
parseNameStatusZ: () => parseNameStatusZ,
|
|
40
|
+
resolveBranchReviewRange: () => resolveBranchReviewRange,
|
|
41
|
+
resolveReviewPathPair: () => resolveReviewPathPair
|
|
42
|
+
});
|
|
43
|
+
module.exports = __toCommonJS(gitComparison_exports);
|
|
44
|
+
var import_child_process = require("child_process");
|
|
45
|
+
var fs = __toESM(require("fs"));
|
|
46
|
+
var path = __toESM(require("path"));
|
|
47
|
+
var DEFAULT_GIT_MAX_BUFFER_BYTES = 64 * 1024 * 1024;
|
|
48
|
+
function createGitRunner(maxBuffer = readGitMaxBuffer()) {
|
|
49
|
+
return (args, cwd) => (0, import_child_process.execFileSync)("git", [...args], {
|
|
50
|
+
cwd,
|
|
51
|
+
encoding: "utf8",
|
|
52
|
+
maxBuffer,
|
|
53
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
54
|
+
}).trimEnd();
|
|
55
|
+
}
|
|
56
|
+
function resolveBranchReviewRange(startPath, headRef = "HEAD", requestedBaseRef, runGit = createGitRunner()) {
|
|
57
|
+
const repoRoot = fs.realpathSync(runGit(["rev-parse", "--show-toplevel"], startPath));
|
|
58
|
+
const currentBranch = tryRunGit(runGit, ["symbolic-ref", "--quiet", "--short", "HEAD"], repoRoot);
|
|
59
|
+
const baseRef = requestedBaseRef || detectDefaultBaseRef(repoRoot, headRef, runGit);
|
|
60
|
+
const headOid = verifyCommit(repoRoot, headRef, runGit);
|
|
61
|
+
const baseOid = verifyCommit(repoRoot, baseRef, runGit);
|
|
62
|
+
const mergeBaseOids = runGit(["merge-base", "--all", baseOid, headOid], repoRoot).split("\n").filter(Boolean);
|
|
63
|
+
if (mergeBaseOids.length === 0) {
|
|
64
|
+
throw new Error(`No merge base exists between ${baseRef} and ${headRef}.`);
|
|
65
|
+
}
|
|
66
|
+
if (mergeBaseOids.length > 1) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`Multiple merge bases exist between ${baseRef} and ${headRef}: ${mergeBaseOids.map((oid) => oid.slice(0, 7)).join(", ")}. Pass one merge-base commit explicitly with --base.`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
const mergeBaseOid = mergeBaseOids[0];
|
|
72
|
+
return {
|
|
73
|
+
repoRoot,
|
|
74
|
+
baseRef,
|
|
75
|
+
headRef,
|
|
76
|
+
baseOid,
|
|
77
|
+
headOid,
|
|
78
|
+
mergeBaseOid,
|
|
79
|
+
currentBranch: currentBranch || void 0,
|
|
80
|
+
dirty: runGit(["status", "--porcelain=v1", "--untracked-files=normal"], repoRoot).length > 0,
|
|
81
|
+
changedPaths: listChangedPaths(repoRoot, mergeBaseOid, headOid, runGit),
|
|
82
|
+
commits: listBranchCommits(repoRoot, mergeBaseOid, headOid, runGit)
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function detectDefaultBaseRef(repoRoot, headRef = "HEAD", runGit = createGitRunner()) {
|
|
86
|
+
const remoteDefault = tryRunGit(
|
|
87
|
+
runGit,
|
|
88
|
+
["symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"],
|
|
89
|
+
repoRoot
|
|
90
|
+
);
|
|
91
|
+
const candidates = [remoteDefault, "main", "master"].filter((candidate) => Boolean(candidate)).filter((candidate, index, all) => all.indexOf(candidate) === index);
|
|
92
|
+
for (const candidate of candidates) {
|
|
93
|
+
if (candidate !== headRef && canResolveCommit(repoRoot, candidate, runGit)) {
|
|
94
|
+
return candidate;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
throw new Error("Could not detect a base branch. Pass --base <ref>.");
|
|
98
|
+
}
|
|
99
|
+
function listChangedPaths(repoRoot, baseOid, headOid, runGit = createGitRunner()) {
|
|
100
|
+
const output = runGit([
|
|
101
|
+
"diff",
|
|
102
|
+
"--name-status",
|
|
103
|
+
"-z",
|
|
104
|
+
"--find-renames",
|
|
105
|
+
"--find-copies",
|
|
106
|
+
baseOid,
|
|
107
|
+
headOid,
|
|
108
|
+
"--"
|
|
109
|
+
], repoRoot);
|
|
110
|
+
return parseNameStatusZ(output);
|
|
111
|
+
}
|
|
112
|
+
function parseNameStatusZ(output) {
|
|
113
|
+
const fields = output.split("\0");
|
|
114
|
+
if (fields[fields.length - 1] === "") {
|
|
115
|
+
fields.pop();
|
|
116
|
+
}
|
|
117
|
+
const changedPaths = [];
|
|
118
|
+
for (let index = 0; index < fields.length; ) {
|
|
119
|
+
const status = fields[index++];
|
|
120
|
+
if (!status) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const code = status[0];
|
|
124
|
+
const similarityText = status.slice(1);
|
|
125
|
+
const similarity = similarityText ? Number.parseInt(similarityText, 10) : void 0;
|
|
126
|
+
if (code === "R" || code === "C") {
|
|
127
|
+
const previousPath = fields[index++];
|
|
128
|
+
const nextPath = fields[index++];
|
|
129
|
+
if (previousPath && nextPath) {
|
|
130
|
+
changedPaths.push({
|
|
131
|
+
kind: code === "R" ? "renamed" : "copied",
|
|
132
|
+
path: nextPath,
|
|
133
|
+
previousPath,
|
|
134
|
+
similarity: Number.isFinite(similarity) ? similarity : void 0
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const changedPath = fields[index++];
|
|
140
|
+
if (!changedPath) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
changedPaths.push({
|
|
144
|
+
kind: changeKindForStatus(code),
|
|
145
|
+
path: changedPath
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return changedPaths;
|
|
149
|
+
}
|
|
150
|
+
function listBranchCommits(repoRoot, mergeBaseOid, headOid, runGit = createGitRunner()) {
|
|
151
|
+
const output = runGit([
|
|
152
|
+
"log",
|
|
153
|
+
"--reverse",
|
|
154
|
+
"--topo-order",
|
|
155
|
+
"--format=%H%x00%h%x00%cI%x00%s%x00%P%x1e",
|
|
156
|
+
`${mergeBaseOid}..${headOid}`
|
|
157
|
+
], repoRoot);
|
|
158
|
+
return output.split("").map((record) => record.replace(/^\n+|\n+$/g, "")).filter(Boolean).map((record) => {
|
|
159
|
+
const [oid = "", shortOid = "", timestamp = "", summary = "", parents = ""] = record.split("\0");
|
|
160
|
+
return {
|
|
161
|
+
oid,
|
|
162
|
+
shortOid,
|
|
163
|
+
timestamp,
|
|
164
|
+
summary,
|
|
165
|
+
parentOids: parents.split(" ").filter(Boolean)
|
|
166
|
+
};
|
|
167
|
+
}).filter((commit) => Boolean(commit.oid));
|
|
168
|
+
}
|
|
169
|
+
function materializeBranchReviewTrees(review, leftRoot, rightRoot) {
|
|
170
|
+
for (const changedPath of review.changedPaths) {
|
|
171
|
+
if (changedPath.kind === "renamed") {
|
|
172
|
+
materializeGitPath(review.repoRoot, review.mergeBaseOid, changedPath.previousPath, leftRoot);
|
|
173
|
+
materializeGitPath(review.repoRoot, review.headOid, changedPath.path, rightRoot);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (changedPath.kind !== "added" && changedPath.kind !== "copied") {
|
|
177
|
+
materializeGitPath(review.repoRoot, review.mergeBaseOid, changedPath.path, leftRoot);
|
|
178
|
+
}
|
|
179
|
+
if (changedPath.kind !== "deleted") {
|
|
180
|
+
materializeGitPath(review.repoRoot, review.headOid, changedPath.path, rightRoot);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function materializeGitTree(repoRoot, relativeDir, targetRoot, commit = "HEAD") {
|
|
185
|
+
for (const relativeFile of listGitBlobPaths(repoRoot, commit, relativeDir)) {
|
|
186
|
+
writeMaterializedGitFile(
|
|
187
|
+
targetRoot,
|
|
188
|
+
relativeFile,
|
|
189
|
+
readGitBlob(repoRoot, commit, relativeFile)
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function resolveReviewPathPair(changedPaths, selectedPath) {
|
|
194
|
+
const changedPath = changedPaths.find((candidate) => candidate.path === selectedPath || candidate.previousPath === selectedPath);
|
|
195
|
+
if (!changedPath) {
|
|
196
|
+
return void 0;
|
|
197
|
+
}
|
|
198
|
+
const relatesDistinctPaths = (changedPath.kind === "renamed" || changedPath.kind === "copied") && Boolean(changedPath.previousPath);
|
|
199
|
+
const leftPath = changedPath.kind === "added" ? null : relatesDistinctPaths ? changedPath.previousPath ?? null : changedPath.path;
|
|
200
|
+
const rightPath = changedPath.kind === "deleted" ? null : changedPath.path;
|
|
201
|
+
const relationVerb = changedPath.kind === "renamed" ? "Renamed" : changedPath.kind === "copied" ? "Copied" : null;
|
|
202
|
+
return {
|
|
203
|
+
key: changedPath.path,
|
|
204
|
+
leftPath,
|
|
205
|
+
rightPath,
|
|
206
|
+
kind: changedPath.kind,
|
|
207
|
+
similarity: changedPath.similarity,
|
|
208
|
+
summary: relationVerb && changedPath.previousPath ? `${relationVerb} ${changedPath.previousPath} \u2192 ${changedPath.path}` + (changedPath.similarity === void 0 ? "" : ` \xB7 ${changedPath.similarity}% similarity`) : void 0
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function verifyCommit(repoRoot, ref, runGit) {
|
|
212
|
+
try {
|
|
213
|
+
return runGit(["rev-parse", "--verify", `${ref}^{commit}`], repoRoot);
|
|
214
|
+
} catch {
|
|
215
|
+
throw new Error(`Could not resolve git ref "${ref}".`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function canResolveCommit(repoRoot, ref, runGit) {
|
|
219
|
+
try {
|
|
220
|
+
verifyCommit(repoRoot, ref, runGit);
|
|
221
|
+
return true;
|
|
222
|
+
} catch {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function tryRunGit(runGit, args, cwd) {
|
|
227
|
+
try {
|
|
228
|
+
return runGit(args, cwd);
|
|
229
|
+
} catch {
|
|
230
|
+
return "";
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
var GITLINK_MODE = "160000";
|
|
234
|
+
function materializeGitPath(repoRoot, commit, relativePath, targetRoot) {
|
|
235
|
+
if (!relativePath || !isGitBlobPath(repoRoot, commit, relativePath)) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
writeMaterializedGitFile(targetRoot, relativePath, readGitBlob(repoRoot, commit, relativePath));
|
|
239
|
+
}
|
|
240
|
+
function writeMaterializedGitFile(targetRoot, relativePath, content) {
|
|
241
|
+
const resolvedRoot = path.resolve(targetRoot);
|
|
242
|
+
const targetFile = path.resolve(resolvedRoot, relativePath);
|
|
243
|
+
if (targetFile !== resolvedRoot && !targetFile.startsWith(`${resolvedRoot}${path.sep}`)) {
|
|
244
|
+
throw new Error(`Refusing to materialize path outside review root: ${relativePath}`);
|
|
245
|
+
}
|
|
246
|
+
fs.mkdirSync(path.dirname(targetFile), { recursive: true });
|
|
247
|
+
fs.writeFileSync(targetFile, content);
|
|
248
|
+
}
|
|
249
|
+
function listGitBlobPaths(repoRoot, commit, relativeDir) {
|
|
250
|
+
if (commit === "INDEX") {
|
|
251
|
+
const args2 = ["ls-files", "--stage", "-z", "--"];
|
|
252
|
+
if (relativeDir) {
|
|
253
|
+
args2.push(relativeDir);
|
|
254
|
+
}
|
|
255
|
+
return parseNulRecords(runGitText(repoRoot, args2)).flatMap((record) => {
|
|
256
|
+
const parsed = parseIndexStageRecord(record);
|
|
257
|
+
return parsed && parsed.mode !== GITLINK_MODE ? [parsed.path] : [];
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
const args = ["ls-tree", "-r", "-z", commit];
|
|
261
|
+
if (relativeDir) {
|
|
262
|
+
args.push("--", relativeDir);
|
|
263
|
+
}
|
|
264
|
+
return parseNulRecords(runGitText(repoRoot, args)).flatMap((record) => {
|
|
265
|
+
const parsed = parseLsTreeRecord(record);
|
|
266
|
+
return parsed && parsed.type === "blob" ? [parsed.path] : [];
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
function isGitBlobPath(repoRoot, commit, relativePath) {
|
|
270
|
+
if (commit === "INDEX") {
|
|
271
|
+
return listGitBlobPaths(repoRoot, commit, relativePath).includes(relativePath);
|
|
272
|
+
}
|
|
273
|
+
const parsed = parseLsTreeRecord(parseNulRecords(runGitText(repoRoot, ["ls-tree", "-z", commit, "--", relativePath]))[0] ?? "");
|
|
274
|
+
return (parsed == null ? void 0 : parsed.type) === "blob";
|
|
275
|
+
}
|
|
276
|
+
function parseNulRecords(output) {
|
|
277
|
+
return output.split("\0").filter((record) => record.length > 0);
|
|
278
|
+
}
|
|
279
|
+
function parseLsTreeRecord(record) {
|
|
280
|
+
const tab = record.indexOf(" ");
|
|
281
|
+
if (tab === -1) {
|
|
282
|
+
return void 0;
|
|
283
|
+
}
|
|
284
|
+
const type = record.slice(0, tab).split(" ")[1];
|
|
285
|
+
const relativePath = record.slice(tab + 1);
|
|
286
|
+
if (!type || !relativePath) {
|
|
287
|
+
return void 0;
|
|
288
|
+
}
|
|
289
|
+
return { type, path: relativePath };
|
|
290
|
+
}
|
|
291
|
+
function parseIndexStageRecord(record) {
|
|
292
|
+
const tab = record.indexOf(" ");
|
|
293
|
+
if (tab === -1) {
|
|
294
|
+
return void 0;
|
|
295
|
+
}
|
|
296
|
+
const mode = record.slice(0, tab).split(" ")[0];
|
|
297
|
+
const relativePath = record.slice(tab + 1);
|
|
298
|
+
if (!mode || !relativePath) {
|
|
299
|
+
return void 0;
|
|
300
|
+
}
|
|
301
|
+
return { mode, path: relativePath };
|
|
302
|
+
}
|
|
303
|
+
function readGitBlob(repoRoot, commit, relativePath) {
|
|
304
|
+
const spec = commit && commit !== "INDEX" ? `${commit}:${relativePath}` : `:${relativePath}`;
|
|
305
|
+
return (0, import_child_process.execFileSync)("git", ["show", spec], {
|
|
306
|
+
cwd: repoRoot,
|
|
307
|
+
encoding: "buffer",
|
|
308
|
+
maxBuffer: readGitMaxBuffer(),
|
|
309
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
function runGitText(repoRoot, args) {
|
|
313
|
+
return (0, import_child_process.execFileSync)("git", [...args], {
|
|
314
|
+
cwd: repoRoot,
|
|
315
|
+
encoding: "utf8",
|
|
316
|
+
maxBuffer: readGitMaxBuffer(),
|
|
317
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
function changeKindForStatus(code) {
|
|
321
|
+
if (code === "A") {
|
|
322
|
+
return "added";
|
|
323
|
+
}
|
|
324
|
+
if (code === "D") {
|
|
325
|
+
return "deleted";
|
|
326
|
+
}
|
|
327
|
+
if (code === "M") {
|
|
328
|
+
return "modified";
|
|
329
|
+
}
|
|
330
|
+
if (code === "T") {
|
|
331
|
+
return "type-changed";
|
|
332
|
+
}
|
|
333
|
+
if (code === "U") {
|
|
334
|
+
return "unmerged";
|
|
335
|
+
}
|
|
336
|
+
return "unknown";
|
|
337
|
+
}
|
|
338
|
+
function readGitMaxBuffer() {
|
|
339
|
+
const parsed = Number.parseInt(process.env.BYGONE_GIT_MAX_BUFFER_BYTES ?? "", 10);
|
|
340
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_GIT_MAX_BUFFER_BYTES;
|
|
341
|
+
}
|
|
342
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
343
|
+
0 && (module.exports = {
|
|
344
|
+
createGitRunner,
|
|
345
|
+
detectDefaultBaseRef,
|
|
346
|
+
listBranchCommits,
|
|
347
|
+
listChangedPaths,
|
|
348
|
+
materializeBranchReviewTrees,
|
|
349
|
+
materializeGitTree,
|
|
350
|
+
parseNameStatusZ,
|
|
351
|
+
resolveBranchReviewRange,
|
|
352
|
+
resolveReviewPathPair
|
|
353
|
+
});
|
|
354
|
+
//# sourceMappingURL=gitComparison.js.map
|