@ricsam/r5d-worker 0.0.86 → 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.
- package/dist/cjs/main.cjs +98 -64
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/project-worktrees.cjs +183 -104
- package/dist/cjs/workspace-git-sync.cjs +491 -189
- package/dist/mjs/main.mjs +98 -64
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/project-worktrees.mjs +183 -104
- package/dist/mjs/workspace-git-sync.mjs +491 -189
- package/dist/types/project-worktrees.d.ts +12 -12
- package/dist/types/workspace-automatic-sync-policy.d.ts +7 -4
- package/dist/types/workspace-git-sync.d.ts +9 -9
- package/package.json +1 -1
|
@@ -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 =
|
|
183
|
+
const primaryCommonDir = await commonGitDirectoryAsync(primaryPath);
|
|
157
184
|
if (!primaryCommonDir) return false;
|
|
158
|
-
|
|
159
|
-
if (branchName === input.primaryBranchName)
|
|
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
|
-
|
|
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
|
|
578
|
-
|
|
579
|
-
|
|
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
|
-
|
|
620
|
+
await gitAsync(input.primaryPath, ["remote", "add", "origin", input.originUrl], "configure project origin");
|
|
582
621
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
605
|
-
|
|
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
|
-
|
|
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
|
-
|
|
654
|
+
await configureProjectBranchTrackingAsync(input.primaryPath, branchName, originBranchName);
|
|
616
655
|
}
|
|
617
656
|
}
|
|
618
657
|
}
|
|
619
|
-
function fetchProjectHeads(input) {
|
|
620
|
-
const originFetch =
|
|
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 =
|
|
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 (
|
|
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 (
|
|
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 =
|
|
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 (!
|
|
676
|
-
const head =
|
|
677
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ||
|
|
731
|
-
const currentHead =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
752
|
-
const output =
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
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
|
-
}
|
|
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
|
|
834
|
-
|
|
835
|
-
|
|
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 =
|
|
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) :
|
|
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 (!
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
1111
|
+
const observations = [];
|
|
1112
|
+
for (const branchName of [...input.branchNames].sort()) {
|
|
1046
1113
|
const checkoutPath = branchPath(input.projectRoot, branchName);
|
|
1047
|
-
const previousHead =
|
|
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 =
|
|
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 ||
|
|
1118
|
+
mirrorHead && mirrorHead !== previousHead && (input.allowNonFastForward || await tryGitAsync(checkoutPath, ["merge-base", "--is-ancestor", previousHead, mirrorHead]))
|
|
1052
1119
|
);
|
|
1053
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
1137
|
+
results.push({ branchName, previousHead: currentHead, mirrorHead, moved: false });
|
|
1138
|
+
continue;
|
|
1069
1139
|
}
|
|
1070
|
-
if (!
|
|
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
|
-
|
|
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())
|
|
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
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
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
|
}));
|