gutterpress 0.11.6-alpha.3 → 0.11.6
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/dist/api/index.d.ts +3 -3
- package/dist/api/index.js +9 -5
- package/dist/{audit-t4r954ff.js → audit-vbyr2yaa.js} +4 -4
- package/dist/{build-8gx90w22.js → build-j0e2hrza.js} +4 -4
- package/dist/{cli-g6vshy3f.js → cli-02sh3r8x.js} +393 -968
- package/dist/{cli-5c2xfsmz.js → cli-1bccnym3.js} +1 -1
- package/dist/cli-4tx0szk7.js +1251 -0
- package/dist/{cli-rra50cp6.js → cli-emp8wz36.js} +1 -1
- package/dist/cli.js +14 -14
- package/dist/{doctor-b1awtn7m.js → doctor-radk1tcg.js} +2 -2
- package/dist/{ext-mxxz3p8t.js → ext-vtnqbct0.js} +4 -4
- package/dist/index-7mqst9tg.js +1302 -0
- package/dist/{index-0h00ve4y.js → index-yzcd0ezr.js} +468 -1042
- package/dist/index.js +9 -5
- package/dist/lib/project-scaffold.d.ts +20 -0
- package/dist/lib/source-provider.d.ts +24 -0
- package/dist/{lint-vyxf5zmw.js → lint-692hba2b.js} +4 -4
- package/dist/{new-r3s74yfv.js → new-431dez0g.js} +4 -4
- package/dist/{preflight-j9e9m4ea.js → preflight-9e0xm760.js} +4 -4
- package/dist/{preview-aprv0c49.js → preview-j429jbx1.js} +4 -4
- package/dist/{publish-4y1kztbd.js → publish-hr60t4j2.js} +4 -4
- package/dist/{source-provider-exy1wakv.js → source-provider-gy3cn7kw.js} +3 -1
- package/dist/{source-provider-y9ktprbx.js → source-provider-m5wes47h.js} +3 -1
- package/dist/{validate-sf6hga9e.js → validate-g1ckwmnz.js} +4 -4
- package/package.json +1 -1
- package/dist/cli-c5360prx.js +0 -645
- package/dist/index-8xkw284t.js +0 -696
package/dist/index-8xkw284t.js
DELETED
|
@@ -1,696 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
capabilitiesFor,
|
|
3
|
-
detectProjectSource,
|
|
4
|
-
findEnclosingRepoDir
|
|
5
|
-
} from "./index-v5mp7a6q.js";
|
|
6
|
-
|
|
7
|
-
// src/lib/git-fs.ts
|
|
8
|
-
import * as nodeFs from "node:fs";
|
|
9
|
-
var MUTABLE_GIT_METADATA = /(?:^|[\\/])\.git[\\/](?:index|HEAD|packed-refs|refs[\\/].+)$/;
|
|
10
|
-
function needsAtomicWrite(file) {
|
|
11
|
-
return typeof file === "string" && MUTABLE_GIT_METADATA.test(file);
|
|
12
|
-
}
|
|
13
|
-
var tempCounter = 0;
|
|
14
|
-
function tempPathFor(file) {
|
|
15
|
-
return `${file}.gp${process.pid.toString(36)}-${(tempCounter++).toString(36)}.tmp`;
|
|
16
|
-
}
|
|
17
|
-
function writeFile2(file, data, options, callback) {
|
|
18
|
-
const cb = typeof options === "function" ? options : callback;
|
|
19
|
-
const opts = typeof options === "function" ? undefined : options;
|
|
20
|
-
if (!needsAtomicWrite(file)) {
|
|
21
|
-
nodeFs.writeFile(file, data, opts, cb);
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
const temp = tempPathFor(file);
|
|
25
|
-
const failed = (err) => nodeFs.unlink(temp, () => cb(err));
|
|
26
|
-
nodeFs.writeFile(temp, data, opts, (writeErr) => {
|
|
27
|
-
if (writeErr)
|
|
28
|
-
return failed(writeErr);
|
|
29
|
-
nodeFs.rename(temp, file, (renameErr) => {
|
|
30
|
-
if (renameErr)
|
|
31
|
-
return failed(renameErr);
|
|
32
|
-
cb(null);
|
|
33
|
-
});
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
async function writeFilePromise(file, data, options) {
|
|
37
|
-
if (!needsAtomicWrite(file)) {
|
|
38
|
-
return nodeFs.promises.writeFile(file, data, options);
|
|
39
|
-
}
|
|
40
|
-
const temp = tempPathFor(file);
|
|
41
|
-
try {
|
|
42
|
-
await nodeFs.promises.writeFile(temp, data, options);
|
|
43
|
-
await nodeFs.promises.rename(temp, file);
|
|
44
|
-
} catch (err) {
|
|
45
|
-
await nodeFs.promises.unlink(temp).catch(() => {});
|
|
46
|
-
throw err;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
var promises2 = {
|
|
50
|
-
...nodeFs.promises,
|
|
51
|
-
writeFile: writeFilePromise
|
|
52
|
-
};
|
|
53
|
-
var gitFs = {
|
|
54
|
-
...nodeFs,
|
|
55
|
-
writeFile: writeFile2,
|
|
56
|
-
promises: promises2
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
// src/lib/source-provider.ts
|
|
60
|
-
import path2 from "node:path";
|
|
61
|
-
import git from "isomorphic-git";
|
|
62
|
-
|
|
63
|
-
// src/lib/remote-auth/operation-log.ts
|
|
64
|
-
import { appendFileSync, mkdirSync } from "node:fs";
|
|
65
|
-
import path from "node:path";
|
|
66
|
-
function sanitizeLogText(value, secrets = []) {
|
|
67
|
-
let text = value;
|
|
68
|
-
for (const secret of secrets) {
|
|
69
|
-
if (!secret)
|
|
70
|
-
continue;
|
|
71
|
-
for (const form of [secret, encodeURIComponent(secret)]) {
|
|
72
|
-
text = text.split(form).join("[redacted]");
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return text.replace(/\/\/[^/\s]*@/g, "//").replace(/([?&](?:access_token|token|password|secret|key|code)=)[^&#\s]*/gi, "$1[redacted]").replace(/\b(Bearer|Basic)\s+[A-Za-z0-9+/_.=~-]+/gi, "$1 [redacted]").replace(/\b(?:gh[pousr]_[A-Za-z0-9_]+|github_pat_[A-Za-z0-9_]+)/g, "[redacted]").replace(/[\s\x00-\x1f\x7f]+/g, " ").trim().slice(0, 6000);
|
|
76
|
-
}
|
|
77
|
-
function errorLogData(error) {
|
|
78
|
-
const out = { error: error == null ? String(error) : undefined };
|
|
79
|
-
let current = error;
|
|
80
|
-
const seen = new Set;
|
|
81
|
-
for (let depth = 0;depth < 3 && current != null && !seen.has(current); depth++) {
|
|
82
|
-
seen.add(current);
|
|
83
|
-
const prefix = depth === 0 ? "" : `cause${depth}_`;
|
|
84
|
-
if (typeof current !== "object") {
|
|
85
|
-
out[`${prefix}error`] = String(current);
|
|
86
|
-
break;
|
|
87
|
-
}
|
|
88
|
-
const err = current;
|
|
89
|
-
out[`${prefix}error`] = typeof err.message === "string" ? err.message : "Unknown error";
|
|
90
|
-
for (const key of ["code", "name", "caller", "stack", "syscall", "path"]) {
|
|
91
|
-
if (typeof err[key] === "string")
|
|
92
|
-
out[`${prefix}${key}`] = err[key];
|
|
93
|
-
}
|
|
94
|
-
const data = err.data;
|
|
95
|
-
if (data && typeof data === "object") {
|
|
96
|
-
for (const key of [
|
|
97
|
-
"statusCode",
|
|
98
|
-
"reason",
|
|
99
|
-
"prettyDetails",
|
|
100
|
-
"oid",
|
|
101
|
-
"ref",
|
|
102
|
-
"what",
|
|
103
|
-
"filepath",
|
|
104
|
-
"filepaths",
|
|
105
|
-
"bothModified",
|
|
106
|
-
"deleteByUs",
|
|
107
|
-
"deleteByTheirs"
|
|
108
|
-
]) {
|
|
109
|
-
const value = data[key];
|
|
110
|
-
if (typeof value === "string" || typeof value === "number")
|
|
111
|
-
out[`${prefix}${key}`] = value;
|
|
112
|
-
else if (Array.isArray(value)) {
|
|
113
|
-
out[`${prefix}${key}`] = value.filter((v) => typeof v === "string").slice(0, 50);
|
|
114
|
-
if (value.length > 50)
|
|
115
|
-
out[`${prefix}${key}Count`] = value.length;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
current = err.cause;
|
|
120
|
-
}
|
|
121
|
-
return out;
|
|
122
|
-
}
|
|
123
|
-
var noop = {
|
|
124
|
-
debug: () => {},
|
|
125
|
-
info: () => {},
|
|
126
|
-
warn: () => {},
|
|
127
|
-
error: () => {}
|
|
128
|
-
};
|
|
129
|
-
var LEVEL_ORDER = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
130
|
-
function createFileLogger(logFile, operation, minLevel = "debug", options = {}) {
|
|
131
|
-
try {
|
|
132
|
-
mkdirSync(path.dirname(logFile), { recursive: true });
|
|
133
|
-
} catch {
|
|
134
|
-
return noop;
|
|
135
|
-
}
|
|
136
|
-
function write(level, step, message, data) {
|
|
137
|
-
if (LEVEL_ORDER[level] < LEVEL_ORDER[minLevel])
|
|
138
|
-
return;
|
|
139
|
-
const ts = new Date().toISOString();
|
|
140
|
-
const clean = (value) => sanitizeLogText(value, options.secrets);
|
|
141
|
-
const fields = formatData({ ...options.context, ...data }, clean);
|
|
142
|
-
const line = `[${ts}] ${level.toUpperCase().padEnd(5)} ${clean(operation)}: step=${clean(step)}${fields} | ${clean(message)}
|
|
143
|
-
`;
|
|
144
|
-
try {
|
|
145
|
-
appendFileSync(logFile, line, "utf8");
|
|
146
|
-
} catch {}
|
|
147
|
-
}
|
|
148
|
-
return {
|
|
149
|
-
debug: (step, message, data) => write("debug", step, message, data),
|
|
150
|
-
info: (step, message, data) => write("info", step, message, data),
|
|
151
|
-
warn: (step, message, data) => write("warn", step, message, data),
|
|
152
|
-
error: (step, message, data) => write("error", step, message, data)
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
function resolveLogger(logFile, operation, options = {}) {
|
|
156
|
-
if (!logFile)
|
|
157
|
-
return noop;
|
|
158
|
-
return createFileLogger(logFile, operation, "debug", options);
|
|
159
|
-
}
|
|
160
|
-
function formatData(data, clean) {
|
|
161
|
-
if (!data)
|
|
162
|
-
return "";
|
|
163
|
-
const parts = [];
|
|
164
|
-
for (const [key, value] of Object.entries(data)) {
|
|
165
|
-
if (value === undefined)
|
|
166
|
-
continue;
|
|
167
|
-
if (Array.isArray(value)) {
|
|
168
|
-
parts.push(`${clean(key)}=${clean(value.join(","))}`);
|
|
169
|
-
} else {
|
|
170
|
-
parts.push(`${clean(key)}=${clean(String(value))}`);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
return parts.length > 0 ? " " + parts.join(" ") : "";
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// src/lib/source-provider.ts
|
|
177
|
-
var noopLogger = {
|
|
178
|
-
debug: () => {},
|
|
179
|
-
info: () => {},
|
|
180
|
-
warn: () => {},
|
|
181
|
-
error: () => {}
|
|
182
|
-
};
|
|
183
|
-
function gitScopeFor(source) {
|
|
184
|
-
return source.repoRoot || source.path;
|
|
185
|
-
}
|
|
186
|
-
var HISTORY_PAGE_LIMIT = 100;
|
|
187
|
-
var DEFAULT_AUTHOR = "gutterpress";
|
|
188
|
-
var DEFAULT_EMAIL = "noreply@gutterpress.local";
|
|
189
|
-
var DEFAULT_BRANCH = "main";
|
|
190
|
-
var SNAPSHOT_STAGING_MARKER = "gutterpress-snapshot-staging";
|
|
191
|
-
var repoQueues = new Map;
|
|
192
|
-
function repoLockKey(projectDir) {
|
|
193
|
-
const resolved = path2.resolve(projectDir);
|
|
194
|
-
try {
|
|
195
|
-
return gitFs.realpathSync(resolved);
|
|
196
|
-
} catch {
|
|
197
|
-
return resolved;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
function withRepoLock(projectDir, fn) {
|
|
201
|
-
const key = repoLockKey(projectDir);
|
|
202
|
-
const prev = repoQueues.get(key) ?? Promise.resolve();
|
|
203
|
-
const run = prev.then(fn, fn);
|
|
204
|
-
const tail = run.then(() => {
|
|
205
|
-
return;
|
|
206
|
-
}, () => {
|
|
207
|
-
return;
|
|
208
|
-
});
|
|
209
|
-
repoQueues.set(key, tail);
|
|
210
|
-
tail.then(() => {
|
|
211
|
-
if (repoQueues.get(key) === tail)
|
|
212
|
-
repoQueues.delete(key);
|
|
213
|
-
});
|
|
214
|
-
return run;
|
|
215
|
-
}
|
|
216
|
-
function __repoLockQueueSizeForTests() {
|
|
217
|
-
return repoQueues.size;
|
|
218
|
-
}
|
|
219
|
-
function gitAuthor(name, email) {
|
|
220
|
-
const n = (name ?? "").trim();
|
|
221
|
-
const e = (email ?? "").trim();
|
|
222
|
-
return { name: n || DEFAULT_AUTHOR, email: e || DEFAULT_EMAIL };
|
|
223
|
-
}
|
|
224
|
-
async function readGitAuthor(dir) {
|
|
225
|
-
const [name, email] = await Promise.all([
|
|
226
|
-
git.getConfig({ fs: gitFs, dir, path: "user.name" }).catch(() => {
|
|
227
|
-
return;
|
|
228
|
-
}),
|
|
229
|
-
git.getConfig({ fs: gitFs, dir, path: "user.email" }).catch(() => {
|
|
230
|
-
return;
|
|
231
|
-
})
|
|
232
|
-
]);
|
|
233
|
-
return {
|
|
234
|
-
name: typeof name === "string" ? name : undefined,
|
|
235
|
-
email: typeof email === "string" ? email : undefined
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
async function resolveGitAuthor(dir, name, email) {
|
|
239
|
-
const existing = await readGitAuthor(dir);
|
|
240
|
-
return gitAuthor(name || existing.name, email || existing.email);
|
|
241
|
-
}
|
|
242
|
-
async function listWorkdirChanges(dir, cache = {}) {
|
|
243
|
-
const adds = [];
|
|
244
|
-
const removes = [];
|
|
245
|
-
await git.walk({
|
|
246
|
-
fs: gitFs,
|
|
247
|
-
dir,
|
|
248
|
-
cache,
|
|
249
|
-
trees: [git.WORKDIR(), git.STAGE()],
|
|
250
|
-
map: async (filepath, [workdir, stage]) => {
|
|
251
|
-
if (filepath === ".")
|
|
252
|
-
return;
|
|
253
|
-
if (!stage && workdir && (filepath.startsWith(".git-damaged") || await git.isIgnored({ fs: gitFs, dir, filepath }))) {
|
|
254
|
-
return null;
|
|
255
|
-
}
|
|
256
|
-
const [wType, sType] = await Promise.all([
|
|
257
|
-
workdir ? workdir.type() : Promise.resolve(undefined),
|
|
258
|
-
stage ? stage.type() : Promise.resolve(undefined)
|
|
259
|
-
]);
|
|
260
|
-
if (wType === "tree" || sType === "tree") {
|
|
261
|
-
if (sType === "blob")
|
|
262
|
-
removes.push(filepath);
|
|
263
|
-
else if (wType === "blob")
|
|
264
|
-
adds.push(filepath);
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
if (wType === "blob" && !sType) {
|
|
268
|
-
adds.push(filepath);
|
|
269
|
-
} else if (!wType && sType === "blob") {
|
|
270
|
-
removes.push(filepath);
|
|
271
|
-
} else if (wType === "blob" && sType === "blob") {
|
|
272
|
-
const workdirOid = await workdir.oid();
|
|
273
|
-
const stageOid = await stage.oid();
|
|
274
|
-
if (workdirOid !== stageOid) {
|
|
275
|
-
adds.push(filepath);
|
|
276
|
-
} else {
|
|
277
|
-
const fileBytes = await gitFs.promises.readFile(path2.join(dir, filepath));
|
|
278
|
-
const { oid: actualOid } = await git.hashBlob({ object: fileBytes });
|
|
279
|
-
if (actualOid !== stageOid)
|
|
280
|
-
adds.push(filepath);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
});
|
|
286
|
-
return { adds, removes };
|
|
287
|
-
}
|
|
288
|
-
async function stageChanges(dir, changes, cache) {
|
|
289
|
-
if (changes.adds.length > 0) {
|
|
290
|
-
await git.add({ fs: gitFs, dir, cache, filepath: changes.adds });
|
|
291
|
-
}
|
|
292
|
-
for (const filepath of changes.removes) {
|
|
293
|
-
await git.remove({ fs: gitFs, dir, cache, filepath });
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
async function hasPendingChanges(dir, cache = {}) {
|
|
297
|
-
const { adds, removes } = await listWorkdirChanges(dir, cache);
|
|
298
|
-
return adds.length > 0 || removes.length > 0;
|
|
299
|
-
}
|
|
300
|
-
async function hasUncommittedChanges(dir, cache = {}) {
|
|
301
|
-
if (await hasPendingChanges(dir, cache))
|
|
302
|
-
return true;
|
|
303
|
-
return !await stageMatchesHead(dir, cache);
|
|
304
|
-
}
|
|
305
|
-
async function stageMatchesHead(dir, cache) {
|
|
306
|
-
let headOid;
|
|
307
|
-
try {
|
|
308
|
-
headOid = await git.resolveRef({ fs: gitFs, dir, ref: "HEAD" });
|
|
309
|
-
} catch {
|
|
310
|
-
return false;
|
|
311
|
-
}
|
|
312
|
-
let differs = false;
|
|
313
|
-
await git.walk({
|
|
314
|
-
fs: gitFs,
|
|
315
|
-
dir,
|
|
316
|
-
cache,
|
|
317
|
-
trees: [git.TREE({ ref: headOid }), git.STAGE()],
|
|
318
|
-
map: async (filepath, [tree, stage]) => {
|
|
319
|
-
if (differs || filepath === ".")
|
|
320
|
-
return differs ? null : undefined;
|
|
321
|
-
const [tType, sType] = await Promise.all([
|
|
322
|
-
tree ? tree.type() : Promise.resolve(undefined),
|
|
323
|
-
stage ? stage.type() : Promise.resolve(undefined)
|
|
324
|
-
]);
|
|
325
|
-
if (tType === "tree" || sType === "tree")
|
|
326
|
-
return;
|
|
327
|
-
if (!tType !== !sType) {
|
|
328
|
-
differs = true;
|
|
329
|
-
return null;
|
|
330
|
-
}
|
|
331
|
-
if (!tType || !sType)
|
|
332
|
-
return;
|
|
333
|
-
const [tOid, sOid] = await Promise.all([tree.oid(), stage.oid()]);
|
|
334
|
-
if (tOid !== sOid)
|
|
335
|
-
differs = true;
|
|
336
|
-
return null;
|
|
337
|
-
}
|
|
338
|
-
});
|
|
339
|
-
return !differs;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
class LocalFolderSourceProvider {
|
|
343
|
-
source;
|
|
344
|
-
capabilities;
|
|
345
|
-
constructor(source) {
|
|
346
|
-
this.source = source;
|
|
347
|
-
this.capabilities = capabilitiesFor(source);
|
|
348
|
-
}
|
|
349
|
-
async initVersionHistory(options) {
|
|
350
|
-
const dir = options.projectDir;
|
|
351
|
-
if (await findEnclosingRepoDir(dir) !== undefined) {
|
|
352
|
-
throw new Error("This folder is already inside a versioned book, so gutterpress " + "won't create a separate history here.");
|
|
353
|
-
}
|
|
354
|
-
return withRepoLock(dir, async () => {
|
|
355
|
-
const cache = {};
|
|
356
|
-
await git.init({ fs: gitFs, dir, defaultBranch: DEFAULT_BRANCH });
|
|
357
|
-
await stageChanges(dir, await listWorkdirChanges(dir, cache), cache);
|
|
358
|
-
await git.commit({
|
|
359
|
-
fs: gitFs,
|
|
360
|
-
dir,
|
|
361
|
-
cache,
|
|
362
|
-
message: options.initialMessage?.trim() || "Created project",
|
|
363
|
-
author: await resolveGitAuthor(dir, options.authorName, options.authorEmail)
|
|
364
|
-
});
|
|
365
|
-
return {
|
|
366
|
-
type: "local-git-folder",
|
|
367
|
-
path: dir,
|
|
368
|
-
repoRoot: dir,
|
|
369
|
-
subPath: "",
|
|
370
|
-
hasRemote: false,
|
|
371
|
-
branch: DEFAULT_BRANCH
|
|
372
|
-
};
|
|
373
|
-
});
|
|
374
|
-
}
|
|
375
|
-
snapshot() {
|
|
376
|
-
return Promise.reject(new Error("This book has no version history yet. Enable version history first."));
|
|
377
|
-
}
|
|
378
|
-
listHistory() {
|
|
379
|
-
return Promise.resolve([]);
|
|
380
|
-
}
|
|
381
|
-
listHistoryPage() {
|
|
382
|
-
return Promise.resolve({ entries: [], hasMore: false });
|
|
383
|
-
}
|
|
384
|
-
restore() {
|
|
385
|
-
return Promise.reject(new Error("This book has no version history yet. Enable version history first."));
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
class LocalGitSourceProvider {
|
|
390
|
-
source;
|
|
391
|
-
capabilities;
|
|
392
|
-
dir;
|
|
393
|
-
constructor(source) {
|
|
394
|
-
this.source = source;
|
|
395
|
-
this.capabilities = capabilitiesFor(source);
|
|
396
|
-
this.dir = gitScopeFor(source);
|
|
397
|
-
}
|
|
398
|
-
async initVersionHistory(options) {
|
|
399
|
-
try {
|
|
400
|
-
await this.snapshot({
|
|
401
|
-
projectDir: options.projectDir,
|
|
402
|
-
message: options.initialMessage?.trim() || "Created project",
|
|
403
|
-
authorName: options.authorName,
|
|
404
|
-
authorEmail: options.authorEmail
|
|
405
|
-
});
|
|
406
|
-
} catch (e) {
|
|
407
|
-
if (!isNoChangesError(e))
|
|
408
|
-
throw e;
|
|
409
|
-
}
|
|
410
|
-
return this.source;
|
|
411
|
-
}
|
|
412
|
-
snapshot(options) {
|
|
413
|
-
return withRepoLock(this.dir, () => this.snapshotUnlocked(options));
|
|
414
|
-
}
|
|
415
|
-
snapshotUnlocked(options) {
|
|
416
|
-
return snapshotWorkingTreeUnlocked({ ...options, repoRoot: this.dir });
|
|
417
|
-
}
|
|
418
|
-
async listHistory(projectDir) {
|
|
419
|
-
return (await this.listHistoryPage(projectDir)).entries;
|
|
420
|
-
}
|
|
421
|
-
async listHistoryPage(_projectDir, options = {}) {
|
|
422
|
-
const dir = this.dir;
|
|
423
|
-
const limit = Math.min(500, Math.max(1, options.limit ?? HISTORY_PAGE_LIMIT));
|
|
424
|
-
const before = options.before;
|
|
425
|
-
let commits;
|
|
426
|
-
try {
|
|
427
|
-
commits = await git.log({
|
|
428
|
-
fs: gitFs,
|
|
429
|
-
dir,
|
|
430
|
-
cache: {},
|
|
431
|
-
depth: limit + 2,
|
|
432
|
-
...before ? { ref: before } : {}
|
|
433
|
-
});
|
|
434
|
-
} catch (e) {
|
|
435
|
-
if (before && e?.code === "NotFoundError") {
|
|
436
|
-
return { entries: [], hasMore: false };
|
|
437
|
-
}
|
|
438
|
-
throw e;
|
|
439
|
-
}
|
|
440
|
-
const seen = new Set;
|
|
441
|
-
let filtered = commits.filter((c) => {
|
|
442
|
-
if (c.oid === before || seen.has(c.oid))
|
|
443
|
-
return false;
|
|
444
|
-
seen.add(c.oid);
|
|
445
|
-
return true;
|
|
446
|
-
});
|
|
447
|
-
const hasMore = filtered.length > limit;
|
|
448
|
-
if (hasMore)
|
|
449
|
-
filtered = filtered.slice(0, limit);
|
|
450
|
-
return {
|
|
451
|
-
entries: filtered.map((c) => ({
|
|
452
|
-
id: c.oid,
|
|
453
|
-
message: c.commit.message.trim(),
|
|
454
|
-
timestamp: c.commit.author.timestamp * 1000,
|
|
455
|
-
author: c.commit.author.name
|
|
456
|
-
})),
|
|
457
|
-
hasMore
|
|
458
|
-
};
|
|
459
|
-
}
|
|
460
|
-
restore(options) {
|
|
461
|
-
return withRepoLock(this.dir, () => this.restoreUnlocked(options));
|
|
462
|
-
}
|
|
463
|
-
async restoreUnlocked(options) {
|
|
464
|
-
await git.checkout({
|
|
465
|
-
fs: gitFs,
|
|
466
|
-
dir: this.dir,
|
|
467
|
-
cache: {},
|
|
468
|
-
ref: options.id,
|
|
469
|
-
force: true,
|
|
470
|
-
noUpdateHead: true
|
|
471
|
-
});
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
async function snapshotWorkingTreeUnlocked(options) {
|
|
475
|
-
const dir = options.repoRoot ?? options.projectDir;
|
|
476
|
-
const logger = options.logFile ? createFileLogger(options.logFile, "snapshot") : noopLogger;
|
|
477
|
-
const cache = options.cache ?? {};
|
|
478
|
-
const stagingMarker = snapshotStagingMarkerPath(dir);
|
|
479
|
-
const staleStaging = gitFs.existsSync(stagingMarker);
|
|
480
|
-
const changes = await listWorkdirChanges(dir, cache);
|
|
481
|
-
const workdirClean = changes.adds.length === 0 && changes.removes.length === 0;
|
|
482
|
-
if (workdirClean && !staleStaging) {
|
|
483
|
-
logger.debug("snapshot", "no changes — skipping");
|
|
484
|
-
throw new Error("No changes since the last snapshot — there is nothing new to save.");
|
|
485
|
-
}
|
|
486
|
-
if (workdirClean && staleStaging && await stageMatchesHead(dir, cache)) {
|
|
487
|
-
logger.debug("snapshot", "stale marker outlived a completed commit — clearing, not recommitting");
|
|
488
|
-
gitFs.rmSync(stagingMarker, { force: true });
|
|
489
|
-
const [head2] = await git.log({ fs: gitFs, dir, cache, depth: 1 });
|
|
490
|
-
return {
|
|
491
|
-
id: head2.oid,
|
|
492
|
-
message: head2.commit.message.trim(),
|
|
493
|
-
timestamp: head2.commit.author.timestamp * 1000,
|
|
494
|
-
author: head2.commit.author.name
|
|
495
|
-
};
|
|
496
|
-
}
|
|
497
|
-
logger.info("snapshot", "committing", {
|
|
498
|
-
adds: changes.adds.length,
|
|
499
|
-
removes: changes.removes.length,
|
|
500
|
-
...staleStaging ? { recoveredStaleStaging: true } : {}
|
|
501
|
-
});
|
|
502
|
-
gitFs.writeFileSync(stagingMarker, "");
|
|
503
|
-
await stageChanges(dir, changes, cache);
|
|
504
|
-
const author = await resolveGitAuthor(dir, options.authorName, options.authorEmail);
|
|
505
|
-
const id = await git.commit({
|
|
506
|
-
fs: gitFs,
|
|
507
|
-
dir,
|
|
508
|
-
cache,
|
|
509
|
-
message: options.message,
|
|
510
|
-
author
|
|
511
|
-
});
|
|
512
|
-
gitFs.rmSync(stagingMarker, { force: true });
|
|
513
|
-
const [head] = await git.log({ fs: gitFs, dir, cache, depth: 1 });
|
|
514
|
-
return {
|
|
515
|
-
id,
|
|
516
|
-
message: options.message,
|
|
517
|
-
timestamp: head ? head.commit.author.timestamp * 1000 : Date.now(),
|
|
518
|
-
author: author.name
|
|
519
|
-
};
|
|
520
|
-
}
|
|
521
|
-
function isNoChangesError(e) {
|
|
522
|
-
return e instanceof Error && /no changes since the last snapshot/i.test(e.message);
|
|
523
|
-
}
|
|
524
|
-
function providerFor(source) {
|
|
525
|
-
switch (source.type) {
|
|
526
|
-
case "local-folder":
|
|
527
|
-
return new LocalFolderSourceProvider(source);
|
|
528
|
-
case "local-git-folder":
|
|
529
|
-
return new LocalGitSourceProvider(source);
|
|
530
|
-
case "managed-github":
|
|
531
|
-
throw new Error("Managed GitHub books are not supported yet (#15/#16).");
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
function gitDirFor(projectDir) {
|
|
535
|
-
return path2.join(projectDir, ".git");
|
|
536
|
-
}
|
|
537
|
-
function snapshotStagingMarkerPath(projectDir) {
|
|
538
|
-
return path2.join(gitDirFor(projectDir), SNAPSHOT_STAGING_MARKER);
|
|
539
|
-
}
|
|
540
|
-
var RESTORE_BACKUP_MESSAGE = "Automatic backup before restoring an earlier version";
|
|
541
|
-
var AUTO_SNAPSHOT_MESSAGE = "Automatic snapshot";
|
|
542
|
-
async function restoreVersionWithBackup(options) {
|
|
543
|
-
const { projectDir, id, authorName, authorEmail } = options;
|
|
544
|
-
const source = await detectProjectSource(projectDir);
|
|
545
|
-
if (source.type !== "local-git-folder") {
|
|
546
|
-
throw new Error("This book has no version history yet. Enable version history first.");
|
|
547
|
-
}
|
|
548
|
-
const provider = new LocalGitSourceProvider(source);
|
|
549
|
-
const repoDir = gitScopeFor(source);
|
|
550
|
-
return withRepoLock(repoDir, async () => {
|
|
551
|
-
let backupId;
|
|
552
|
-
if (await hasPendingChanges(repoDir)) {
|
|
553
|
-
const backup = await provider.snapshotUnlocked({
|
|
554
|
-
projectDir,
|
|
555
|
-
message: RESTORE_BACKUP_MESSAGE,
|
|
556
|
-
authorName,
|
|
557
|
-
authorEmail
|
|
558
|
-
});
|
|
559
|
-
backupId = backup.id;
|
|
560
|
-
}
|
|
561
|
-
try {
|
|
562
|
-
await provider.restoreUnlocked({ projectDir, id });
|
|
563
|
-
} catch (cause) {
|
|
564
|
-
throw new Error(backupId ? "The restore could not be completed, but your work is safe — it was " + `automatically saved as a backup snapshot (${backupId.slice(0, 7)}) ` + "and appears in your version history." : "The restore could not be completed. Your book files were not changed.", { cause });
|
|
565
|
-
}
|
|
566
|
-
return { restoredId: id, backupId };
|
|
567
|
-
});
|
|
568
|
-
}
|
|
569
|
-
var SWITCH_BRANCH_SNAPSHOT_MESSAGE = "Saved before switching copy";
|
|
570
|
-
async function listLocalBranches(dir) {
|
|
571
|
-
const source = await detectProjectSource(dir);
|
|
572
|
-
if (source.type !== "local-git-folder")
|
|
573
|
-
return null;
|
|
574
|
-
const repoDir = gitScopeFor(source);
|
|
575
|
-
const [local, current, remotes] = await Promise.all([
|
|
576
|
-
git.listBranches({ fs: gitFs, dir: repoDir }),
|
|
577
|
-
git.currentBranch({ fs: gitFs, dir: repoDir, fullname: false }).catch(() => {
|
|
578
|
-
return;
|
|
579
|
-
}),
|
|
580
|
-
git.listRemotes({ fs: gitFs, dir: repoDir }).catch(() => [])
|
|
581
|
-
]);
|
|
582
|
-
const localNames = new Set(local);
|
|
583
|
-
const remoteOnly = new Set;
|
|
584
|
-
for (const { remote } of remotes) {
|
|
585
|
-
const names = await git.listBranches({ fs: gitFs, dir: repoDir, remote }).catch(() => []);
|
|
586
|
-
for (const name of names) {
|
|
587
|
-
if (name === "HEAD")
|
|
588
|
-
continue;
|
|
589
|
-
if (!localNames.has(name))
|
|
590
|
-
remoteOnly.add(name);
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
return {
|
|
594
|
-
current: current ?? null,
|
|
595
|
-
branches: [...localNames, ...remoteOnly].sort(),
|
|
596
|
-
remoteOnly: [...remoteOnly].sort()
|
|
597
|
-
};
|
|
598
|
-
}
|
|
599
|
-
async function resolveCopy(repoDir, branch) {
|
|
600
|
-
const local = await git.resolveRef({ fs: gitFs, dir: repoDir, ref: `refs/heads/${branch}` }).catch(() => {
|
|
601
|
-
return;
|
|
602
|
-
});
|
|
603
|
-
if (local)
|
|
604
|
-
return { oid: local };
|
|
605
|
-
const remotes = await git.listRemotes({ fs: gitFs, dir: repoDir }).catch(() => []);
|
|
606
|
-
for (const { remote } of remotes) {
|
|
607
|
-
const oid = await git.resolveRef({ fs: gitFs, dir: repoDir, ref: `refs/remotes/${remote}/${branch}` }).catch(() => {
|
|
608
|
-
return;
|
|
609
|
-
});
|
|
610
|
-
if (oid)
|
|
611
|
-
return { oid, remote };
|
|
612
|
-
}
|
|
613
|
-
throw new Error(`Couldn't find a copy named "${branch}" — it may have been renamed or deleted.`);
|
|
614
|
-
}
|
|
615
|
-
function isCheckoutConflictError(e) {
|
|
616
|
-
return e?.code === "CheckoutConflictError";
|
|
617
|
-
}
|
|
618
|
-
async function diffCommitPaths(dir, fromOid, toOid, cache) {
|
|
619
|
-
const changed = [];
|
|
620
|
-
await git.walk({
|
|
621
|
-
fs: gitFs,
|
|
622
|
-
dir,
|
|
623
|
-
cache,
|
|
624
|
-
trees: [git.TREE({ ref: fromOid }), git.TREE({ ref: toOid })],
|
|
625
|
-
map: async (filepath, [from, to]) => {
|
|
626
|
-
if (filepath === ".")
|
|
627
|
-
return;
|
|
628
|
-
const [fType, tType] = await Promise.all([
|
|
629
|
-
from ? from.type() : Promise.resolve(undefined),
|
|
630
|
-
to ? to.type() : Promise.resolve(undefined)
|
|
631
|
-
]);
|
|
632
|
-
if (fType === "tree" || tType === "tree")
|
|
633
|
-
return;
|
|
634
|
-
if (!fType && !tType)
|
|
635
|
-
return;
|
|
636
|
-
if (!fType || !tType) {
|
|
637
|
-
changed.push(filepath);
|
|
638
|
-
return;
|
|
639
|
-
}
|
|
640
|
-
const [fOid, tOid] = await Promise.all([from.oid(), to.oid()]);
|
|
641
|
-
if (fOid !== tOid)
|
|
642
|
-
changed.push(filepath);
|
|
643
|
-
}
|
|
644
|
-
});
|
|
645
|
-
return changed;
|
|
646
|
-
}
|
|
647
|
-
async function switchBranch(options) {
|
|
648
|
-
const { projectDir, branch, authorName, authorEmail } = options;
|
|
649
|
-
const source = await detectProjectSource(projectDir);
|
|
650
|
-
if (source.type !== "local-git-folder") {
|
|
651
|
-
throw new Error("This book has no version history yet. Enable version history first.");
|
|
652
|
-
}
|
|
653
|
-
const repoDir = gitScopeFor(source);
|
|
654
|
-
return withRepoLock(repoDir, async () => {
|
|
655
|
-
const cache = {};
|
|
656
|
-
try {
|
|
657
|
-
await snapshotWorkingTreeUnlocked({
|
|
658
|
-
projectDir,
|
|
659
|
-
repoRoot: repoDir,
|
|
660
|
-
message: SWITCH_BRANCH_SNAPSHOT_MESSAGE,
|
|
661
|
-
authorName,
|
|
662
|
-
authorEmail,
|
|
663
|
-
cache
|
|
664
|
-
});
|
|
665
|
-
} catch (e) {
|
|
666
|
-
if (!isNoChangesError(e))
|
|
667
|
-
throw e;
|
|
668
|
-
}
|
|
669
|
-
const fromOid = await git.resolveRef({ fs: gitFs, dir: repoDir, ref: "HEAD" }).catch(() => {
|
|
670
|
-
return;
|
|
671
|
-
});
|
|
672
|
-
const { oid: toOid, remote } = await resolveCopy(repoDir, branch);
|
|
673
|
-
try {
|
|
674
|
-
await git.checkout({
|
|
675
|
-
fs: gitFs,
|
|
676
|
-
dir: repoDir,
|
|
677
|
-
cache,
|
|
678
|
-
ref: branch,
|
|
679
|
-
force: false,
|
|
680
|
-
...remote ? { remote } : {}
|
|
681
|
-
});
|
|
682
|
-
} catch (e) {
|
|
683
|
-
if (isCheckoutConflictError(e)) {
|
|
684
|
-
throw new Error("Couldn't switch copies — some files changed on disk right as the switch " + "started. Try again.", { cause: e });
|
|
685
|
-
}
|
|
686
|
-
throw e;
|
|
687
|
-
}
|
|
688
|
-
const changedPaths = fromOid && fromOid !== toOid ? await diffCommitPaths(repoDir, fromOid, toOid, cache) : [];
|
|
689
|
-
return {
|
|
690
|
-
current: branch,
|
|
691
|
-
changedFiles: changedPaths.map((p) => path2.join(repoDir, p))
|
|
692
|
-
};
|
|
693
|
-
});
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
export { gitFs, errorLogData, resolveLogger, gitScopeFor, HISTORY_PAGE_LIMIT, withRepoLock, __repoLockQueueSizeForTests, resolveGitAuthor, listWorkdirChanges, stageChanges, hasPendingChanges, hasUncommittedChanges, snapshotWorkingTreeUnlocked, isNoChangesError, providerFor, gitDirFor, snapshotStagingMarkerPath, RESTORE_BACKUP_MESSAGE, AUTO_SNAPSHOT_MESSAGE, restoreVersionWithBackup, SWITCH_BRANCH_SNAPSHOT_MESSAGE, listLocalBranches, switchBranch };
|