git-worktree-organize 1.0.11 → 1.0.13
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 +114 -16
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -113,7 +113,7 @@ async function listWorktrees(repoPath) {
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
// src/migrate.ts
|
|
116
|
-
import { mkdirSync, writeFileSync, readFileSync as readFileSync2, existsSync as existsSync2, renameSync, statSync as statSync2 } from "node:fs";
|
|
116
|
+
import { mkdirSync, writeFileSync, readFileSync as readFileSync2, existsSync as existsSync2, renameSync, statSync as statSync2, readdirSync } from "node:fs";
|
|
117
117
|
import { join as join2, dirname, basename, resolve as resolve2 } from "node:path";
|
|
118
118
|
|
|
119
119
|
// src/git.ts
|
|
@@ -144,27 +144,88 @@ async function moveDir(src, dest) {
|
|
|
144
144
|
function sanitizeBranch(branch) {
|
|
145
145
|
return branch.replace(/\//g, "-");
|
|
146
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
|
+
}
|
|
147
157
|
function isPartialMigration(dest) {
|
|
148
158
|
const gitFile = join2(dest, ".git");
|
|
149
159
|
return existsSync2(join2(dest, ".bare")) && existsSync2(gitFile) && statSync2(gitFile).isFile();
|
|
150
160
|
}
|
|
161
|
+
function findHub(startPath) {
|
|
162
|
+
let current = resolve2(startPath);
|
|
163
|
+
while (true) {
|
|
164
|
+
if (isPartialMigration(current))
|
|
165
|
+
return current;
|
|
166
|
+
const parent = dirname(current);
|
|
167
|
+
if (parent === current)
|
|
168
|
+
return null;
|
|
169
|
+
current = parent;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async function repairHub(dest, log = console.log) {
|
|
173
|
+
const adminBase = join2(dest, ".bare", "worktrees");
|
|
174
|
+
if (!existsSync2(adminBase))
|
|
175
|
+
return;
|
|
176
|
+
for (const adminName of readdirSync(adminBase)) {
|
|
177
|
+
const adminDir = join2(adminBase, adminName);
|
|
178
|
+
if (!statSync2(adminDir).isDirectory())
|
|
179
|
+
continue;
|
|
180
|
+
const gitdirFile = join2(adminDir, "gitdir");
|
|
181
|
+
if (!existsSync2(gitdirFile))
|
|
182
|
+
continue;
|
|
183
|
+
const registeredGitFile = readFileSync2(gitdirFile, "utf8").trim();
|
|
184
|
+
const worktreePath = dirname(registeredGitFile);
|
|
185
|
+
if (!worktreePath.startsWith(dest + "/"))
|
|
186
|
+
continue;
|
|
187
|
+
if (!existsSync2(registeredGitFile) || !statSync2(registeredGitFile).isFile())
|
|
188
|
+
continue;
|
|
189
|
+
const content = readFileSync2(registeredGitFile, "utf8");
|
|
190
|
+
const match = content.match(/^gitdir:\s*(.+)/m);
|
|
191
|
+
if (!match)
|
|
192
|
+
continue;
|
|
193
|
+
if (match[1].trim() === adminDir)
|
|
194
|
+
continue;
|
|
195
|
+
log(`Repairing .git for [${basename(worktreePath)}]`);
|
|
196
|
+
writeFileSync(registeredGitFile, `gitdir: ${adminDir}
|
|
197
|
+
`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
151
200
|
async function resumeMigrate(dest, log = console.log) {
|
|
152
201
|
const destBare = join2(dest, ".bare");
|
|
153
202
|
const hubWorktrees = await listWorktrees(dest);
|
|
154
|
-
const pending = hubWorktrees.filter((wt) =>
|
|
203
|
+
const pending = hubWorktrees.filter((wt) => {
|
|
204
|
+
if (wt.isBare)
|
|
205
|
+
return false;
|
|
206
|
+
const branch = wt.branch ?? `detached-${wt.head.slice(0, 8)}`;
|
|
207
|
+
return wt.path !== join2(dest, sanitizeBranch(branch));
|
|
208
|
+
});
|
|
155
209
|
if (pending.length === 0) {
|
|
156
210
|
log("Nothing to resume — all worktrees are already in place.");
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
211
|
+
} else {
|
|
212
|
+
for (const wt of pending) {
|
|
213
|
+
const branch = wt.branch ?? `detached-${wt.head.slice(0, 8)}`;
|
|
214
|
+
const expectedPath = join2(dest, sanitizeBranch(branch));
|
|
215
|
+
let wtPath = wt.path;
|
|
216
|
+
if (!existsSync2(wtPath)) {
|
|
217
|
+
if (existsSync2(expectedPath)) {
|
|
218
|
+
wtPath = expectedPath;
|
|
219
|
+
} else {
|
|
220
|
+
log(`warn: Skipping [${branch}] — path no longer exists: ${wt.path}`);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
log(`Moving [${branch}] → ${expectedPath}`);
|
|
225
|
+
await processLinkedWorktree({ ...wt, path: wtPath }, dest, destBare);
|
|
164
226
|
}
|
|
165
|
-
log(`Moving [${branch}] → ${join2(dest, sanitizeBranch(branch))}`);
|
|
166
|
-
await processLinkedWorktree(wt, dest, destBare);
|
|
167
227
|
}
|
|
228
|
+
await repairHub(dest, log);
|
|
168
229
|
return dest;
|
|
169
230
|
}
|
|
170
231
|
async function migrate(config, options) {
|
|
@@ -191,6 +252,8 @@ async function migrate(config, options) {
|
|
|
191
252
|
await setGitConfig("remote.origin.fetch", "+refs/heads/*:refs/remotes/origin/*", { gitdir: destBare });
|
|
192
253
|
writeFileSync(join2(dest, ".git"), `gitdir: ./.bare
|
|
193
254
|
`);
|
|
255
|
+
const sourceParent = dirname(source);
|
|
256
|
+
const worktreesResolved = worktrees.map((wt, i) => i === 0 && config.type === "standard" ? wt : { ...wt, path: resolveWorktreePath(wt.path, dest, sourceParent) });
|
|
194
257
|
if (config.type === "standard") {
|
|
195
258
|
const mainBranch = worktrees[0].branch;
|
|
196
259
|
const mainSafe = sanitizeBranch(mainBranch);
|
|
@@ -214,11 +277,11 @@ async function migrate(config, options) {
|
|
|
214
277
|
}
|
|
215
278
|
writeFileSync(join2(mainDest, ".git"), `gitdir: ${mainAdminDir}
|
|
216
279
|
`);
|
|
217
|
-
for (let i = 1;i <
|
|
218
|
-
await processLinkedWorktree(
|
|
280
|
+
for (let i = 1;i < worktreesResolved.length; i++) {
|
|
281
|
+
await processLinkedWorktree(worktreesResolved[i], dest, destBare);
|
|
219
282
|
}
|
|
220
283
|
} else {
|
|
221
|
-
for (const wt of
|
|
284
|
+
for (const wt of worktreesResolved) {
|
|
222
285
|
await processLinkedWorktree(wt, dest, destBare);
|
|
223
286
|
}
|
|
224
287
|
}
|
|
@@ -295,9 +358,39 @@ async function main() {
|
|
|
295
358
|
const destArg = args[1];
|
|
296
359
|
const source = resolve3(sourcePath);
|
|
297
360
|
const dest = isPartialMigration(source) ? source : destArg ? resolve3(destArg) : join3(dirname2(source), basename2(source) + "-bare");
|
|
361
|
+
if (!isPartialMigration(source) && !destArg) {
|
|
362
|
+
const ancestorHub = findHub(dirname2(source));
|
|
363
|
+
if (ancestorHub) {
|
|
364
|
+
console.log(`
|
|
365
|
+
${yellow("warn:")} ${bold(source)} is inside an existing hub at ${bold(ancestorHub)}`);
|
|
366
|
+
console.log(`
|
|
367
|
+
This looks like manually-placed worktrees with stale .git files.`);
|
|
368
|
+
console.log(`Running repair will fix all worktree .git connections in the hub.
|
|
369
|
+
`);
|
|
370
|
+
process.stdout.write(`Repair hub at ${ancestorHub}? [y/N] `);
|
|
371
|
+
const repairAns = await prompt();
|
|
372
|
+
process.stdin.destroy();
|
|
373
|
+
if (!/^[Yy]$/.test(repairAns)) {
|
|
374
|
+
console.log("Aborted.");
|
|
375
|
+
process.exit(0);
|
|
376
|
+
}
|
|
377
|
+
console.log();
|
|
378
|
+
await repairHub(ancestorHub, (msg) => console.log(`${green("==>")} ${msg}`));
|
|
379
|
+
console.log();
|
|
380
|
+
console.log(`${green("==>")} Verifying with git worktree list...`);
|
|
381
|
+
console.log(run("git", ["-C", ancestorHub, "worktree", "list"]).stdout);
|
|
382
|
+
console.log(`Done! Hub: ${ancestorHub}`);
|
|
383
|
+
process.exit(0);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
298
386
|
if (isPartialMigration(dest)) {
|
|
299
387
|
const hubWorktrees = await listWorktrees(dest);
|
|
300
|
-
const pending = hubWorktrees.filter((wt) =>
|
|
388
|
+
const pending = hubWorktrees.filter((wt) => {
|
|
389
|
+
if (wt.isBare)
|
|
390
|
+
return false;
|
|
391
|
+
const branch = wt.branch ?? `detached-${wt.head.slice(0, 8)}`;
|
|
392
|
+
return wt.path !== join3(dest, sanitizeBranch(branch));
|
|
393
|
+
});
|
|
301
394
|
console.log(`
|
|
302
395
|
${yellow("warn:")} Partial migration detected at ${bold(dest)}`);
|
|
303
396
|
if (pending.length === 0) {
|
|
@@ -332,7 +425,12 @@ ${green("==>")} Reading worktrees from ${source}
|
|
|
332
425
|
`);
|
|
333
426
|
const config = await detect(source);
|
|
334
427
|
const allWorktrees = await listWorktrees(source);
|
|
335
|
-
const missing = allWorktrees.filter((wt) =>
|
|
428
|
+
const missing = allWorktrees.filter((wt) => {
|
|
429
|
+
if (wt.isBare)
|
|
430
|
+
return false;
|
|
431
|
+
const actual = resolveWorktreePath(wt.path, dest, dirname2(source));
|
|
432
|
+
return !existsSync3(actual);
|
|
433
|
+
});
|
|
336
434
|
if (missing.length > 0) {
|
|
337
435
|
console.log(`${yellow("warn:")} The following worktree paths no longer exist:`);
|
|
338
436
|
for (const wt of missing) {
|