silvermoon 0.0.1
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 +236 -0
- package/README.zh-CN.md +199 -0
- package/assets/silvermoon.svg +175 -0
- package/bin/silvermoon.js +5 -0
- package/package.json +65 -0
- package/schema/v1/config.schema.json +24 -0
- package/schema/v1/definitions.schema.json +25 -0
- package/schema/v1/idea-status.schema.json +41 -0
- package/skills/silvermoon/SKILL.md +209 -0
- package/skills/silvermoon/references/adoption.md +122 -0
- package/src/adoption.js +313 -0
- package/src/cli.js +197 -0
- package/src/config.js +176 -0
- package/src/create-idea.js +235 -0
- package/src/git.js +284 -0
- package/src/idea-layout.js +439 -0
- package/src/idea-templates.js +109 -0
- package/src/ideas.js +132 -0
- package/src/index.js +260 -0
- package/src/layout.js +21 -0
- package/src/repository.js +88 -0
- package/src/whatsnext.js +379 -0
- package/src/yaml.js +66 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { lstat, mkdir, readFile, rm, rmdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { loadConfig } from "./config.js";
|
|
6
|
+
import {
|
|
7
|
+
DEPLOYMENT_TEMPLATE,
|
|
8
|
+
IDEA_TEMPLATE,
|
|
9
|
+
IMPLEMENTATION_TEMPLATE,
|
|
10
|
+
LEDGER_TEMPLATE,
|
|
11
|
+
} from "./idea-templates.js";
|
|
12
|
+
import { isValidUlid, serializeIdeaStatus } from "./ideas.js";
|
|
13
|
+
import { IDEAS_ROOT, ideaPaths } from "./layout.js";
|
|
14
|
+
import { whatsNext } from "./whatsnext.js";
|
|
15
|
+
|
|
16
|
+
const ULID_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
17
|
+
const MAX_ID_ATTEMPTS = 32;
|
|
18
|
+
|
|
19
|
+
function encode(value, length) {
|
|
20
|
+
let encoded = "";
|
|
21
|
+
for (let index = 0; index < length; index += 1) {
|
|
22
|
+
encoded = ULID_ALPHABET[Number(value & 31n)] + encoded;
|
|
23
|
+
value >>= 5n;
|
|
24
|
+
}
|
|
25
|
+
return encoded;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function generateUlid({ now = Date.now(), random = randomBytes(10) } = {}) {
|
|
29
|
+
return encode(BigInt(now), 10) + encode(BigInt(`0x${Buffer.from(random).toString("hex")}`), 16);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function pathExists(path, inspect) {
|
|
33
|
+
try {
|
|
34
|
+
await inspect(path);
|
|
35
|
+
return true;
|
|
36
|
+
} catch (caught) {
|
|
37
|
+
if (caught.code === "ENOENT") return false;
|
|
38
|
+
throw caught;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function removeOwnedFile(path, expected, read, remove) {
|
|
43
|
+
let current;
|
|
44
|
+
try {
|
|
45
|
+
current = await read(path);
|
|
46
|
+
} catch (caught) {
|
|
47
|
+
if (caught.code === "ENOENT") return;
|
|
48
|
+
throw caught;
|
|
49
|
+
}
|
|
50
|
+
const expectedBytes = Buffer.from(expected);
|
|
51
|
+
if (
|
|
52
|
+
current.length <= expectedBytes.length &&
|
|
53
|
+
expectedBytes.subarray(0, current.length).equals(current)
|
|
54
|
+
) {
|
|
55
|
+
await remove(path, { force: true });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function removeCreatedDirectories(created, removeDirectory) {
|
|
60
|
+
for (const path of [...created].reverse()) {
|
|
61
|
+
try {
|
|
62
|
+
await removeDirectory(path);
|
|
63
|
+
} catch {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function ensureDirectoryPath(root, relativePath, inspect, makeDirectory, removeDirectory) {
|
|
70
|
+
const created = [];
|
|
71
|
+
let current = root;
|
|
72
|
+
try {
|
|
73
|
+
for (const segment of relativePath.split("/")) {
|
|
74
|
+
current = resolve(current, segment);
|
|
75
|
+
let metadata;
|
|
76
|
+
try {
|
|
77
|
+
metadata = await inspect(current);
|
|
78
|
+
} catch (caught) {
|
|
79
|
+
if (caught.code !== "ENOENT") throw caught;
|
|
80
|
+
}
|
|
81
|
+
if (metadata) {
|
|
82
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
|
|
83
|
+
throw new Error(`Silvermoon path segment is not a regular directory: ${current}`);
|
|
84
|
+
}
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
await makeDirectory(current);
|
|
89
|
+
created.push(current);
|
|
90
|
+
} catch (caught) {
|
|
91
|
+
if (caught.code !== "EEXIST") throw caught;
|
|
92
|
+
metadata = await inspect(current);
|
|
93
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink()) throw caught;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return created;
|
|
97
|
+
} catch (caught) {
|
|
98
|
+
await removeCreatedDirectories(created, removeDirectory);
|
|
99
|
+
throw caught;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function failure(root, preflight, caught) {
|
|
104
|
+
return {
|
|
105
|
+
command: "create-idea",
|
|
106
|
+
ok: false,
|
|
107
|
+
root,
|
|
108
|
+
diagnostics: [{
|
|
109
|
+
code: "idea.create.failed",
|
|
110
|
+
level: "error",
|
|
111
|
+
message: caught.message,
|
|
112
|
+
remediation: "Preserve existing paths, resolve the filesystem error, and retry.",
|
|
113
|
+
}],
|
|
114
|
+
result: {
|
|
115
|
+
observedPrimaryCommit: preflight.result?.observedPrimaryCommit ?? null,
|
|
116
|
+
request: { kind: "create-idea" },
|
|
117
|
+
selectedIdea: null,
|
|
118
|
+
action: null,
|
|
119
|
+
createdIdea: null,
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function createIdea({
|
|
125
|
+
generateId = generateUlid,
|
|
126
|
+
operations = {},
|
|
127
|
+
root = process.cwd(),
|
|
128
|
+
} = {}) {
|
|
129
|
+
const repositoryRoot = resolve(root);
|
|
130
|
+
const preflight = await whatsNext({ create: true, root: repositoryRoot });
|
|
131
|
+
const routed = { ...preflight, command: "create-idea" };
|
|
132
|
+
if (!preflight.ok || preflight.result.action?.code !== "create-idea") return routed;
|
|
133
|
+
|
|
134
|
+
const loaded = await loadConfig({ root: repositoryRoot });
|
|
135
|
+
if (!loaded.config) return { ...routed, ok: false, diagnostics: loaded.diagnostics };
|
|
136
|
+
|
|
137
|
+
const inspect = operations.lstat ?? lstat;
|
|
138
|
+
const makeDirectory = operations.mkdir ?? mkdir;
|
|
139
|
+
const read = operations.readFile ?? readFile;
|
|
140
|
+
const remove = operations.rm ?? rm;
|
|
141
|
+
const removeDirectory = operations.rmdir ?? rmdir;
|
|
142
|
+
const write = operations.writeFile ?? writeFile;
|
|
143
|
+
let rootDirectories;
|
|
144
|
+
try {
|
|
145
|
+
rootDirectories = await ensureDirectoryPath(
|
|
146
|
+
repositoryRoot,
|
|
147
|
+
IDEAS_ROOT,
|
|
148
|
+
inspect,
|
|
149
|
+
makeDirectory,
|
|
150
|
+
removeDirectory,
|
|
151
|
+
);
|
|
152
|
+
} catch (caught) {
|
|
153
|
+
return failure(repositoryRoot, preflight, caught);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const failRoot = async (caught) => {
|
|
157
|
+
await removeCreatedDirectories(rootDirectories, removeDirectory);
|
|
158
|
+
return failure(repositoryRoot, preflight, caught);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
for (let attempt = 0; attempt < MAX_ID_ATTEMPTS; attempt += 1) {
|
|
162
|
+
const id = await generateId();
|
|
163
|
+
if (!isValidUlid(id)) {
|
|
164
|
+
return failRoot(new Error(`Generated idea id is not a canonical ULID: ${id}`));
|
|
165
|
+
}
|
|
166
|
+
const paths = ideaPaths(id);
|
|
167
|
+
const folder = resolve(repositoryRoot, paths.ideaPath);
|
|
168
|
+
if (await pathExists(folder, inspect)) continue;
|
|
169
|
+
|
|
170
|
+
const directoryPaths = [
|
|
171
|
+
paths.ideaPath,
|
|
172
|
+
paths.outerPath,
|
|
173
|
+
paths.innerPath,
|
|
174
|
+
paths.idealPath,
|
|
175
|
+
];
|
|
176
|
+
const files = [
|
|
177
|
+
[paths.ideaDocumentPath, IDEA_TEMPLATE],
|
|
178
|
+
[paths.implementationDocumentPath, IMPLEMENTATION_TEMPLATE],
|
|
179
|
+
[paths.deploymentDocumentPath, DEPLOYMENT_TEMPLATE],
|
|
180
|
+
[paths.ledgerPath, LEDGER_TEMPLATE],
|
|
181
|
+
[paths.statusPath, serializeIdeaStatus({ version: 1, id })],
|
|
182
|
+
];
|
|
183
|
+
const createdDirectories = [];
|
|
184
|
+
const cleanupFiles = [];
|
|
185
|
+
try {
|
|
186
|
+
for (const relativePath of directoryPaths) {
|
|
187
|
+
const absolutePath = resolve(repositoryRoot, relativePath);
|
|
188
|
+
await makeDirectory(absolutePath);
|
|
189
|
+
createdDirectories.push(absolutePath);
|
|
190
|
+
}
|
|
191
|
+
for (const [relativePath, source] of files) {
|
|
192
|
+
const absolutePath = resolve(repositoryRoot, relativePath);
|
|
193
|
+
const cleanup = [absolutePath, source];
|
|
194
|
+
cleanupFiles.push(cleanup);
|
|
195
|
+
try {
|
|
196
|
+
await write(absolutePath, source, { flag: "wx" });
|
|
197
|
+
} catch (caught) {
|
|
198
|
+
if (caught.code === "EEXIST") cleanupFiles.pop();
|
|
199
|
+
throw caught;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
command: "create-idea",
|
|
204
|
+
ok: true,
|
|
205
|
+
root: repositoryRoot,
|
|
206
|
+
diagnostics: [],
|
|
207
|
+
result: {
|
|
208
|
+
observedPrimaryCommit: preflight.result.observedPrimaryCommit,
|
|
209
|
+
request: { kind: "create-idea" },
|
|
210
|
+
selectedIdea: null,
|
|
211
|
+
createdIdea: {
|
|
212
|
+
id,
|
|
213
|
+
ideaPath: paths.ideaPath,
|
|
214
|
+
statusPath: paths.statusPath,
|
|
215
|
+
ideaDocumentPath: paths.ideaDocumentPath,
|
|
216
|
+
implementationDocumentPath: paths.implementationDocumentPath,
|
|
217
|
+
deploymentDocumentPath: paths.deploymentDocumentPath,
|
|
218
|
+
ledgerPath: paths.ledgerPath,
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
} catch (caught) {
|
|
223
|
+
for (const [path, source] of [...cleanupFiles].reverse()) {
|
|
224
|
+
await removeOwnedFile(path, source, read, remove).catch(() => { });
|
|
225
|
+
}
|
|
226
|
+
await removeCreatedDirectories(createdDirectories, removeDirectory);
|
|
227
|
+
if (caught.code === "EEXIST") continue;
|
|
228
|
+
return failRoot(caught);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return failRoot(
|
|
233
|
+
new Error(`Could not allocate a unique idea id after ${MAX_ID_ATTEMPTS} attempts.`),
|
|
234
|
+
);
|
|
235
|
+
}
|
package/src/git.js
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { mkdir, mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
|
|
7
|
+
let commandObserver = null;
|
|
8
|
+
|
|
9
|
+
export function runGit(root, args, { env } = {}) {
|
|
10
|
+
if (commandObserver) commandObserver([...args]);
|
|
11
|
+
const result = spawnSync("git", ["-C", root, ...args], {
|
|
12
|
+
encoding: "utf8",
|
|
13
|
+
env,
|
|
14
|
+
windowsHide: true,
|
|
15
|
+
});
|
|
16
|
+
return {
|
|
17
|
+
error: result.error ?? null,
|
|
18
|
+
ok: result.status === 0,
|
|
19
|
+
status: result.status,
|
|
20
|
+
stderr: result.stderr?.trim() ?? "",
|
|
21
|
+
stdout: result.stdout?.trim() ?? "",
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function observeGitCommands(observer, callback) {
|
|
26
|
+
if (commandObserver) throw new Error("Git command observation is already active");
|
|
27
|
+
commandObserver = observer;
|
|
28
|
+
try {
|
|
29
|
+
return await callback();
|
|
30
|
+
} finally {
|
|
31
|
+
commandObserver = null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function sanitizeGitMessage(value) {
|
|
36
|
+
return String(value)
|
|
37
|
+
.replace(
|
|
38
|
+
/\b([a-z][a-z0-9+.-]*:\/\/)([^\s/@]+)@/gi,
|
|
39
|
+
"$1[redacted]@",
|
|
40
|
+
)
|
|
41
|
+
.replace(
|
|
42
|
+
/([?&](?:access_token|auth|credential|key|password|signature|token)=)[^&#\s]+/gi,
|
|
43
|
+
"$1[redacted]",
|
|
44
|
+
)
|
|
45
|
+
.replace(/\bgh[pousr]_[A-Za-z0-9_]{20,}\b/g, "[redacted]");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function requireGit(root, args, label, options) {
|
|
49
|
+
const result = runGit(root, args, options);
|
|
50
|
+
if (!result.ok) {
|
|
51
|
+
const safeResult = {
|
|
52
|
+
...result,
|
|
53
|
+
stderr: sanitizeGitMessage(result.stderr),
|
|
54
|
+
};
|
|
55
|
+
const error = new Error(`${label}: ${safeResult.stderr || result.error?.message || "Git failed"}`);
|
|
56
|
+
error.git = safeResult;
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
return result.stdout;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function gitObjectIdLength(root) {
|
|
63
|
+
const format = requireGit(
|
|
64
|
+
root,
|
|
65
|
+
["rev-parse", "--show-object-format"],
|
|
66
|
+
"Cannot determine Git object format",
|
|
67
|
+
);
|
|
68
|
+
if (format === "sha1") return 40;
|
|
69
|
+
if (format === "sha256") return 64;
|
|
70
|
+
throw new Error(`Unsupported Git object format: ${format}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function worktreePathTree(root, path) {
|
|
74
|
+
const directory = mkdtempSync(join(tmpdir(), "silvermoon-index-"));
|
|
75
|
+
const env = { ...process.env, GIT_INDEX_FILE: join(directory, "index") };
|
|
76
|
+
try {
|
|
77
|
+
const populated = runGit(root, ["read-tree", "HEAD"], { env });
|
|
78
|
+
if (!populated.ok) {
|
|
79
|
+
requireGit(root, ["read-tree", "--empty"], "Cannot initialize snapshot index", { env });
|
|
80
|
+
}
|
|
81
|
+
requireGit(
|
|
82
|
+
root,
|
|
83
|
+
["add", "--all", "--", path],
|
|
84
|
+
`Cannot snapshot ${path}`,
|
|
85
|
+
{ env },
|
|
86
|
+
);
|
|
87
|
+
const tree = requireGit(root, ["write-tree"], "Cannot write snapshot tree", { env });
|
|
88
|
+
const object = requireGit(
|
|
89
|
+
root,
|
|
90
|
+
["rev-parse", `${tree}:${path}`],
|
|
91
|
+
`Cannot resolve tree for ${path}`,
|
|
92
|
+
);
|
|
93
|
+
const type = requireGit(root, ["cat-file", "-t", object], `Cannot inspect ${path}`);
|
|
94
|
+
if (type !== "tree") throw new Error(`${path} does not resolve to a Git tree`);
|
|
95
|
+
return object;
|
|
96
|
+
} finally {
|
|
97
|
+
rmSync(directory, { recursive: true, force: true });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
export function worktreeSnapshot(root) {
|
|
101
|
+
const directory = mkdtempSync(join(tmpdir(), "silvermoon-index-"));
|
|
102
|
+
const env = { ...process.env, GIT_INDEX_FILE: join(directory, "index") };
|
|
103
|
+
try {
|
|
104
|
+
const populated = runGit(root, ["read-tree", "HEAD"], { env });
|
|
105
|
+
if (!populated.ok) {
|
|
106
|
+
requireGit(root, ["read-tree", "--empty"], "Cannot initialize snapshot index", { env });
|
|
107
|
+
}
|
|
108
|
+
requireGit(root, ["add", "--all"], "Cannot snapshot worktree", { env });
|
|
109
|
+
return {
|
|
110
|
+
tree: requireGit(root, ["write-tree"], "Cannot write worktree snapshot", { env }),
|
|
111
|
+
};
|
|
112
|
+
} finally {
|
|
113
|
+
rmSync(directory, { recursive: true, force: true });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function paths(stdout) {
|
|
118
|
+
return stdout.split(/\r?\n/).filter(Boolean);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const CHANGE_KINDS = {
|
|
122
|
+
A: "added",
|
|
123
|
+
C: "copied",
|
|
124
|
+
D: "deleted",
|
|
125
|
+
M: "modified",
|
|
126
|
+
R: "renamed",
|
|
127
|
+
T: "type-changed",
|
|
128
|
+
U: "unmerged",
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const CONFLICT_KINDS = {
|
|
132
|
+
AA: "both-added",
|
|
133
|
+
AU: "added-by-us",
|
|
134
|
+
DD: "both-deleted",
|
|
135
|
+
DU: "deleted-by-us",
|
|
136
|
+
UA: "added-by-them",
|
|
137
|
+
UD: "deleted-by-them",
|
|
138
|
+
UU: "both-modified",
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
function recordFields(record, count) {
|
|
142
|
+
const fields = [];
|
|
143
|
+
let offset = 2;
|
|
144
|
+
for (let index = 0; index < count; index += 1) {
|
|
145
|
+
const separator = record.indexOf(" ", offset);
|
|
146
|
+
if (separator < 0) throw new Error(`Malformed Git status record: ${record}`);
|
|
147
|
+
fields.push(record.slice(offset, separator));
|
|
148
|
+
offset = separator + 1;
|
|
149
|
+
}
|
|
150
|
+
return { fields, path: record.slice(offset) };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function changeEntry(path, code, originalPath) {
|
|
154
|
+
const entry = { path, kind: CHANGE_KINDS[code] ?? "unknown" };
|
|
155
|
+
if (originalPath !== undefined) entry.originalPath = originalPath;
|
|
156
|
+
return entry;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function sortChanges(changes) {
|
|
160
|
+
changes.sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function parseWorktreeChanges(source) {
|
|
164
|
+
const result = { staged: [], unstaged: [], untracked: [], conflicted: [] };
|
|
165
|
+
const records = source.split("\0");
|
|
166
|
+
for (let index = 0; index < records.length; index += 1) {
|
|
167
|
+
const record = records[index];
|
|
168
|
+
if (!record) continue;
|
|
169
|
+
if (record.startsWith("? ")) {
|
|
170
|
+
result.untracked.push({ path: record.slice(2) });
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (record.startsWith("! ") || record.startsWith("# ")) continue;
|
|
174
|
+
if (record.startsWith("u ")) {
|
|
175
|
+
const { fields, path } = recordFields(record, 9);
|
|
176
|
+
result.conflicted.push({ path, kind: CONFLICT_KINDS[fields[0]] ?? "unmerged" });
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (record.startsWith("1 ") || record.startsWith("2 ")) {
|
|
180
|
+
const renamed = record.startsWith("2 ");
|
|
181
|
+
const { fields, path } = recordFields(record, renamed ? 8 : 7);
|
|
182
|
+
const [indexCode, worktreeCode] = fields[0];
|
|
183
|
+
const originalPath = renamed ? records[++index] : undefined;
|
|
184
|
+
if (indexCode !== ".") {
|
|
185
|
+
result.staged.push(changeEntry(path, indexCode, originalPath));
|
|
186
|
+
}
|
|
187
|
+
if (worktreeCode !== ".") {
|
|
188
|
+
result.unstaged.push(changeEntry(path, worktreeCode));
|
|
189
|
+
}
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
throw new Error(`Unsupported Git status record: ${record}`);
|
|
193
|
+
}
|
|
194
|
+
for (const changes of Object.values(result)) sortChanges(changes);
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function resolveCommit(root, revision) {
|
|
199
|
+
return requireGit(
|
|
200
|
+
root,
|
|
201
|
+
["rev-parse", "--verify", "--end-of-options", `${revision}^{commit}`],
|
|
202
|
+
`Cannot resolve commit ${revision}`,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function commitChangedPaths(root, commit) {
|
|
207
|
+
const parent = runGit(root, ["rev-parse", "--verify", `${commit}^1`]);
|
|
208
|
+
const changed = parent.ok
|
|
209
|
+
? requireGit(root, ["diff", "--name-only", parent.stdout, commit, "--"], `Cannot inspect commit ${commit}`)
|
|
210
|
+
: requireGit(
|
|
211
|
+
root,
|
|
212
|
+
["diff-tree", "--root", "--no-commit-id", "--name-only", "-r", commit, "--"],
|
|
213
|
+
`Cannot inspect root commit ${commit}`,
|
|
214
|
+
);
|
|
215
|
+
return paths(changed);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function indexSnapshot(root) {
|
|
219
|
+
return {
|
|
220
|
+
paths: paths(requireGit(root, ["diff", "--cached", "--name-only", "--"], "Cannot inspect staged changes")),
|
|
221
|
+
tree: requireGit(root, ["write-tree"], "Cannot snapshot the index"),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function fetchRepositoryBranch(root, repository, branch) {
|
|
226
|
+
const remoteRef = `refs/heads/${branch}`;
|
|
227
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
228
|
+
const advertised = requireGit(
|
|
229
|
+
root,
|
|
230
|
+
["ls-remote", "--refs", repository, remoteRef],
|
|
231
|
+
`Cannot inspect ${branch}`,
|
|
232
|
+
);
|
|
233
|
+
const [primary, advertisedRef] = advertised.split(/\s+/);
|
|
234
|
+
if (!primary || advertisedRef !== remoteRef) {
|
|
235
|
+
throw new Error(`Cannot resolve remote branch ${branch}`);
|
|
236
|
+
}
|
|
237
|
+
requireGit(
|
|
238
|
+
root,
|
|
239
|
+
[
|
|
240
|
+
"fetch",
|
|
241
|
+
"--no-tags",
|
|
242
|
+
"--no-write-fetch-head",
|
|
243
|
+
repository,
|
|
244
|
+
remoteRef,
|
|
245
|
+
],
|
|
246
|
+
`Cannot fetch ${branch}`,
|
|
247
|
+
);
|
|
248
|
+
if (runGit(root, ["cat-file", "-e", `${primary}^{commit}`]).ok) return primary;
|
|
249
|
+
}
|
|
250
|
+
throw new Error(`Remote branch ${branch} moved repeatedly while fetching`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function fetchPrimary(root, config) {
|
|
254
|
+
return fetchRepositoryBranch(
|
|
255
|
+
root,
|
|
256
|
+
config.primaryRepository,
|
|
257
|
+
config.primaryBranch,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export async function withTemporaryWorktree(root, commit, callback) {
|
|
262
|
+
const tree = requireGit(root, ["rev-parse", `${commit}^{tree}`], `Cannot resolve tree for ${commit}`);
|
|
263
|
+
return withTemporaryTree(root, tree, callback);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export async function withTemporaryTree(root, tree, callback) {
|
|
267
|
+
const temporaryRoot = await mkdtemp(join(tmpdir(), "silvermoon-tree-"));
|
|
268
|
+
const directory = join(temporaryRoot, "snapshot");
|
|
269
|
+
const env = { ...process.env, GIT_INDEX_FILE: join(temporaryRoot, "index") };
|
|
270
|
+
try {
|
|
271
|
+
await mkdir(directory);
|
|
272
|
+
requireGit(root, ["read-tree", tree], "Cannot populate isolated index", { env });
|
|
273
|
+
const prefix = `${directory.replaceAll("\\", "/")}/`;
|
|
274
|
+
requireGit(
|
|
275
|
+
root,
|
|
276
|
+
["checkout-index", "--all", "--force", `--prefix=${prefix}`],
|
|
277
|
+
"Cannot populate isolated tree snapshot",
|
|
278
|
+
{ env },
|
|
279
|
+
);
|
|
280
|
+
return await callback(directory, tree);
|
|
281
|
+
} finally {
|
|
282
|
+
await rm(temporaryRoot, { recursive: true, force: true });
|
|
283
|
+
}
|
|
284
|
+
}
|