vde-worktree 0.0.10 → 0.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/index.mjs CHANGED
@@ -601,6 +601,143 @@ const readNumberFromEnvOrDefault = ({ rawValue, defaultValue }) => {
601
601
  return rawValue;
602
602
  };
603
603
 
604
+ //#endregion
605
+ //#region src/core/worktree-merge-lifecycle.ts
606
+ const lifecycleFilePath = (repoRoot, branch) => {
607
+ return join(getStateDirectoryPath(repoRoot), "branches", `${branchToWorktreeId(branch)}.json`);
608
+ };
609
+ const hasStateDirectory = async (repoRoot) => {
610
+ try {
611
+ await access(getStateDirectoryPath(repoRoot), constants.F_OK);
612
+ return true;
613
+ } catch {
614
+ return false;
615
+ }
616
+ };
617
+ const parseLifecycle = (content) => {
618
+ try {
619
+ const parsed = JSON.parse(content);
620
+ if (parsed.schemaVersion !== 1 || typeof parsed.branch !== "string" || typeof parsed.worktreeId !== "string" || typeof parsed.baseBranch !== "string" || typeof parsed.createdHead !== "string" || parsed.createdHead.length === 0 || typeof parsed.createdAt !== "string" || typeof parsed.updatedAt !== "string") return {
621
+ valid: false,
622
+ record: null
623
+ };
624
+ return {
625
+ valid: true,
626
+ record: parsed
627
+ };
628
+ } catch {
629
+ return {
630
+ valid: false,
631
+ record: null
632
+ };
633
+ }
634
+ };
635
+ const writeJsonAtomically$1 = async ({ filePath, payload }) => {
636
+ await mkdir(dirname(filePath), { recursive: true });
637
+ const tmpPath = `${filePath}.tmp-${String(process.pid)}-${String(Date.now())}`;
638
+ await writeFile(tmpPath, `${JSON.stringify(payload)}\n`, "utf8");
639
+ await rename(tmpPath, filePath);
640
+ };
641
+ const readWorktreeMergeLifecycle = async ({ repoRoot, branch }) => {
642
+ const path = lifecycleFilePath(repoRoot, branch);
643
+ try {
644
+ await access(path, constants.F_OK);
645
+ } catch {
646
+ return {
647
+ path,
648
+ exists: false,
649
+ valid: true,
650
+ record: null
651
+ };
652
+ }
653
+ try {
654
+ return {
655
+ path,
656
+ exists: true,
657
+ ...parseLifecycle(await readFile(path, "utf8"))
658
+ };
659
+ } catch {
660
+ return {
661
+ path,
662
+ exists: true,
663
+ valid: false,
664
+ record: null
665
+ };
666
+ }
667
+ };
668
+ const upsertWorktreeMergeLifecycle = async ({ repoRoot, branch, baseBranch, createdHead }) => {
669
+ if (await hasStateDirectory(repoRoot) !== true) {
670
+ const now = (/* @__PURE__ */ new Date()).toISOString();
671
+ return {
672
+ schemaVersion: 1,
673
+ branch,
674
+ worktreeId: branchToWorktreeId(branch),
675
+ baseBranch,
676
+ createdHead,
677
+ createdAt: now,
678
+ updatedAt: now
679
+ };
680
+ }
681
+ const current = await readWorktreeMergeLifecycle({
682
+ repoRoot,
683
+ branch
684
+ });
685
+ if (current.valid && current.record !== null && current.record.baseBranch === baseBranch) return current.record;
686
+ const now = (/* @__PURE__ */ new Date()).toISOString();
687
+ const next = {
688
+ schemaVersion: 1,
689
+ branch,
690
+ worktreeId: branchToWorktreeId(branch),
691
+ baseBranch,
692
+ createdHead: current.record?.createdHead ?? createdHead,
693
+ createdAt: current.record?.createdAt ?? now,
694
+ updatedAt: now
695
+ };
696
+ await writeJsonAtomically$1({
697
+ filePath: current.path,
698
+ payload: next
699
+ });
700
+ return next;
701
+ };
702
+ const moveWorktreeMergeLifecycle = async ({ repoRoot, fromBranch, toBranch, baseBranch, createdHead }) => {
703
+ if (await hasStateDirectory(repoRoot) !== true) {
704
+ const now = (/* @__PURE__ */ new Date()).toISOString();
705
+ return {
706
+ schemaVersion: 1,
707
+ branch: toBranch,
708
+ worktreeId: branchToWorktreeId(toBranch),
709
+ baseBranch,
710
+ createdHead,
711
+ createdAt: now,
712
+ updatedAt: now
713
+ };
714
+ }
715
+ const source = await readWorktreeMergeLifecycle({
716
+ repoRoot,
717
+ branch: fromBranch
718
+ });
719
+ const targetPath = lifecycleFilePath(repoRoot, toBranch);
720
+ const now = (/* @__PURE__ */ new Date()).toISOString();
721
+ const next = {
722
+ schemaVersion: 1,
723
+ branch: toBranch,
724
+ worktreeId: branchToWorktreeId(toBranch),
725
+ baseBranch,
726
+ createdHead: source.record?.createdHead ?? createdHead,
727
+ createdAt: source.record?.createdAt ?? now,
728
+ updatedAt: now
729
+ };
730
+ await writeJsonAtomically$1({
731
+ filePath: targetPath,
732
+ payload: next
733
+ });
734
+ if (source.path !== targetPath) await rm(source.path, { force: true });
735
+ return next;
736
+ };
737
+ const deleteWorktreeMergeLifecycle = async ({ repoRoot, branch }) => {
738
+ await rm(lifecycleFilePath(repoRoot, branch), { force: true });
739
+ };
740
+
604
741
  //#endregion
605
742
  //#region src/core/worktree-lock.ts
606
743
  const parseLock = (content) => {
@@ -707,8 +844,9 @@ const parseMergedResult = (raw) => {
707
844
  return null;
708
845
  }
709
846
  };
710
- const resolveMergedByPr = async ({ repoRoot, branch, enabled = true, runGh = defaultRunGh }) => {
847
+ const resolveMergedByPr = async ({ repoRoot, branch, baseBranch, enabled = true, runGh = defaultRunGh }) => {
711
848
  if (enabled !== true) return null;
849
+ if (baseBranch === null) return null;
712
850
  try {
713
851
  const result = await runGh({
714
852
  cwd: repoRoot,
@@ -719,6 +857,8 @@ const resolveMergedByPr = async ({ repoRoot, branch, enabled = true, runGh = def
719
857
  "merged",
720
858
  "--head",
721
859
  branch,
860
+ "--base",
861
+ baseBranch,
722
862
  "--limit",
723
863
  "1",
724
864
  "--json",
@@ -878,7 +1018,7 @@ const resolveLockState = async ({ repoRoot, branch }) => {
878
1018
  };
879
1019
  }
880
1020
  };
881
- const resolveMergedState = async ({ repoRoot, branch, baseBranch, enableGh }) => {
1021
+ const resolveMergedState = async ({ repoRoot, branch, head, baseBranch, enableGh }) => {
882
1022
  if (branch === null) return {
883
1023
  byAncestry: null,
884
1024
  byPR: null,
@@ -902,21 +1042,35 @@ const resolveMergedState = async ({ repoRoot, branch, baseBranch, enableGh }) =>
902
1042
  const byPR = await resolveMergedByPr({
903
1043
  repoRoot,
904
1044
  branch,
1045
+ baseBranch,
905
1046
  enabled: enableGh
906
1047
  });
1048
+ let byLifecycle = null;
1049
+ if (baseBranch !== null) {
1050
+ const lifecycle = await upsertWorktreeMergeLifecycle({
1051
+ repoRoot,
1052
+ branch,
1053
+ baseBranch,
1054
+ createdHead: head
1055
+ });
1056
+ if (byAncestry === true) byLifecycle = lifecycle.createdHead !== head;
1057
+ else if (byAncestry === false) byLifecycle = false;
1058
+ }
907
1059
  return {
908
1060
  byAncestry,
909
1061
  byPR,
910
1062
  overall: resolveMergedOverall({
911
1063
  byAncestry,
912
- byPR
1064
+ byPR,
1065
+ byLifecycle
913
1066
  })
914
1067
  };
915
1068
  };
916
- const resolveMergedOverall = ({ byAncestry, byPR }) => {
917
- if (byPR === true) return true;
918
- if (byPR === false) return false;
919
- return byAncestry;
1069
+ const resolveMergedOverall = ({ byAncestry, byPR, byLifecycle }) => {
1070
+ if (byPR === true || byLifecycle === true) return true;
1071
+ if (byAncestry === false) return false;
1072
+ if (byPR === false || byLifecycle === false) return false;
1073
+ return null;
920
1074
  };
921
1075
  const resolveUpstreamState = async (worktreePath) => {
922
1076
  const upstreamRef = await runGitCommand({
@@ -968,6 +1122,7 @@ const enrichWorktree = async ({ repoRoot, worktree, baseBranch, enableGh }) => {
968
1122
  resolveMergedState({
969
1123
  repoRoot,
970
1124
  branch: worktree.branch,
1125
+ head: worktree.head,
971
1126
  baseBranch,
972
1127
  enableGh
973
1128
  }),
@@ -1690,6 +1845,22 @@ const resolveBaseBranch = async (repoRoot) => {
1690
1845
  for (const candidate of ["main", "master"]) if (await doesGitRefExist(repoRoot, `refs/heads/${candidate}`)) return candidate;
1691
1846
  throw createCliError("INVALID_ARGUMENT", { message: "Unable to resolve base branch. Configure vde-worktree.baseBranch." });
1692
1847
  };
1848
+ const resolveBranchHead = async ({ repoRoot, branch }) => {
1849
+ const resolved = await runGitCommand({
1850
+ cwd: repoRoot,
1851
+ args: [
1852
+ "rev-parse",
1853
+ "--verify",
1854
+ branch
1855
+ ],
1856
+ reject: false
1857
+ });
1858
+ if (resolved.exitCode !== 0 || resolved.stdout.trim().length === 0) throw createCliError("INVALID_ARGUMENT", {
1859
+ message: `Failed to resolve branch head: ${branch}`,
1860
+ details: { branch }
1861
+ });
1862
+ return resolved.stdout.trim();
1863
+ };
1693
1864
  const ensureTargetPathWritable = async (targetPath) => {
1694
1865
  try {
1695
1866
  await access(targetPath, constants.F_OK);
@@ -2818,6 +2989,15 @@ const createCli = (options = {}) => {
2818
2989
  baseBranch
2819
2990
  ]
2820
2991
  });
2992
+ await upsertWorktreeMergeLifecycle({
2993
+ repoRoot,
2994
+ branch,
2995
+ baseBranch,
2996
+ createdHead: await resolveBranchHead({
2997
+ repoRoot,
2998
+ branch
2999
+ })
3000
+ });
2821
3001
  await runPostHook({
2822
3002
  name: "new",
2823
3003
  context: hookContext
@@ -2848,12 +3028,27 @@ const createCli = (options = {}) => {
2848
3028
  });
2849
3029
  const branch = commandArgs[0];
2850
3030
  const result = await runWriteOperation(async () => {
2851
- const existing = (await collectWorktreeSnapshot(repoRoot)).worktrees.find((worktree) => worktree.branch === branch);
2852
- if (existing !== void 0) return {
2853
- status: "existing",
2854
- branch,
2855
- path: existing.path
2856
- };
3031
+ const snapshot = await collectWorktreeSnapshot(repoRoot);
3032
+ const existing = snapshot.worktrees.find((worktree) => worktree.branch === branch);
3033
+ if (existing !== void 0) {
3034
+ if (snapshot.baseBranch !== null) {
3035
+ const branchHead = await resolveBranchHead({
3036
+ repoRoot,
3037
+ branch
3038
+ });
3039
+ await upsertWorktreeMergeLifecycle({
3040
+ repoRoot,
3041
+ branch,
3042
+ baseBranch: snapshot.baseBranch,
3043
+ createdHead: branchHead
3044
+ });
3045
+ }
3046
+ return {
3047
+ status: "existing",
3048
+ branch,
3049
+ path: existing.path
3050
+ };
3051
+ }
2857
3052
  const targetPath = branchToWorktreePath(repoRoot, branch);
2858
3053
  await ensureTargetPathWritable(targetPath);
2859
3054
  const hookContext = createHookContext({
@@ -2868,6 +3063,7 @@ const createCli = (options = {}) => {
2868
3063
  name: "switch",
2869
3064
  context: hookContext
2870
3065
  });
3066
+ let lifecycleBaseBranch = snapshot.baseBranch;
2871
3067
  if (await doesGitRefExist(repoRoot, `refs/heads/${branch}`)) await runGitCommand({
2872
3068
  cwd: repoRoot,
2873
3069
  args: [
@@ -2877,17 +3073,33 @@ const createCli = (options = {}) => {
2877
3073
  branch
2878
3074
  ]
2879
3075
  });
2880
- else await runGitCommand({
2881
- cwd: repoRoot,
2882
- args: [
2883
- "worktree",
2884
- "add",
2885
- "-b",
3076
+ else {
3077
+ const baseBranch = await resolveBaseBranch(repoRoot);
3078
+ lifecycleBaseBranch = baseBranch;
3079
+ await runGitCommand({
3080
+ cwd: repoRoot,
3081
+ args: [
3082
+ "worktree",
3083
+ "add",
3084
+ "-b",
3085
+ branch,
3086
+ targetPath,
3087
+ baseBranch
3088
+ ]
3089
+ });
3090
+ }
3091
+ if (lifecycleBaseBranch !== null) {
3092
+ const branchHead = await resolveBranchHead({
3093
+ repoRoot,
3094
+ branch
3095
+ });
3096
+ await upsertWorktreeMergeLifecycle({
3097
+ repoRoot,
2886
3098
  branch,
2887
- targetPath,
2888
- await resolveBaseBranch(repoRoot)
2889
- ]
2890
- });
3099
+ baseBranch: lifecycleBaseBranch,
3100
+ createdHead: branchHead
3101
+ });
3102
+ }
2891
3103
  await runPostHook({
2892
3104
  name: "switch",
2893
3105
  context: hookContext
@@ -2987,6 +3199,19 @@ const createCli = (options = {}) => {
2987
3199
  newPath
2988
3200
  ]
2989
3201
  });
3202
+ if (snapshot.baseBranch !== null) {
3203
+ const branchHead = await resolveBranchHead({
3204
+ repoRoot,
3205
+ branch: newBranch
3206
+ });
3207
+ await moveWorktreeMergeLifecycle({
3208
+ repoRoot,
3209
+ fromBranch: oldBranch,
3210
+ toBranch: newBranch,
3211
+ baseBranch: snapshot.baseBranch,
3212
+ createdHead: branchHead
3213
+ });
3214
+ }
2990
3215
  await runPostHook({
2991
3216
  name: "mv",
2992
3217
  context: hookContext
@@ -3076,6 +3301,10 @@ const createCli = (options = {}) => {
3076
3301
  repoRoot,
3077
3302
  branch: target.branch
3078
3303
  });
3304
+ await deleteWorktreeMergeLifecycle({
3305
+ repoRoot,
3306
+ branch: target.branch
3307
+ });
3079
3308
  await runPostHook({
3080
3309
  name: "del",
3081
3310
  context: hookContext
@@ -3107,7 +3336,7 @@ const createCli = (options = {}) => {
3107
3336
  if (parsedArgs.apply === true && parsedArgs.dryRun === true) throw createCliError("INVALID_ARGUMENT", { message: "Cannot use --apply and --dry-run together" });
3108
3337
  const dryRun = parsedArgs.apply !== true;
3109
3338
  const execute = async () => {
3110
- const candidates = (await collectWorktreeSnapshot(repoRoot)).worktrees.filter((worktree) => worktree.branch !== null).filter((worktree) => worktree.path !== repoRoot).filter((worktree) => worktree.dirty === false).filter((worktree) => worktree.locked.value === false).filter((worktree) => worktree.merged.byAncestry === true).map((worktree) => worktree.branch);
3339
+ const candidates = (await collectWorktreeSnapshot(repoRoot)).worktrees.filter((worktree) => worktree.branch !== null).filter((worktree) => worktree.path !== repoRoot).filter((worktree) => worktree.dirty === false).filter((worktree) => worktree.locked.value === false).filter((worktree) => worktree.merged.overall === true).map((worktree) => worktree.branch);
3111
3340
  if (dryRun) return {
3112
3341
  deleted: [],
3113
3342
  candidates,
@@ -3150,6 +3379,10 @@ const createCli = (options = {}) => {
3150
3379
  repoRoot,
3151
3380
  branch
3152
3381
  });
3382
+ await deleteWorktreeMergeLifecycle({
3383
+ repoRoot,
3384
+ branch
3385
+ });
3153
3386
  deleted.push(branch);
3154
3387
  }
3155
3388
  await runPostHook({
@@ -3239,8 +3472,19 @@ const createCli = (options = {}) => {
3239
3472
  `${remote}/${branch}`
3240
3473
  ]
3241
3474
  });
3242
- const existing = (await collectWorktreeSnapshot(repoRoot)).worktrees.find((worktree) => worktree.branch === branch);
3475
+ const snapshot = await collectWorktreeSnapshot(repoRoot);
3476
+ const lifecycleBaseBranch = snapshot.baseBranch;
3477
+ const existing = snapshot.worktrees.find((worktree) => worktree.branch === branch);
3243
3478
  if (existing !== void 0) {
3479
+ if (lifecycleBaseBranch !== null) await upsertWorktreeMergeLifecycle({
3480
+ repoRoot,
3481
+ branch,
3482
+ baseBranch: lifecycleBaseBranch,
3483
+ createdHead: await resolveBranchHead({
3484
+ repoRoot,
3485
+ branch
3486
+ })
3487
+ });
3244
3488
  await runPostHook({
3245
3489
  name: "get",
3246
3490
  context: hookContext
@@ -3262,6 +3506,15 @@ const createCli = (options = {}) => {
3262
3506
  branch
3263
3507
  ]
3264
3508
  });
3509
+ if (lifecycleBaseBranch !== null) await upsertWorktreeMergeLifecycle({
3510
+ repoRoot,
3511
+ branch,
3512
+ baseBranch: lifecycleBaseBranch,
3513
+ createdHead: await resolveBranchHead({
3514
+ repoRoot,
3515
+ branch
3516
+ })
3517
+ });
3265
3518
  await runPostHook({
3266
3519
  name: "get",
3267
3520
  context: hookContext
@@ -3368,6 +3621,15 @@ const createCli = (options = {}) => {
3368
3621
  branch
3369
3622
  ]
3370
3623
  });
3624
+ await upsertWorktreeMergeLifecycle({
3625
+ repoRoot,
3626
+ branch,
3627
+ baseBranch,
3628
+ createdHead: await resolveBranchHead({
3629
+ repoRoot,
3630
+ branch
3631
+ })
3632
+ });
3371
3633
  if (stashOid !== null) {
3372
3634
  if ((await runGitCommand({
3373
3635
  cwd: targetPath,