@ricsam/r5d-worker 0.0.83 → 0.0.87

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.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.83",
3
+ "version": "0.0.87",
4
4
  "type": "module"
5
5
  }
@@ -80,6 +80,29 @@ function git(cwd, args, action) {
80
80
  function tryGit(cwd, args) {
81
81
  return gitResult(cwd, args).exitCode === 0;
82
82
  }
83
+ async function gitResultAsync(cwd, args) {
84
+ const subprocess = Bun.spawn(gitCommandArgs(args), {
85
+ cwd,
86
+ stdin: "ignore",
87
+ stdout: "pipe",
88
+ stderr: "pipe",
89
+ env: workerGitProcessEnvironment()
90
+ });
91
+ const [stdout, stderr, exitCode] = await Promise.all([
92
+ new Response(subprocess.stdout).text(),
93
+ new Response(subprocess.stderr).text(),
94
+ subprocess.exited
95
+ ]);
96
+ return { exitCode, stdout: stdout.trim(), stderr: stderr.trim() };
97
+ }
98
+ async function gitAsync(cwd, args, action) {
99
+ const result = await gitResultAsync(cwd, args);
100
+ if (result.exitCode !== 0) throw new Error(`${action}: ${result.stderr || result.stdout || `git exited ${result.exitCode}`}`);
101
+ return result.stdout;
102
+ }
103
+ async function tryGitAsync(cwd, args) {
104
+ return (await gitResultAsync(cwd, args)).exitCode === 0;
105
+ }
83
106
  function branchPath(projectRoot, branchName) {
84
107
  validateManagedBranchName(branchName);
85
108
  const result = path.join(projectRoot, ...branchName.split("/"));
@@ -139,6 +162,10 @@ function commonGitDirectory(checkoutPath) {
139
162
  const result = gitResult(checkoutPath, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
140
163
  return result.exitCode === 0 ? path.resolve(result.stdout) : null;
141
164
  }
165
+ async function commonGitDirectoryAsync(checkoutPath) {
166
+ const result = await gitResultAsync(checkoutPath, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
167
+ return result.exitCode === 0 ? path.resolve(result.stdout) : null;
168
+ }
142
169
  function hasLinkedProjectWorktreeLayout(input) {
143
170
  const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
144
171
  if (gitDirectoryKind(primaryPath) !== "directory") return false;
@@ -150,17 +177,19 @@ function hasLinkedProjectWorktreeLayout(input) {
150
177
  return gitDirectoryKind(checkoutPath) === "file" && commonGitDirectory(checkoutPath) === primaryCommonDir;
151
178
  });
152
179
  }
153
- function hasCompatibleLinkedProjectWorktreeLayout(input) {
180
+ async function hasCompatibleLinkedProjectWorktreeLayout(input) {
154
181
  const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
155
182
  if (gitDirectoryKind(primaryPath) !== "directory") return false;
156
- const primaryCommonDir = commonGitDirectory(primaryPath);
183
+ const primaryCommonDir = await commonGitDirectoryAsync(primaryPath);
157
184
  if (!primaryCommonDir) return false;
158
- return input.branches.every(({ branchName }) => {
159
- if (branchName === input.primaryBranchName) return true;
185
+ for (const { branchName } of input.branches) {
186
+ if (branchName === input.primaryBranchName) continue;
160
187
  const checkoutPath = branchPath(input.projectRoot, branchName);
161
188
  const kind = gitDirectoryKind(checkoutPath);
162
- return kind === null || kind === "file" && commonGitDirectory(checkoutPath) === primaryCommonDir;
163
- });
189
+ if (kind === null) continue;
190
+ if (kind !== "file" || await commonGitDirectoryAsync(checkoutPath) !== primaryCommonDir) return false;
191
+ }
192
+ return true;
164
193
  }
165
194
  function fsyncDirectory(directory) {
166
195
  const descriptor = fs.openSync(directory, "r");
@@ -574,24 +603,34 @@ function configureProjectBranchTracking(checkoutPath, branchName, originBranchNa
574
603
  `configure upstream branch for ${branchName}`
575
604
  );
576
605
  }
577
- function configureRepository(input) {
578
- if (tryGit(input.primaryPath, ["remote", "get-url", "origin"])) {
579
- git(input.primaryPath, ["remote", "set-url", "origin", input.originUrl], "configure project origin");
606
+ async function configureProjectBranchTrackingAsync(checkoutPath, branchName, originBranchName) {
607
+ validateManagedBranchName(branchName);
608
+ validateManagedBranchName(originBranchName);
609
+ await gitAsync(checkoutPath, ["config", "--local", `branch.${branchName}.remote`, "origin"], `configure origin for ${branchName}`);
610
+ await gitAsync(
611
+ checkoutPath,
612
+ ["config", "--local", `branch.${branchName}.merge`, `refs/heads/${originBranchName}`],
613
+ `configure upstream branch for ${branchName}`
614
+ );
615
+ }
616
+ async function configureRepository(input) {
617
+ if (await tryGitAsync(input.primaryPath, ["remote", "get-url", "origin"])) {
618
+ await gitAsync(input.primaryPath, ["remote", "set-url", "origin", input.originUrl], "configure project origin");
580
619
  } else {
581
- git(input.primaryPath, ["remote", "add", "origin", input.originUrl], "configure project origin");
620
+ await gitAsync(input.primaryPath, ["remote", "add", "origin", input.originUrl], "configure project origin");
582
621
  }
583
- tryGit(input.primaryPath, ["config", "--local", "--unset-all", "remote.origin.pushurl"]);
584
- git(input.primaryPath, ["config", "--local", "--replace-all", "credential.helper", ""], "reset project credential helpers");
585
- git(input.primaryPath, ["config", "--local", "credential.useHttpPath", "false"], "configure project credential path matching");
622
+ await tryGitAsync(input.primaryPath, ["config", "--local", "--unset-all", "remote.origin.pushurl"]);
623
+ await gitAsync(input.primaryPath, ["config", "--local", "--replace-all", "credential.helper", ""], "reset project credential helpers");
624
+ await gitAsync(input.primaryPath, ["config", "--local", "credential.useHttpPath", "false"], "configure project credential path matching");
586
625
  if (input.originCredentialUsername) {
587
- git(
626
+ await gitAsync(
588
627
  input.primaryPath,
589
628
  ["config", "--local", "--replace-all", gitCredentialUsernameConfigKey(input.originUrl), input.originCredentialUsername],
590
629
  "configure project credential username"
591
630
  );
592
631
  }
593
632
  if (input.credentialHelper) {
594
- git(
633
+ await gitAsync(
595
634
  input.primaryPath,
596
635
  ["config", "--local", "--add", "credential.helper", input.credentialHelper],
597
636
  "configure project credential helper"
@@ -601,23 +640,23 @@ function configureRepository(input) {
601
640
  const name = input.gitIdentity.name.trim();
602
641
  const email = input.gitIdentity.email.trim();
603
642
  if (!name || !email) throw new Error("Git identity must include name and email");
604
- git(input.primaryPath, ["config", "--local", "user.name", name], "configure project Git user name");
605
- git(input.primaryPath, ["config", "--local", "user.email", email], "configure project Git user email");
643
+ await gitAsync(input.primaryPath, ["config", "--local", "user.name", name], "configure project Git user name");
644
+ await gitAsync(input.primaryPath, ["config", "--local", "user.email", email], "configure project Git user email");
606
645
  }
607
646
  if (input.primaryBranchName && input.defaultBranch && input.branches) {
608
- git(input.primaryPath, ["config", "--local", "push.default", "upstream"], "configure project push mapping");
647
+ await gitAsync(input.primaryPath, ["config", "--local", "push.default", "upstream"], "configure project push mapping");
609
648
  for (const { branchName } of input.branches) {
610
649
  const originBranchName = projectOriginBranchName({
611
650
  branchName,
612
651
  primaryBranchName: input.primaryBranchName,
613
652
  defaultBranch: input.defaultBranch
614
653
  });
615
- configureProjectBranchTracking(input.primaryPath, branchName, originBranchName);
654
+ await configureProjectBranchTrackingAsync(input.primaryPath, branchName, originBranchName);
616
655
  }
617
656
  }
618
657
  }
619
- function fetchProjectHeads(input) {
620
- const originFetch = gitResult(input.primaryPath, [
658
+ async function fetchProjectHeads(input) {
659
+ const originFetch = await gitResultAsync(input.primaryPath, [
621
660
  ...gitTransportSecurityArgs(input.originUrl, input.credentialHelper, input.originCredentialUsername),
622
661
  "fetch",
623
662
  "--no-recurse-submodules",
@@ -628,7 +667,7 @@ function fetchProjectHeads(input) {
628
667
  if (originFetch.exitCode !== 0) {
629
668
  throw new Error(`Fetch project origin: ${originFetch.stderr || originFetch.stdout || `git exited ${originFetch.exitCode}`}`);
630
669
  }
631
- const mirrorFetch = gitResult(input.primaryPath, [
670
+ const mirrorFetch = await gitResultAsync(input.primaryPath, [
632
671
  ...gitTransportSecurityArgs(input.mirrorUrl, input.credentialHelper, input.mirrorCredentialUsername),
633
672
  "fetch",
634
673
  "--no-recurse-submodules",
@@ -643,13 +682,13 @@ ${mirrorFetch.stdout}`
643
682
  throw new Error(`Fetch project head mirror: ${mirrorFetch.stderr || mirrorFetch.stdout || `git exited ${mirrorFetch.exitCode}`}`);
644
683
  }
645
684
  }
646
- function ensureCommitAvailable(input) {
647
- if (tryGit(input.primaryPath, ["cat-file", "-e", `${input.commitHash}^{commit}`])) return;
685
+ async function ensureCommitAvailable(input) {
686
+ if (await tryGitAsync(input.primaryPath, ["cat-file", "-e", `${input.commitHash}^{commit}`])) return;
648
687
  for (const [url, username] of [
649
688
  [input.mirrorUrl, input.mirrorCredentialUsername],
650
689
  [input.originUrl, input.originCredentialUsername]
651
690
  ]) {
652
- if (tryGit(input.primaryPath, [
691
+ if (await tryGitAsync(input.primaryPath, [
653
692
  ...gitTransportSecurityArgs(url, input.credentialHelper, username),
654
693
  "fetch",
655
694
  "--no-recurse-submodules",
@@ -661,34 +700,34 @@ function ensureCommitAvailable(input) {
661
700
  }
662
701
  throw new Error(`Project base commit ${input.commitHash} is unavailable from origin and the canonical mirror`);
663
702
  }
664
- function seedForBranch(input) {
703
+ async function seedForBranch(input) {
665
704
  const mirrorRef = `refs/r5d/mirror/${input.branch.branchName}`;
666
- const mirrorHead = tryGit(input.primaryPath, ["show-ref", "--verify", "--quiet", mirrorRef]) ? git(input.primaryPath, ["rev-parse", mirrorRef], "resolve project mirror head") : null;
705
+ const mirrorHead = await tryGitAsync(input.primaryPath, ["show-ref", "--verify", "--quiet", mirrorRef]) ? await gitAsync(input.primaryPath, ["rev-parse", mirrorRef], "resolve project mirror head") : null;
667
706
  if (input.requireMirrorHead && !mirrorHead) {
668
707
  throw new Error(`Exact hidden project mirror head is missing for ${input.branch.branchName}`);
669
708
  }
670
709
  const seed = mirrorHead ?? input.branch.baseCommitHash;
671
- ensureCommitAvailable({ ...input, commitHash: seed });
710
+ await ensureCommitAvailable({ ...input, commitHash: seed });
672
711
  return { seed, mirrorHead };
673
712
  }
674
- function preserveHead(checkoutPath) {
675
- if (!tryGit(checkoutPath, ["rev-parse", "--verify", "HEAD"])) return;
676
- const head = git(checkoutPath, ["rev-parse", "HEAD"], "read pre-reconciliation project head");
677
- git(checkoutPath, ["update-ref", `refs/r5d/pre-connect/${head}`, head], "preserve pre-reconciliation project head");
713
+ async function preserveHead(checkoutPath) {
714
+ if (!await tryGitAsync(checkoutPath, ["rev-parse", "--verify", "HEAD"])) return;
715
+ const head = await gitAsync(checkoutPath, ["rev-parse", "HEAD"], "read pre-reconciliation project head");
716
+ await gitAsync(checkoutPath, ["update-ref", `refs/r5d/pre-connect/${head}`, head], "preserve pre-reconciliation project head");
678
717
  }
679
- function initializeLinkedLayout(input) {
718
+ async function initializeLinkedLayout(input) {
680
719
  for (const { branchName } of [...input.branches].sort((left, right) => right.branchName.length - left.branchName.length)) {
681
720
  fs.rmSync(branchPath(input.projectRoot, branchName), { recursive: true, force: true });
682
721
  }
683
722
  const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
684
723
  fs.mkdirSync(path.dirname(primaryPath), { recursive: true });
685
- git(void 0, ["init", `--initial-branch=${input.primaryBranchName}`, primaryPath], "initialize primary project clone");
686
- configureRepository({ ...input, primaryPath });
687
- fetchProjectHeads({ ...input, primaryPath });
724
+ await gitAsync(void 0, ["init", `--initial-branch=${input.primaryBranchName}`, primaryPath], "initialize primary project clone");
725
+ await configureRepository({ ...input, primaryPath });
726
+ await fetchProjectHeads({ ...input, primaryPath });
688
727
  const seeds = /* @__PURE__ */ new Map();
689
728
  const mirrorHeads = /* @__PURE__ */ new Map();
690
729
  for (const branch of input.branches) {
691
- const resolved = seedForBranch({
730
+ const resolved = await seedForBranch({
692
731
  ...input,
693
732
  primaryPath,
694
733
  branch,
@@ -697,63 +736,71 @@ function initializeLinkedLayout(input) {
697
736
  seeds.set(branch.branchName, resolved.seed);
698
737
  mirrorHeads.set(branch.branchName, resolved.mirrorHead);
699
738
  }
700
- git(
739
+ await gitAsync(
701
740
  primaryPath,
702
741
  ["checkout", "--no-recurse-submodules", "-B", input.primaryBranchName, seeds.get(input.primaryBranchName)],
703
742
  "anchor primary project worktree"
704
743
  );
705
744
  for (const branch of input.branches) {
706
745
  if (branch.branchName === input.primaryBranchName) continue;
707
- git(primaryPath, ["branch", "-f", branch.branchName, seeds.get(branch.branchName)], `anchor project branch ${branch.branchName}`);
746
+ await gitAsync(
747
+ primaryPath,
748
+ ["branch", "-f", branch.branchName, seeds.get(branch.branchName)],
749
+ `anchor project branch ${branch.branchName}`
750
+ );
708
751
  const checkoutPath = branchPath(input.projectRoot, branch.branchName);
709
752
  fs.mkdirSync(path.dirname(checkoutPath), { recursive: true });
710
- git(primaryPath, ["worktree", "add", "--force", checkoutPath, branch.branchName], `create linked worktree ${branch.branchName}`);
753
+ await gitAsync(primaryPath, ["worktree", "add", "--force", checkoutPath, branch.branchName], `create linked worktree ${branch.branchName}`);
711
754
  }
712
755
  return mirrorHeads;
713
756
  }
714
- function reconcileLinkedLayout(input) {
757
+ async function reconcileLinkedLayout(input) {
715
758
  const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
716
- configureRepository({ ...input, primaryPath });
717
- tryGit(primaryPath, ["worktree", "prune"]);
718
- fetchProjectHeads({ ...input, primaryPath });
759
+ await configureRepository({ ...input, primaryPath });
760
+ await tryGitAsync(primaryPath, ["worktree", "prune"]);
761
+ await fetchProjectHeads({ ...input, primaryPath });
719
762
  const mirrorHeads = /* @__PURE__ */ new Map();
720
763
  const mutatedBranches = /* @__PURE__ */ new Set();
721
764
  for (const branch of input.branches) {
722
765
  const checkoutPath = branchPath(input.projectRoot, branch.branchName);
723
- const resolved = seedForBranch({
766
+ const resolved = await seedForBranch({
724
767
  ...input,
725
768
  primaryPath,
726
769
  branch,
727
770
  requireMirrorHead: input.exactMirrorBranches?.has(branch.branchName)
728
771
  });
729
772
  mirrorHeads.set(branch.branchName, resolved.mirrorHead);
730
- if (branch.branchName === input.primaryBranchName || commonGitDirectory(checkoutPath) === commonGitDirectory(primaryPath)) {
731
- const currentHead = git(checkoutPath, ["rev-parse", "HEAD"], `resolve linked worktree head ${branch.branchName}`);
773
+ if (branch.branchName === input.primaryBranchName || await commonGitDirectoryAsync(checkoutPath) === await commonGitDirectoryAsync(primaryPath)) {
774
+ const currentHead = await gitAsync(checkoutPath, ["rev-parse", "HEAD"], `resolve linked worktree head ${branch.branchName}`);
732
775
  if (currentHead === resolved.seed) continue;
733
776
  input.beforeDestructiveMutation();
734
777
  mutatedBranches.add(branch.branchName);
735
- preserveHead(checkoutPath);
736
- git(checkoutPath, ["reset", "--hard", resolved.seed], `anchor linked worktree ${branch.branchName}`);
778
+ await preserveHead(checkoutPath);
779
+ await gitAsync(checkoutPath, ["reset", "--hard", resolved.seed], `anchor linked worktree ${branch.branchName}`);
737
780
  continue;
738
781
  }
739
782
  input.beforeDestructiveMutation();
740
783
  mutatedBranches.add(branch.branchName);
741
784
  fs.rmSync(checkoutPath, { recursive: true, force: true });
742
- git(primaryPath, ["branch", "-f", branch.branchName, resolved.seed], `anchor project branch ${branch.branchName}`);
785
+ await gitAsync(primaryPath, ["branch", "-f", branch.branchName, resolved.seed], `anchor project branch ${branch.branchName}`);
743
786
  fs.mkdirSync(path.dirname(checkoutPath), { recursive: true });
744
- git(primaryPath, ["worktree", "add", "--force", checkoutPath, branch.branchName], `create linked worktree ${branch.branchName}`);
787
+ await gitAsync(primaryPath, ["worktree", "add", "--force", checkoutPath, branch.branchName], `create linked worktree ${branch.branchName}`);
745
788
  }
746
789
  return { mirrorHeads, mutatedBranches };
747
790
  }
748
- function aheadBehind(checkoutPath, branchName, primaryBranchName, defaultBranch) {
791
+ async function aheadBehind(checkoutPath, branchName, primaryBranchName, defaultBranch) {
749
792
  const originBranchName = projectOriginBranchName({ branchName, primaryBranchName, defaultBranch });
750
793
  const originRef = `refs/remotes/origin/${originBranchName}`;
751
- if (!tryGit(checkoutPath, ["show-ref", "--verify", "--quiet", originRef])) return { ahead: null, behind: null };
752
- const output = git(checkoutPath, ["rev-list", "--left-right", "--count", `${originRef}...HEAD`], "measure project origin divergence");
794
+ if (!await tryGitAsync(checkoutPath, ["show-ref", "--verify", "--quiet", originRef])) return { ahead: null, behind: null };
795
+ const output = await gitAsync(
796
+ checkoutPath,
797
+ ["rev-list", "--left-right", "--count", `${originRef}...HEAD`],
798
+ "measure project origin divergence"
799
+ );
753
800
  const [behind, ahead] = output.split(/\s+/).map(Number);
754
801
  return Number.isFinite(ahead) && Number.isFinite(behind) ? { ahead, behind } : { ahead: null, behind: null };
755
802
  }
756
- function ensureProjectWorktrees(input) {
803
+ async function ensureProjectWorktrees(input) {
757
804
  const projectRoot = path.resolve(input.projectRoot);
758
805
  const defaultBranch = input.defaultBranch ?? input.primaryBranchName;
759
806
  assertNonOverlappingBranches(input.branches);
@@ -769,7 +816,7 @@ function ensureProjectWorktrees(input) {
769
816
  };
770
817
  try {
771
818
  fs.mkdirSync(projectRoot, { recursive: true });
772
- const linked = hasCompatibleLinkedProjectWorktreeLayout({
819
+ const linked = await hasCompatibleLinkedProjectWorktreeLayout({
773
820
  projectRoot,
774
821
  primaryBranchName: input.primaryBranchName,
775
822
  branches: input.branches
@@ -777,7 +824,7 @@ function ensureProjectWorktrees(input) {
777
824
  let mirrorHeads;
778
825
  let mutatedBranches;
779
826
  if (linked) {
780
- ({ mirrorHeads, mutatedBranches } = reconcileLinkedLayout({
827
+ ({ mirrorHeads, mutatedBranches } = await reconcileLinkedLayout({
781
828
  ...input,
782
829
  projectRoot,
783
830
  defaultBranch,
@@ -785,7 +832,7 @@ function ensureProjectWorktrees(input) {
785
832
  }));
786
833
  } else {
787
834
  ensureSnapshot();
788
- mirrorHeads = initializeLinkedLayout({ ...input, projectRoot, defaultBranch });
835
+ mirrorHeads = await initializeLinkedLayout({ ...input, projectRoot, defaultBranch });
789
836
  mutatedBranches = new Set(input.branches.map(({ branchName }) => branchName));
790
837
  }
791
838
  const preparedSnapshot = snapshot;
@@ -803,17 +850,19 @@ function ensureProjectWorktrees(input) {
803
850
  deletionMode: "git"
804
851
  });
805
852
  }
806
- const states = input.branches.map((branch) => {
853
+ const states = [];
854
+ for (const branch of input.branches) {
807
855
  const checkoutPath = branchPath(projectRoot, branch.branchName);
808
- const divergence = aheadBehind(checkoutPath, branch.branchName, input.primaryBranchName, defaultBranch);
809
- return {
856
+ const divergence = await aheadBehind(checkoutPath, branch.branchName, input.primaryBranchName, defaultBranch);
857
+ states.push({
810
858
  branchName: branch.branchName,
811
859
  branchPath: checkoutPath,
812
- head: git(checkoutPath, ["rev-parse", "HEAD"], `resolve project head ${branch.branchName}`),
860
+ head: await gitAsync(checkoutPath, ["rev-parse", "HEAD"], `resolve project head ${branch.branchName}`),
813
861
  mirrorHead: mirrorHeads.get(branch.branchName) ?? null,
814
862
  ...divergence
815
- };
816
- }).sort((left, right) => left.branchName.localeCompare(right.branchName));
863
+ });
864
+ }
865
+ states.sort((left, right) => left.branchName.localeCompare(right.branchName));
817
866
  if (snapshot) persistReconciledProjectAndConsumeSnapshot(snapshot);
818
867
  return states;
819
868
  } catch (error) {
@@ -829,14 +878,31 @@ function ensureProjectWorktrees(input) {
829
878
  throw error;
830
879
  }
831
880
  }
881
+ const WORKTREE_OPERATION_STATE_PATHS = ["MERGE_HEAD", "CHERRY_PICK_HEAD", "REVERT_HEAD", "rebase-merge", "rebase-apply", "sequencer"];
882
+ function perWorktreeGitDirectory(checkoutPath) {
883
+ const gitPath = path.join(checkoutPath, ".git");
884
+ let stat;
885
+ try {
886
+ stat = fs.lstatSync(gitPath);
887
+ } catch {
888
+ return null;
889
+ }
890
+ if (stat.isDirectory()) return gitPath;
891
+ if (!stat.isFile()) return null;
892
+ let pointer;
893
+ try {
894
+ pointer = fs.readFileSync(gitPath, "utf8");
895
+ } catch {
896
+ return null;
897
+ }
898
+ const match = /^gitdir:[ \t]*(.+?)[ \t\r\n]*$/m.exec(pointer);
899
+ if (!match?.[1]) return null;
900
+ return path.resolve(checkoutPath, match[1]);
901
+ }
832
902
  function projectWorktreeOperationInProgress(checkoutPath) {
833
- const gitPaths = ["MERGE_HEAD", "CHERRY_PICK_HEAD", "REVERT_HEAD", "rebase-merge", "rebase-apply", "sequencer"];
834
- return gitPaths.some((gitPath) => {
835
- const result = gitResult(checkoutPath, ["rev-parse", "--git-path", gitPath]);
836
- if (result.exitCode !== 0) return false;
837
- const resolved = path.isAbsolute(result.stdout) ? result.stdout : path.resolve(checkoutPath, result.stdout);
838
- return fs.existsSync(resolved);
839
- });
903
+ const gitDirectory = perWorktreeGitDirectory(checkoutPath);
904
+ if (!gitDirectory) return false;
905
+ return WORKTREE_OPERATION_STATE_PATHS.some((statePath) => fs.existsSync(path.join(gitDirectory, statePath)));
840
906
  }
841
907
  function createLinkedProjectBranch(input) {
842
908
  validateManagedBranchName(input.sourceBranchName);
@@ -977,9 +1043,9 @@ function removeProjectWorktrees(input) {
977
1043
  }
978
1044
  fs.rmSync(projectRoot, { recursive: true, force: true });
979
1045
  }
980
- function deleteProjectMirrorBranch(input) {
1046
+ async function deleteProjectMirrorBranch(input) {
981
1047
  validateManagedBranchName(input.branchName);
982
- const deleted = gitResult(input.gitDirectory, [
1048
+ const deleted = await gitResultAsync(input.gitDirectory, [
983
1049
  ...gitTransportSecurityArgs(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
984
1050
  "push",
985
1051
  "--no-recurse-submodules",
@@ -993,23 +1059,23 @@ ${deleted.stdout}`)) {
993
1059
  );
994
1060
  }
995
1061
  }
996
- function pushProjectMirrorHeads(input) {
1062
+ async function pushProjectMirrorHeads(input) {
997
1063
  const results = [];
998
1064
  for (const branchName of [...input.branchNames].sort()) {
999
1065
  if (input.onlyBranches && !input.onlyBranches.has(branchName)) continue;
1000
1066
  const checkoutPath = branchPath(input.projectRoot, branchName);
1001
- const head = input.publicationHeads ? input.publicationHeads.get(branchName) : git(checkoutPath, ["rev-parse", "HEAD"], `resolve mirror head for ${branchName}`);
1067
+ const head = input.publicationHeads ? input.publicationHeads.get(branchName) : await gitAsync(checkoutPath, ["rev-parse", "HEAD"], `resolve mirror head for ${branchName}`);
1002
1068
  if (!head || !/^[0-9a-f]{40}(?:[0-9a-f]{24})?$/.test(head)) {
1003
1069
  throw new Error(`Missing exact publication head for project branch ${branchName}`);
1004
1070
  }
1005
- if (!tryGit(checkoutPath, ["cat-file", "-e", `${head}^{commit}`])) {
1071
+ if (!await tryGitAsync(checkoutPath, ["cat-file", "-e", `${head}^{commit}`])) {
1006
1072
  throw new Error(`Exact publication head ${head} is unavailable for project branch ${branchName}`);
1007
1073
  }
1008
1074
  if (projectWorktreeOperationInProgress(checkoutPath)) {
1009
1075
  results.push({ branchName, head, pushed: false, reason: "git_operation_in_progress" });
1010
1076
  continue;
1011
1077
  }
1012
- git(
1078
+ await gitAsync(
1013
1079
  checkoutPath,
1014
1080
  [
1015
1081
  ...gitTransportSecurityArgs(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
@@ -1030,9 +1096,9 @@ function assertProjectMirrorHeadsPushed(results) {
1030
1096
  throw new Error(`Hidden project mirror publication skipped ${skipped.branchName}${skipped.reason ? ` (${skipped.reason})` : ""}`);
1031
1097
  }
1032
1098
  }
1033
- function observeProjectMirrorHeads(input) {
1099
+ async function observeProjectMirrorHeads(input) {
1034
1100
  const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
1035
- const fetch = gitResult(primaryPath, [
1101
+ const fetch = await gitResultAsync(primaryPath, [
1036
1102
  ...gitTransportSecurityArgs(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
1037
1103
  "fetch",
1038
1104
  "--no-recurse-submodules",
@@ -1042,50 +1108,63 @@ function observeProjectMirrorHeads(input) {
1042
1108
  ]);
1043
1109
  if (fetch.exitCode !== 0)
1044
1110
  throw new Error(`Refresh project head mirror: ${fetch.stderr || fetch.stdout || `git exited ${fetch.exitCode}`}`);
1045
- return [...input.branchNames].sort().map((branchName) => {
1111
+ const observations = [];
1112
+ for (const branchName of [...input.branchNames].sort()) {
1046
1113
  const checkoutPath = branchPath(input.projectRoot, branchName);
1047
- const previousHead = git(checkoutPath, ["rev-parse", "HEAD"], `resolve local project head ${branchName}`);
1114
+ const previousHead = await gitAsync(checkoutPath, ["rev-parse", "HEAD"], `resolve local project head ${branchName}`);
1048
1115
  const mirrorRef = `refs/r5d/mirror/${branchName}`;
1049
- const mirrorHead = tryGit(primaryPath, ["show-ref", "--verify", "--quiet", mirrorRef]) ? git(primaryPath, ["rev-parse", mirrorRef], `resolve refreshed mirror head ${branchName}`) : null;
1116
+ const mirrorHead = await tryGitAsync(primaryPath, ["show-ref", "--verify", "--quiet", mirrorRef]) ? await gitAsync(primaryPath, ["rev-parse", mirrorRef], `resolve refreshed mirror head ${branchName}`) : null;
1050
1117
  const shouldMove = Boolean(
1051
- mirrorHead && mirrorHead !== previousHead && (input.allowNonFastForward || tryGit(checkoutPath, ["merge-base", "--is-ancestor", previousHead, mirrorHead]))
1118
+ mirrorHead && mirrorHead !== previousHead && (input.allowNonFastForward || await tryGitAsync(checkoutPath, ["merge-base", "--is-ancestor", previousHead, mirrorHead]))
1052
1119
  );
1053
- return {
1120
+ observations.push({
1054
1121
  branchName,
1055
1122
  previousHead,
1056
1123
  mirrorHead,
1057
1124
  mode: input.allowNonFastForward ? "reseed" : "fast_forward",
1058
1125
  shouldMove
1059
- };
1060
- });
1126
+ });
1127
+ }
1128
+ return observations;
1061
1129
  }
1062
- function applyObservedProjectMirrorHeads(input) {
1063
- return [...input.observations].sort((left, right) => left.branchName.localeCompare(right.branchName)).map((observation) => {
1130
+ async function applyObservedProjectMirrorHeads(input) {
1131
+ const results = [];
1132
+ for (const observation of [...input.observations].sort((left, right) => left.branchName.localeCompare(right.branchName))) {
1064
1133
  const { branchName, mirrorHead } = observation;
1065
1134
  const checkoutPath = branchPath(input.projectRoot, branchName);
1066
- const currentHead = git(checkoutPath, ["rev-parse", "HEAD"], `resolve local project head ${branchName}`);
1135
+ const currentHead = await gitAsync(checkoutPath, ["rev-parse", "HEAD"], `resolve local project head ${branchName}`);
1067
1136
  if (!observation.shouldMove || !mirrorHead || currentHead === mirrorHead || currentHead !== observation.previousHead) {
1068
- return { branchName, previousHead: currentHead, mirrorHead, moved: false };
1137
+ results.push({ branchName, previousHead: currentHead, mirrorHead, moved: false });
1138
+ continue;
1069
1139
  }
1070
- if (!tryGit(checkoutPath, ["cat-file", "-e", `${mirrorHead}^{commit}`])) {
1140
+ if (!await tryGitAsync(checkoutPath, ["cat-file", "-e", `${mirrorHead}^{commit}`])) {
1071
1141
  throw new Error(`Observed hidden mirror head is no longer available for ${branchName}: ${mirrorHead}`);
1072
1142
  }
1073
1143
  const mutationAllowed = () => !projectWorktreeOperationInProgress(checkoutPath) && input.branchMutationAllowed?.(branchName, checkoutPath) !== false;
1074
1144
  if (!mutationAllowed()) {
1075
- return { branchName, previousHead: currentHead, mirrorHead, moved: false };
1145
+ results.push({ branchName, previousHead: currentHead, mirrorHead, moved: false });
1146
+ continue;
1076
1147
  }
1077
1148
  if (observation.mode === "reseed") {
1078
- preserveHead(checkoutPath);
1079
- if (!mutationAllowed()) return { branchName, previousHead: currentHead, mirrorHead, moved: false };
1149
+ await preserveHead(checkoutPath);
1150
+ if (!mutationAllowed()) {
1151
+ results.push({ branchName, previousHead: currentHead, mirrorHead, moved: false });
1152
+ continue;
1153
+ }
1080
1154
  }
1081
1155
  const action = observation.mode === "reseed" ? "reseed" : "fast-forward";
1082
- git(checkoutPath, ["read-tree", "--reset", mirrorHead], `${action} project index ${branchName}`);
1083
- git(checkoutPath, ["update-ref", `refs/heads/${branchName}`, mirrorHead, currentHead], `${action} project branch ${branchName}`);
1084
- return { branchName, previousHead: currentHead, mirrorHead, moved: true };
1085
- });
1156
+ await gitAsync(checkoutPath, ["read-tree", "--reset", mirrorHead], `${action} project index ${branchName}`);
1157
+ await gitAsync(
1158
+ checkoutPath,
1159
+ ["update-ref", `refs/heads/${branchName}`, mirrorHead, currentHead],
1160
+ `${action} project branch ${branchName}`
1161
+ );
1162
+ results.push({ branchName, previousHead: currentHead, mirrorHead, moved: true });
1163
+ }
1164
+ return results;
1086
1165
  }
1087
- function updateProjectHeadsFromMirror(input) {
1088
- const observations = observeProjectMirrorHeads(input);
1166
+ async function updateProjectHeadsFromMirror(input) {
1167
+ const observations = await observeProjectMirrorHeads(input);
1089
1168
  return applyObservedProjectMirrorHeads({
1090
1169
  projectRoot: input.projectRoot,
1091
1170
  primaryBranchName: input.primaryBranchName,
@@ -1093,14 +1172,14 @@ function updateProjectHeadsFromMirror(input) {
1093
1172
  branchMutationAllowed: input.branchMutationAllowed
1094
1173
  });
1095
1174
  }
1096
- function fastForwardProjectHeadsFromMirror(input) {
1097
- return updateProjectHeadsFromMirror({ ...input, allowNonFastForward: false }).map(({ moved, ...state }) => ({
1175
+ async function fastForwardProjectHeadsFromMirror(input) {
1176
+ return (await updateProjectHeadsFromMirror({ ...input, allowNonFastForward: false })).map(({ moved, ...state }) => ({
1098
1177
  ...state,
1099
1178
  fastForwarded: moved
1100
1179
  }));
1101
1180
  }
1102
- function reseedProjectHeadsFromMirror(input) {
1103
- return updateProjectHeadsFromMirror({ ...input, allowNonFastForward: true }).map(({ moved, ...state }) => ({
1181
+ async function reseedProjectHeadsFromMirror(input) {
1182
+ return (await updateProjectHeadsFromMirror({ ...input, allowNonFastForward: true })).map(({ moved, ...state }) => ({
1104
1183
  ...state,
1105
1184
  reseeded: moved
1106
1185
  }));
@@ -15,6 +15,12 @@ function projectBranchHasActiveWorkspaceTarget(projectId, branchName, activeTarg
15
15
  }
16
16
  return false;
17
17
  }
18
+ function projectBranchMountBusyDuringExclusiveSync(input) {
19
+ return input.executionDisabled || input.worktreeOperationInProgress || input.retainedCanonicalProcessGroup || projectBranchHasActiveWorkspaceTarget(input.projectId, input.branchName, input.activeTargets);
20
+ }
21
+ function workspacePlansMountBusyDuringExclusiveSync(activeTargets, retainedCanonicalProcessGroup = false) {
22
+ return retainedCanonicalProcessGroup || hasActiveVisibleProjectsWorkspaceTarget(activeTargets);
23
+ }
18
24
  function shouldDeferAutomaticWorkspaceSyncForPendingBranch(pendingCreatedBranches, activeTargets) {
19
25
  if (pendingCreatedBranches.length === 0) return false;
20
26
  const targets = [...activeTargets];
@@ -36,5 +42,7 @@ export {
36
42
  pendingCreatedBranchAutomaticSyncDeferral,
37
43
  pendingCreatedBranchKey,
38
44
  projectBranchHasActiveWorkspaceTarget,
39
- shouldDeferAutomaticWorkspaceSyncForPendingBranch
45
+ projectBranchMountBusyDuringExclusiveSync,
46
+ shouldDeferAutomaticWorkspaceSyncForPendingBranch,
47
+ workspacePlansMountBusyDuringExclusiveSync
40
48
  };
@@ -20,12 +20,32 @@ function shouldSerializeWorkspaceCommand(target) {
20
20
  function runWorkspaceCommand(target, coordinator, operation) {
21
21
  return shouldSerializeWorkspaceCommand(target) ? coordinator.runMutation(operation) : Promise.resolve().then(operation);
22
22
  }
23
+ async function runReservedWorkspaceCommand(target, coordinator, operation, releaseReservation) {
24
+ let released = false;
25
+ const releaseOnce = () => {
26
+ if (released) return;
27
+ released = true;
28
+ releaseReservation();
29
+ };
30
+ try {
31
+ return await runWorkspaceCommand(target, coordinator, async () => {
32
+ try {
33
+ return await operation();
34
+ } finally {
35
+ releaseOnce();
36
+ }
37
+ });
38
+ } finally {
39
+ releaseOnce();
40
+ }
41
+ }
23
42
  function acquireWorkspaceCommandMutation(target, coordinator) {
24
43
  return shouldSerializeWorkspaceCommand(target) ? coordinator.acquireMutation() : Promise.resolve(void 0);
25
44
  }
26
45
  export {
27
46
  acquireWorkspaceCommandMutation,
28
47
  reserveWorkspaceCommandAfterCurrentSync,
48
+ runReservedWorkspaceCommand,
29
49
  runWorkspaceCommand,
30
50
  shouldSerializeWorkspaceCommand
31
51
  };