@clipboard-health/groundcrew 4.3.4 → 4.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/worktrees.d.ts.map +1 -1
- package/dist/lib/worktrees.js +71 -22
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worktrees.d.ts","sourceRoot":"","sources":["../../src/lib/worktrees.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAOH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAGlD,OAAO,EAAE,KAAK,cAAc,EAAc,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"worktrees.d.ts","sourceRoot":"","sources":["../../src/lib/worktrees.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAOH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAGlD,OAAO,EAAE,KAAK,cAAc,EAAc,MAAM,iBAAiB,CAAC;AAKlE,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC;AAElC,qBAAa,0BAA2B,SAAQ,KAAK;IACnD,SAAgB,GAAG,EAAE,MAAM,CAAC;IAE5B,YAAmB,GAAG,EAAE,MAAM,EAI7B;CACF;AAED,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,0BAA0B,CAEhG;AAED,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,sDAAsD;IACtD,MAAM,EAAE,MAAM,CAAC;IACf,+CAA+C;IAC/C,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,YAAY,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;CAChB;AAaD,iBAAS,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEnD;AA8PD,MAAM,MAAM,iBAAiB,GACzB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACtD;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GACjB;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC;AAiHxB,iBAAS,IAAI,CAAC,MAAM,EAAE,cAAc,GAAG,aAAa,EAAE,CAErD;AAED,iBAAS,YAAY,CAAC,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,GAAG,aAAa,EAAE,CAE7E;AAED,iBAAe,MAAM,CACnB,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,YAAY,EAClB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,aAAa,CAAC,CAQxB;AAED,iBAAe,MAAM,CACnB,MAAM,EAAE,cAAc,EACtB,KAAK,EAAE,aAAa,EACpB,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,GAClD,OAAO,CAAC,IAAI,CAAC,CAKf;AAED,MAAM,MAAM,YAAY,GAAG,iBAAiB,GAAG,iBAAiB,CAAC;AAEjE,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,aAAa,CAAC;IACrB,IAAI,EAAE,YAAY,CAAC;IACnB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,+DAA+D;IAC/D,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,sCAAsC;IACtC,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,wDAAwD;IACxD,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,cAAc,EAAE,cAAc,CAAC;CAChC;AAyBD,iBAAe,QAAQ,CACrB,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,SAAS,aAAa,EAAE,EACjC,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,GAClD,OAAO,CAAC,cAAc,CAAC,CAiDzB;AAED,iBAAe,gBAAgB,CAAC,KAAK,EAAE;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAE7B;AAED,eAAO,MAAM,SAAS;;;;;;;;CAQrB,CAAC"}
|
package/dist/lib/worktrees.js
CHANGED
|
@@ -7,14 +7,15 @@
|
|
|
7
7
|
* (workspace-close + worktree-remove paired) so callers don't reach into
|
|
8
8
|
* git directly.
|
|
9
9
|
*/
|
|
10
|
-
import { existsSync, readdirSync } from "node:fs";
|
|
10
|
+
import { existsSync, readdirSync, rmSync } from "node:fs";
|
|
11
11
|
import { userInfo } from "node:os";
|
|
12
|
-
import { resolve } from "node:path";
|
|
12
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
13
13
|
import { runCommandAsync } from "./commandRunner.js";
|
|
14
14
|
import { resolveDefaultBranch } from "./defaultBranch.js";
|
|
15
15
|
import { errorMessage, log } from "./util.js";
|
|
16
16
|
import { workspaces } from "./workspaces.js";
|
|
17
17
|
const LONG_RUNNING_COMMAND_OPTIONS = { stdio: "inherit", timeoutMs: 0 };
|
|
18
|
+
const WORKTREE_LIST_PREFIX = "worktree ";
|
|
18
19
|
export class WorktreeAlreadyExistsError extends Error {
|
|
19
20
|
dir;
|
|
20
21
|
constructor(dir) {
|
|
@@ -186,26 +187,46 @@ async function removeWorktree(config, entry, options) {
|
|
|
186
187
|
// ourselves so the failure message names the condition — dirty
|
|
187
188
|
// (modified/untracked files, fixable with `crew cleanup --force`) or
|
|
188
189
|
// orphan (directory exists on disk but is not registered with the
|
|
189
|
-
// parent repo,
|
|
190
|
-
|
|
190
|
+
// parent repo, fixable with `crew cleanup --force` when the path still
|
|
191
|
+
// matches groundcrew's expected worktree location).
|
|
192
|
+
if (options.signal?.aborted === true) {
|
|
191
193
|
throw error;
|
|
192
194
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
195
|
+
if (options.force) {
|
|
196
|
+
const registration = await probeWorktreeRegistration({
|
|
197
|
+
repoDir,
|
|
198
|
+
worktreeDir: entry.dir,
|
|
199
|
+
...signalProperty(options.signal),
|
|
200
|
+
});
|
|
201
|
+
if (registration !== "orphan") {
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
removeOrphanWorktreeDirectory(config, entry);
|
|
201
205
|
}
|
|
202
|
-
|
|
203
|
-
const
|
|
204
|
-
if (
|
|
205
|
-
throw new Error(
|
|
206
|
+
else {
|
|
207
|
+
const dirtiness = await probeWorktreeDirtiness(entry.dir, options.signal);
|
|
208
|
+
if (dirtiness.kind === "dirty") {
|
|
209
|
+
throw new Error(describeDirtyWorktree({
|
|
210
|
+
ticket: entry.ticket,
|
|
211
|
+
dir: entry.dir,
|
|
212
|
+
modified: dirtiness.modified,
|
|
213
|
+
untracked: dirtiness.untracked,
|
|
214
|
+
}), { cause: error });
|
|
206
215
|
}
|
|
216
|
+
if (dirtiness.kind === "unknown") {
|
|
217
|
+
const registration = await probeWorktreeRegistration({
|
|
218
|
+
repoDir,
|
|
219
|
+
worktreeDir: entry.dir,
|
|
220
|
+
...signalProperty(options.signal),
|
|
221
|
+
});
|
|
222
|
+
if (registration === "orphan") {
|
|
223
|
+
throw new Error(describeOrphanWorktree({ ticket: entry.ticket, dir: entry.dir }), {
|
|
224
|
+
cause: error,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
throw error;
|
|
207
229
|
}
|
|
208
|
-
throw error;
|
|
209
230
|
}
|
|
210
231
|
}
|
|
211
232
|
else {
|
|
@@ -258,19 +279,47 @@ function describeDirtyWorktree(arguments_) {
|
|
|
258
279
|
const pronoun = modified + untracked === 1 ? "it" : "them";
|
|
259
280
|
return `worktree has ${summary}. Run \`crew cleanup --force ${ticket}\` to discard ${pronoun}, or commit/stash in ${dir} first.`;
|
|
260
281
|
}
|
|
261
|
-
async function probeWorktreeRegistration(
|
|
282
|
+
async function probeWorktreeRegistration(arguments_) {
|
|
262
283
|
let output;
|
|
263
284
|
try {
|
|
264
|
-
output = await runCommandAsync("git", ["-C",
|
|
285
|
+
output = await runCommandAsync("git", ["-C", arguments_.repoDir, "worktree", "list", "--porcelain"], signalProperty(arguments_.signal));
|
|
265
286
|
}
|
|
266
287
|
catch {
|
|
267
288
|
return "unknown";
|
|
268
289
|
}
|
|
269
|
-
|
|
290
|
+
const resolvedWorktreeDir = resolve(arguments_.worktreeDir);
|
|
291
|
+
for (const line of output.split("\n")) {
|
|
292
|
+
if (!line.startsWith(WORKTREE_LIST_PREFIX)) {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (resolve(line.slice(WORKTREE_LIST_PREFIX.length)) === resolvedWorktreeDir) {
|
|
296
|
+
return "registered";
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return "orphan";
|
|
270
300
|
}
|
|
271
301
|
function describeOrphanWorktree(arguments_) {
|
|
272
|
-
const { dir } = arguments_;
|
|
273
|
-
return `directory exists but is not a registered git worktree.
|
|
302
|
+
const { ticket, dir } = arguments_;
|
|
303
|
+
return `directory exists but is not a registered git worktree. Run \`crew cleanup --force ${ticket}\` to remove ${dir}, or inspect it first if it may contain valuable files.`;
|
|
304
|
+
}
|
|
305
|
+
function expectedHostWorktreeDir(config, entry) {
|
|
306
|
+
return resolve(config.workspace.projectDir, `${entry.repository}-${entry.ticket}`);
|
|
307
|
+
}
|
|
308
|
+
function isInsideDirectory(parentDir, childDir) {
|
|
309
|
+
const childRelativePath = relative(parentDir, childDir);
|
|
310
|
+
return (childRelativePath.length > 0 &&
|
|
311
|
+
!childRelativePath.startsWith("..") &&
|
|
312
|
+
!isAbsolute(childRelativePath));
|
|
313
|
+
}
|
|
314
|
+
function removeOrphanWorktreeDirectory(config, entry) {
|
|
315
|
+
const projectDir = resolve(config.workspace.projectDir);
|
|
316
|
+
const expectedDir = expectedHostWorktreeDir(config, entry);
|
|
317
|
+
const targetDir = resolve(entry.dir);
|
|
318
|
+
if (targetDir !== expectedDir || !isInsideDirectory(projectDir, targetDir)) {
|
|
319
|
+
throw new Error(`Refusing to force-delete ${entry.dir}: expected groundcrew worktree path ${expectedDir}.`);
|
|
320
|
+
}
|
|
321
|
+
log(`Removing orphaned worktree directory ${entry.dir} (--force)...`);
|
|
322
|
+
rmSync(targetDir, { recursive: true, force: true });
|
|
274
323
|
}
|
|
275
324
|
function list(config) {
|
|
276
325
|
return listWorktrees(config);
|
package/package.json
CHANGED