git-worktree-organize 1.0.10 → 1.0.12
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/cli.js +120 -79
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
2
|
-
// @bun
|
|
1
|
+
#!/usr/bin/env node
|
|
3
2
|
|
|
4
3
|
// src/cli.ts
|
|
5
|
-
import { resolve as resolve3, join as join3, dirname as dirname2, basename as basename2 } from "path";
|
|
4
|
+
import { resolve as resolve3, join as join3, dirname as dirname2, basename as basename2 } from "node:path";
|
|
5
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
6
6
|
|
|
7
7
|
// src/run.ts
|
|
8
8
|
import { spawnSync } from "node:child_process";
|
|
@@ -24,28 +24,9 @@ function run(cmd, args, options) {
|
|
|
24
24
|
// src/detect.ts
|
|
25
25
|
import { existsSync, statSync, readFileSync } from "node:fs";
|
|
26
26
|
import { join, resolve, isAbsolute } from "node:path";
|
|
27
|
-
|
|
28
|
-
// src/run.ts
|
|
29
|
-
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
30
|
-
function run2(cmd, args, options) {
|
|
31
|
-
const result = spawnSync2(cmd, args, {
|
|
32
|
-
encoding: "utf8",
|
|
33
|
-
cwd: options?.cwd,
|
|
34
|
-
env: options?.env ?? process.env
|
|
35
|
-
});
|
|
36
|
-
if (result.status !== 0) {
|
|
37
|
-
const err = new Error(`${cmd} ${args.join(" ")} failed: ${result.stderr?.trim() || result.error?.message}`);
|
|
38
|
-
err.exitCode = result.status;
|
|
39
|
-
err.stderr = result.stderr;
|
|
40
|
-
throw err;
|
|
41
|
-
}
|
|
42
|
-
return { stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// src/detect.ts
|
|
46
27
|
function readCoreBare(gitdir) {
|
|
47
28
|
try {
|
|
48
|
-
const result =
|
|
29
|
+
const result = run("git", ["--git-dir", gitdir, "config", "--get", "core.bare"]);
|
|
49
30
|
return result.stdout.trim() === "true";
|
|
50
31
|
} catch {
|
|
51
32
|
return false;
|
|
@@ -127,7 +108,7 @@ function parsePorcelain(output) {
|
|
|
127
108
|
return worktrees;
|
|
128
109
|
}
|
|
129
110
|
async function listWorktrees(repoPath) {
|
|
130
|
-
const result =
|
|
111
|
+
const result = run("git", ["-C", repoPath, "worktree", "list", "--porcelain"]);
|
|
131
112
|
return parsePorcelain(result.stdout);
|
|
132
113
|
}
|
|
133
114
|
|
|
@@ -135,46 +116,9 @@ async function listWorktrees(repoPath) {
|
|
|
135
116
|
import { mkdirSync, writeFileSync, readFileSync as readFileSync2, existsSync as existsSync2, renameSync, statSync as statSync2 } from "node:fs";
|
|
136
117
|
import { join as join2, dirname, basename, resolve as resolve2 } from "node:path";
|
|
137
118
|
|
|
138
|
-
// src/worktrees.ts
|
|
139
|
-
function parsePorcelain2(output) {
|
|
140
|
-
const worktrees = [];
|
|
141
|
-
const blocks = output.trim().split(/\n\n+/);
|
|
142
|
-
for (const block of blocks) {
|
|
143
|
-
if (!block.trim())
|
|
144
|
-
continue;
|
|
145
|
-
const lines = block.trim().split(`
|
|
146
|
-
`);
|
|
147
|
-
let path = "";
|
|
148
|
-
let head = "";
|
|
149
|
-
let branch = null;
|
|
150
|
-
let isBare = false;
|
|
151
|
-
for (const line of lines) {
|
|
152
|
-
if (line.startsWith("worktree ")) {
|
|
153
|
-
path = line.slice("worktree ".length);
|
|
154
|
-
} else if (line.startsWith("HEAD ")) {
|
|
155
|
-
head = line.slice("HEAD ".length);
|
|
156
|
-
} else if (line.startsWith("branch ")) {
|
|
157
|
-
const ref = line.slice("branch ".length);
|
|
158
|
-
branch = ref.startsWith("refs/heads/") ? ref.slice("refs/heads/".length) : ref;
|
|
159
|
-
} else if (line === "detached") {
|
|
160
|
-
branch = null;
|
|
161
|
-
} else if (line === "bare") {
|
|
162
|
-
isBare = true;
|
|
163
|
-
branch = null;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
worktrees.push({ path, head, branch, isBare });
|
|
167
|
-
}
|
|
168
|
-
return worktrees;
|
|
169
|
-
}
|
|
170
|
-
async function listWorktrees2(repoPath) {
|
|
171
|
-
const result = run2("git", ["-C", repoPath, "worktree", "list", "--porcelain"]);
|
|
172
|
-
return parsePorcelain2(result.stdout);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
119
|
// src/git.ts
|
|
176
120
|
async function git(args, options) {
|
|
177
|
-
const result =
|
|
121
|
+
const result = run("git", args, {
|
|
178
122
|
cwd: options?.cwd,
|
|
179
123
|
env: options?.env ? { ...process.env, ...options.env } : undefined
|
|
180
124
|
});
|
|
@@ -193,13 +137,46 @@ async function moveDir(src, dest) {
|
|
|
193
137
|
if (statSync2(src).dev === statSync2(destForStat).dev) {
|
|
194
138
|
renameSync(src, dest);
|
|
195
139
|
} else {
|
|
196
|
-
|
|
197
|
-
|
|
140
|
+
run("cp", ["-a", src, dest]);
|
|
141
|
+
run("rm", ["-rf", src]);
|
|
198
142
|
}
|
|
199
143
|
}
|
|
200
144
|
function sanitizeBranch(branch) {
|
|
201
145
|
return branch.replace(/\//g, "-");
|
|
202
146
|
}
|
|
147
|
+
function resolveWorktreePath(worktreePath, dest, sourceParent) {
|
|
148
|
+
if (existsSync2(worktreePath))
|
|
149
|
+
return worktreePath;
|
|
150
|
+
if (worktreePath.startsWith(dest + "/")) {
|
|
151
|
+
const remapped = sourceParent + worktreePath.slice(dest.length);
|
|
152
|
+
if (existsSync2(remapped))
|
|
153
|
+
return remapped;
|
|
154
|
+
}
|
|
155
|
+
return worktreePath;
|
|
156
|
+
}
|
|
157
|
+
function isPartialMigration(dest) {
|
|
158
|
+
const gitFile = join2(dest, ".git");
|
|
159
|
+
return existsSync2(join2(dest, ".bare")) && existsSync2(gitFile) && statSync2(gitFile).isFile();
|
|
160
|
+
}
|
|
161
|
+
async function resumeMigrate(dest, log = console.log) {
|
|
162
|
+
const destBare = join2(dest, ".bare");
|
|
163
|
+
const hubWorktrees = await listWorktrees(dest);
|
|
164
|
+
const pending = hubWorktrees.filter((wt) => !wt.isBare && !wt.path.startsWith(dest + "/"));
|
|
165
|
+
if (pending.length === 0) {
|
|
166
|
+
log("Nothing to resume — all worktrees are already in place.");
|
|
167
|
+
return dest;
|
|
168
|
+
}
|
|
169
|
+
for (const wt of pending) {
|
|
170
|
+
const branch = wt.branch ?? `detached-${wt.head.slice(0, 8)}`;
|
|
171
|
+
if (!existsSync2(wt.path)) {
|
|
172
|
+
log(`warn: Skipping [${branch}] — path no longer exists: ${wt.path}`);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
log(`Moving [${branch}] → ${join2(dest, sanitizeBranch(branch))}`);
|
|
176
|
+
await processLinkedWorktree(wt, dest, destBare);
|
|
177
|
+
}
|
|
178
|
+
return dest;
|
|
179
|
+
}
|
|
203
180
|
async function migrate(config, options) {
|
|
204
181
|
const source = resolve2(options.source);
|
|
205
182
|
const dest = options.dest ? resolve2(options.dest) : join2(dirname(source), basename(source) + "-bare");
|
|
@@ -207,7 +184,7 @@ async function migrate(config, options) {
|
|
|
207
184
|
if (existsSync2(destBare)) {
|
|
208
185
|
throw new Error(`'${destBare}' already exists`);
|
|
209
186
|
}
|
|
210
|
-
const allWorktrees = await
|
|
187
|
+
const allWorktrees = await listWorktrees(source);
|
|
211
188
|
const worktrees = allWorktrees.filter((wt) => !wt.isBare);
|
|
212
189
|
const seen = new Map;
|
|
213
190
|
for (const wt of worktrees) {
|
|
@@ -219,17 +196,19 @@ async function migrate(config, options) {
|
|
|
219
196
|
seen.set(safe, branch);
|
|
220
197
|
}
|
|
221
198
|
mkdirSync(destBare, { recursive: true });
|
|
222
|
-
|
|
199
|
+
run("cp", ["-a", config.gitdir + "/.", destBare + "/"]);
|
|
223
200
|
await setGitConfig("core.bare", "true", { gitdir: destBare });
|
|
224
201
|
await setGitConfig("remote.origin.fetch", "+refs/heads/*:refs/remotes/origin/*", { gitdir: destBare });
|
|
225
202
|
writeFileSync(join2(dest, ".git"), `gitdir: ./.bare
|
|
226
203
|
`);
|
|
204
|
+
const sourceParent = dirname(source);
|
|
205
|
+
const worktreesResolved = worktrees.map((wt, i) => i === 0 && config.type === "standard" ? wt : { ...wt, path: resolveWorktreePath(wt.path, dest, sourceParent) });
|
|
227
206
|
if (config.type === "standard") {
|
|
228
207
|
const mainBranch = worktrees[0].branch;
|
|
229
208
|
const mainSafe = sanitizeBranch(mainBranch);
|
|
230
209
|
const mainDest = join2(dest, mainSafe);
|
|
231
210
|
const mainHeadContent = readFileSync2(join2(destBare, "HEAD"), "utf8");
|
|
232
|
-
|
|
211
|
+
run("rm", ["-rf", join2(source, ".git")]);
|
|
233
212
|
await moveDir(source, mainDest);
|
|
234
213
|
const mainAdminDir = join2(destBare, "worktrees", mainSafe);
|
|
235
214
|
mkdirSync(mainAdminDir, { recursive: true });
|
|
@@ -247,11 +226,11 @@ async function migrate(config, options) {
|
|
|
247
226
|
}
|
|
248
227
|
writeFileSync(join2(mainDest, ".git"), `gitdir: ${mainAdminDir}
|
|
249
228
|
`);
|
|
250
|
-
for (let i = 1;i <
|
|
251
|
-
await processLinkedWorktree(
|
|
229
|
+
for (let i = 1;i < worktreesResolved.length; i++) {
|
|
230
|
+
await processLinkedWorktree(worktreesResolved[i], dest, destBare);
|
|
252
231
|
}
|
|
253
232
|
} else {
|
|
254
|
-
for (const wt of
|
|
233
|
+
for (const wt of worktreesResolved) {
|
|
255
234
|
await processLinkedWorktree(wt, dest, destBare);
|
|
256
235
|
}
|
|
257
236
|
}
|
|
@@ -296,14 +275,20 @@ function yellow(s) {
|
|
|
296
275
|
function bold(s) {
|
|
297
276
|
return `${BOLD}${s}${RESET}`;
|
|
298
277
|
}
|
|
278
|
+
function prompt() {
|
|
279
|
+
return new Promise((res) => {
|
|
280
|
+
process.stdin.setEncoding("utf8");
|
|
281
|
+
process.stdin.once("data", (chunk) => res(chunk.toString().trim()));
|
|
282
|
+
});
|
|
283
|
+
}
|
|
299
284
|
function usage() {
|
|
300
285
|
console.log(`Usage: git-worktree-organize <source> [destination]
|
|
301
286
|
|
|
302
287
|
Convert a git repository into the canonical bare-hub worktree layout:
|
|
303
288
|
|
|
304
|
-
<dest>/.bare/
|
|
305
|
-
<dest>/.git
|
|
306
|
-
<dest>/<branch>/
|
|
289
|
+
<dest>/.bare/ ← bare git repo
|
|
290
|
+
<dest>/.git ← plain file: "gitdir: ./.bare"
|
|
291
|
+
<dest>/<branch>/ ← one directory per worktree
|
|
307
292
|
|
|
308
293
|
Arguments:
|
|
309
294
|
source Path to existing git repository
|
|
@@ -321,12 +306,71 @@ async function main() {
|
|
|
321
306
|
const sourcePath = args[0];
|
|
322
307
|
const destArg = args[1];
|
|
323
308
|
const source = resolve3(sourcePath);
|
|
324
|
-
const dest = destArg ? resolve3(destArg) : join3(dirname2(source), basename2(source) + "-bare");
|
|
309
|
+
const dest = isPartialMigration(source) ? source : destArg ? resolve3(destArg) : join3(dirname2(source), basename2(source) + "-bare");
|
|
310
|
+
if (isPartialMigration(dest)) {
|
|
311
|
+
const hubWorktrees = await listWorktrees(dest);
|
|
312
|
+
const pending = hubWorktrees.filter((wt) => !wt.isBare && !wt.path.startsWith(dest + "/"));
|
|
313
|
+
console.log(`
|
|
314
|
+
${yellow("warn:")} Partial migration detected at ${bold(dest)}`);
|
|
315
|
+
if (pending.length === 0) {
|
|
316
|
+
console.log("All worktrees are already in place — nothing to resume.");
|
|
317
|
+
process.exit(0);
|
|
318
|
+
}
|
|
319
|
+
console.log(`
|
|
320
|
+
Worktrees still to move:`);
|
|
321
|
+
for (const wt of pending) {
|
|
322
|
+
const branch = wt.branch ?? `detached-${wt.head.slice(0, 8)}`;
|
|
323
|
+
const exists = existsSync3(wt.path);
|
|
324
|
+
const status = exists ? "" : ` ${yellow("(path missing)")}`;
|
|
325
|
+
console.log(` [${bold(branch)}] ${wt.path} → ${join3(dest, sanitizeBranch(branch))}${status}`);
|
|
326
|
+
}
|
|
327
|
+
console.log();
|
|
328
|
+
process.stdout.write("Resume migration? [y/N] ");
|
|
329
|
+
const resumeAns = await prompt();
|
|
330
|
+
if (!/^[Yy]$/.test(resumeAns)) {
|
|
331
|
+
console.log("Aborted.");
|
|
332
|
+
process.exit(0);
|
|
333
|
+
}
|
|
334
|
+
console.log();
|
|
335
|
+
const hubPath2 = await resumeMigrate(dest, (msg) => console.log(`${green("==>")} ${msg}`));
|
|
336
|
+
console.log();
|
|
337
|
+
console.log(`${green("==>")} Verifying with git worktree list...`);
|
|
338
|
+
console.log(run("git", ["-C", hubPath2, "worktree", "list"]).stdout);
|
|
339
|
+
console.log(`Done! Hub: ${hubPath2}`);
|
|
340
|
+
process.exit(0);
|
|
341
|
+
}
|
|
325
342
|
console.log(`
|
|
326
343
|
${green("==>")} Reading worktrees from ${source}
|
|
327
344
|
`);
|
|
328
345
|
const config = await detect(source);
|
|
329
346
|
const allWorktrees = await listWorktrees(source);
|
|
347
|
+
const missing = allWorktrees.filter((wt) => {
|
|
348
|
+
if (wt.isBare)
|
|
349
|
+
return false;
|
|
350
|
+
const actual = resolveWorktreePath(wt.path, dest, dirname2(source));
|
|
351
|
+
return !existsSync3(actual);
|
|
352
|
+
});
|
|
353
|
+
if (missing.length > 0) {
|
|
354
|
+
console.log(`${yellow("warn:")} The following worktree paths no longer exist:`);
|
|
355
|
+
for (const wt of missing) {
|
|
356
|
+
const branch = wt.branch ?? `detached-${wt.head.slice(0, 8)}`;
|
|
357
|
+
console.log(` [${branch}] ${wt.path}`);
|
|
358
|
+
}
|
|
359
|
+
console.log();
|
|
360
|
+
process.stdout.write("Remove them with `git worktree prune` and continue? [y/N] ");
|
|
361
|
+
const pruneAns = await prompt();
|
|
362
|
+
if (!/^[Yy]$/.test(pruneAns)) {
|
|
363
|
+
console.log("Aborted.");
|
|
364
|
+
process.stdin.destroy();
|
|
365
|
+
process.exit(0);
|
|
366
|
+
}
|
|
367
|
+
run("git", ["-C", source, "worktree", "prune"]);
|
|
368
|
+
console.log(`${green("==>")} Pruned stale worktrees.
|
|
369
|
+
`);
|
|
370
|
+
const refreshed = await listWorktrees(source);
|
|
371
|
+
allWorktrees.length = 0;
|
|
372
|
+
allWorktrees.push(...refreshed);
|
|
373
|
+
}
|
|
330
374
|
const worktrees = allWorktrees.filter((wt) => !wt.isBare);
|
|
331
375
|
let mainBranch = null;
|
|
332
376
|
if (config.type === "standard" && worktrees.length > 0) {
|
|
@@ -346,16 +390,13 @@ ${green("==>")} Reading worktrees from ${source}
|
|
|
346
390
|
const tagPad = isMain ? ` (labeled ${yellow("[main]")})` : "";
|
|
347
391
|
const nameCol = bold(`[${branch}]`).padEnd(maxNameLen + 2 + BOLD.length + RESET.length);
|
|
348
392
|
const annotation = isMain ? ` (labeled ${yellow("[main]")})`.padEnd(18 + YELLOW.length + RESET.length) : "".padEnd(18);
|
|
349
|
-
console.log(` ${nameCol}${annotation}
|
|
393
|
+
console.log(` ${nameCol}${annotation} → ${destDir}`);
|
|
350
394
|
}
|
|
351
395
|
console.log();
|
|
352
396
|
console.log(`Hub destination: ${bold(dest)} (bare repo at ${dest}/.bare)`);
|
|
353
397
|
console.log();
|
|
354
398
|
process.stdout.write("Proceed? [y/N] ");
|
|
355
|
-
const ans = await
|
|
356
|
-
process.stdin.setEncoding("utf8");
|
|
357
|
-
process.stdin.once("data", (chunk) => resolve4(chunk.toString().trim()));
|
|
358
|
-
});
|
|
399
|
+
const ans = await prompt();
|
|
359
400
|
process.stdin.destroy();
|
|
360
401
|
if (!/^[Yy]$/.test(ans)) {
|
|
361
402
|
console.log("Aborted.");
|