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.
Files changed (2) hide show
  1. package/dist/cli.js +114 -16
  2. 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) => !wt.isBare && !wt.path.startsWith(dest + "/"));
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
- return dest;
158
- }
159
- for (const wt of pending) {
160
- const branch = wt.branch ?? `detached-${wt.head.slice(0, 8)}`;
161
- if (!existsSync2(wt.path)) {
162
- log(`warn: Skipping [${branch}] — path no longer exists: ${wt.path}`);
163
- continue;
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 < worktrees.length; i++) {
218
- await processLinkedWorktree(worktrees[i], dest, destBare);
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 worktrees) {
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) => !wt.isBare && !wt.path.startsWith(dest + "/"));
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) => !wt.isBare && !existsSync3(wt.path));
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-worktree-organize",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
4
4
  "description": "Convert any git repo into the canonical bare-hub worktree layout",
5
5
  "type": "module",
6
6
  "bin": {