sync-worktrees 3.3.1 → 3.5.0
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.js +293 -107
- package/dist/index.js.map +4 -4
- package/dist/mcp-server.js +234 -81
- package/dist/mcp-server.js.map +3 -3
- package/package.json +3 -1
package/dist/mcp-server.js
CHANGED
|
@@ -5,7 +5,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
5
5
|
|
|
6
6
|
// src/mcp/context.ts
|
|
7
7
|
import * as fs7 from "fs/promises";
|
|
8
|
-
import * as
|
|
8
|
+
import * as path9 from "path";
|
|
9
9
|
import simpleGit5 from "simple-git";
|
|
10
10
|
|
|
11
11
|
// src/constants.ts
|
|
@@ -57,7 +57,11 @@ var DEFAULT_CONFIG = {
|
|
|
57
57
|
MAX_SAFE_TOTAL_CONCURRENT_OPS: 100
|
|
58
58
|
},
|
|
59
59
|
UPDATE_EXISTING_WORKTREES: true,
|
|
60
|
-
HOOK_TIMEOUT_MS: 6e4
|
|
60
|
+
HOOK_TIMEOUT_MS: 6e4,
|
|
61
|
+
FETCH_TIMEOUT_MS: 3e5,
|
|
62
|
+
CLONE_TIMEOUT_MS: 9e5,
|
|
63
|
+
LOCK_STALE_MS: 6e5,
|
|
64
|
+
LOCK_UPDATE_MS: 3e4
|
|
61
65
|
};
|
|
62
66
|
var ERROR_MESSAGES = {
|
|
63
67
|
GIT_NOT_INITIALIZED: "Git service not initialized. Call initialize() first.",
|
|
@@ -198,9 +202,9 @@ var WorktreeError = class extends SyncWorktreesError {
|
|
|
198
202
|
}
|
|
199
203
|
};
|
|
200
204
|
var WorktreeNotCleanError = class extends WorktreeError {
|
|
201
|
-
constructor(
|
|
202
|
-
super(`Worktree at '${
|
|
203
|
-
this.path =
|
|
205
|
+
constructor(path11, reasons) {
|
|
206
|
+
super(`Worktree at '${path11}' is not clean: ${reasons.join(", ")}`, "NOT_CLEAN");
|
|
207
|
+
this.path = path11;
|
|
204
208
|
this.reasons = reasons;
|
|
205
209
|
}
|
|
206
210
|
};
|
|
@@ -751,8 +755,9 @@ function defaultConsoleOutput(msg, level) {
|
|
|
751
755
|
|
|
752
756
|
// src/services/worktree-sync.service.ts
|
|
753
757
|
import * as fs6 from "fs/promises";
|
|
754
|
-
import * as
|
|
758
|
+
import * as path8 from "path";
|
|
755
759
|
import pLimit from "p-limit";
|
|
760
|
+
import * as lockfile from "proper-lockfile";
|
|
756
761
|
|
|
757
762
|
// src/utils/date-filter.ts
|
|
758
763
|
function parseDuration(durationStr) {
|
|
@@ -1002,7 +1007,7 @@ function formatTimingTable(totalDuration, phaseResults, repoName) {
|
|
|
1002
1007
|
|
|
1003
1008
|
// src/services/git.service.ts
|
|
1004
1009
|
import * as fs4 from "fs/promises";
|
|
1005
|
-
import * as
|
|
1010
|
+
import * as path6 from "path";
|
|
1006
1011
|
import simpleGit4 from "simple-git";
|
|
1007
1012
|
|
|
1008
1013
|
// src/utils/worktree-list-parser.ts
|
|
@@ -1047,11 +1052,13 @@ function parseWorktreeListPorcelain(output) {
|
|
|
1047
1052
|
}
|
|
1048
1053
|
|
|
1049
1054
|
// src/services/sparse-checkout.service.ts
|
|
1055
|
+
import * as path3 from "path";
|
|
1050
1056
|
import simpleGit from "simple-git";
|
|
1051
1057
|
var SparseCheckoutService = class {
|
|
1052
1058
|
logger;
|
|
1053
1059
|
gitFactory;
|
|
1054
1060
|
warnedConfigs = /* @__PURE__ */ new WeakSet();
|
|
1061
|
+
matcherCache = /* @__PURE__ */ new WeakMap();
|
|
1055
1062
|
constructor(logger, gitFactory) {
|
|
1056
1063
|
this.logger = logger ?? Logger.createDefault();
|
|
1057
1064
|
this.gitFactory = gitFactory ?? ((p) => simpleGit(p));
|
|
@@ -1135,11 +1142,66 @@ var SparseCheckoutService = class {
|
|
|
1135
1142
|
const bt = b.map((x) => x.trim());
|
|
1136
1143
|
return at.every((v, i) => v === bt[i]);
|
|
1137
1144
|
}
|
|
1145
|
+
/**
|
|
1146
|
+
* Decide whether a list of changed file paths intersects the sparse-checkout
|
|
1147
|
+
* set defined by `cfg`. Used to skip fast-forward updates when upstream
|
|
1148
|
+
* commits only touch files outside the materialized worktree.
|
|
1149
|
+
*
|
|
1150
|
+
* Cone mode materializes:
|
|
1151
|
+
* - all files at the repository root,
|
|
1152
|
+
* - all files directly inside every ancestor of an included directory
|
|
1153
|
+
* (e.g. include `tools/build` keeps `tools/foo.txt` checked out too),
|
|
1154
|
+
* - everything inside an included directory.
|
|
1155
|
+
* We mirror those rules here. Missing the ancestor-files case would let
|
|
1156
|
+
* stale files linger when only those parent files change upstream.
|
|
1157
|
+
*
|
|
1158
|
+
* No-cone mode: gitignore-style matching with negation is non-trivial and
|
|
1159
|
+
* not implemented here yet. We return `true` so the caller falls back to
|
|
1160
|
+
* the safe behavior of always running the update.
|
|
1161
|
+
*
|
|
1162
|
+
* The matcher derived from `cfg` is cached on the cfg object identity
|
|
1163
|
+
* (WeakMap), so callers should reuse the same `cfg` reference across
|
|
1164
|
+
* invocations to benefit from the cache.
|
|
1165
|
+
*/
|
|
1166
|
+
pathsTouchSparse(changedPaths, cfg) {
|
|
1167
|
+
if (changedPaths.length === 0) return false;
|
|
1168
|
+
const matcher = this.getMatcher(cfg);
|
|
1169
|
+
if (matcher.mode === "no-cone") return true;
|
|
1170
|
+
if (matcher.patterns.length === 0) return true;
|
|
1171
|
+
return changedPaths.some((p) => {
|
|
1172
|
+
if (!p.includes("/")) return true;
|
|
1173
|
+
for (const pat of matcher.patterns) {
|
|
1174
|
+
if (p === pat || p.startsWith(pat + "/")) return true;
|
|
1175
|
+
}
|
|
1176
|
+
return matcher.ancestorDirs.has(path3.posix.dirname(p));
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
getMatcher(cfg) {
|
|
1180
|
+
const cached = this.matcherCache.get(cfg);
|
|
1181
|
+
if (cached) return cached;
|
|
1182
|
+
const mode = this.resolveMode(cfg);
|
|
1183
|
+
if (mode === "no-cone") {
|
|
1184
|
+
const matcher2 = { mode, patterns: [], ancestorDirs: /* @__PURE__ */ new Set() };
|
|
1185
|
+
this.matcherCache.set(cfg, matcher2);
|
|
1186
|
+
return matcher2;
|
|
1187
|
+
}
|
|
1188
|
+
const patterns = cfg.include.map((p) => p.trim()).filter((p) => p.length > 0).map((p) => p.endsWith("/") ? p.slice(0, -1) : p);
|
|
1189
|
+
const ancestorDirs = /* @__PURE__ */ new Set();
|
|
1190
|
+
for (const pat of patterns) {
|
|
1191
|
+
const parts = pat.split("/");
|
|
1192
|
+
for (let i = 1; i < parts.length; i++) {
|
|
1193
|
+
ancestorDirs.add(parts.slice(0, i).join("/"));
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
const matcher = { mode, patterns, ancestorDirs };
|
|
1197
|
+
this.matcherCache.set(cfg, matcher);
|
|
1198
|
+
return matcher;
|
|
1199
|
+
}
|
|
1138
1200
|
};
|
|
1139
1201
|
|
|
1140
1202
|
// src/services/worktree-metadata.service.ts
|
|
1141
1203
|
import * as fs2 from "fs/promises";
|
|
1142
|
-
import * as
|
|
1204
|
+
import * as path4 from "path";
|
|
1143
1205
|
import simpleGit2 from "simple-git";
|
|
1144
1206
|
var WorktreeMetadataService = class {
|
|
1145
1207
|
logger;
|
|
@@ -1152,7 +1214,7 @@ var WorktreeMetadataService = class {
|
|
|
1152
1214
|
* For example: /worktrees/fix/test-branch -> test-branch (not fix/test-branch)
|
|
1153
1215
|
*/
|
|
1154
1216
|
getWorktreeDirectoryName(worktreePath) {
|
|
1155
|
-
return
|
|
1217
|
+
return path4.basename(worktreePath);
|
|
1156
1218
|
}
|
|
1157
1219
|
async getMetadataPath(bareRepoPath, worktreeName) {
|
|
1158
1220
|
if (worktreeName.includes("/") || worktreeName.includes("\\")) {
|
|
@@ -1160,7 +1222,7 @@ var WorktreeMetadataService = class {
|
|
|
1160
1222
|
`getMetadataPath requires a filesystem-safe worktree directory name, got '${worktreeName}'. Use getMetadataPathFromWorktreePath when starting from a raw branch name.`
|
|
1161
1223
|
);
|
|
1162
1224
|
}
|
|
1163
|
-
return
|
|
1225
|
+
return path4.join(
|
|
1164
1226
|
bareRepoPath,
|
|
1165
1227
|
METADATA_CONSTANTS.WORKTREE_METADATA_PATH,
|
|
1166
1228
|
worktreeName,
|
|
@@ -1173,7 +1235,7 @@ var WorktreeMetadataService = class {
|
|
|
1173
1235
|
}
|
|
1174
1236
|
async saveMetadata(bareRepoPath, worktreeName, metadata) {
|
|
1175
1237
|
const metadataPath = await this.getMetadataPath(bareRepoPath, worktreeName);
|
|
1176
|
-
await fs2.mkdir(
|
|
1238
|
+
await fs2.mkdir(path4.dirname(metadataPath), { recursive: true });
|
|
1177
1239
|
const tmpPath = `${metadataPath}.${process.pid}.${Date.now()}.tmp`;
|
|
1178
1240
|
let renamed = false;
|
|
1179
1241
|
try {
|
|
@@ -1366,7 +1428,7 @@ var WorktreeMetadataService = class {
|
|
|
1366
1428
|
|
|
1367
1429
|
// src/services/worktree-status.service.ts
|
|
1368
1430
|
import * as fs3 from "fs/promises";
|
|
1369
|
-
import * as
|
|
1431
|
+
import * as path5 from "path";
|
|
1370
1432
|
import simpleGit3 from "simple-git";
|
|
1371
1433
|
var OPERATION_FILES = [
|
|
1372
1434
|
{ file: GIT_OPERATIONS.MERGE_HEAD, type: "merge" },
|
|
@@ -1569,7 +1631,7 @@ var WorktreeStatusService = class {
|
|
|
1569
1631
|
async detectOperationFile(gitDir) {
|
|
1570
1632
|
const results = await Promise.all(
|
|
1571
1633
|
OPERATION_FILES.map(
|
|
1572
|
-
({ file }) => fs3.access(
|
|
1634
|
+
({ file }) => fs3.access(path5.join(gitDir, file)).then(
|
|
1573
1635
|
() => true,
|
|
1574
1636
|
() => false
|
|
1575
1637
|
)
|
|
@@ -1690,14 +1752,14 @@ var WorktreeStatusService = class {
|
|
|
1690
1752
|
}
|
|
1691
1753
|
}
|
|
1692
1754
|
async resolveGitDir(worktreePath) {
|
|
1693
|
-
const gitPath =
|
|
1755
|
+
const gitPath = path5.join(worktreePath, PATH_CONSTANTS.GIT_DIR);
|
|
1694
1756
|
try {
|
|
1695
1757
|
const stat4 = await fs3.stat(gitPath);
|
|
1696
1758
|
if (stat4.isFile()) {
|
|
1697
1759
|
const content = await fs3.readFile(gitPath, "utf-8");
|
|
1698
1760
|
const gitdirMatch = content.match(new RegExp(`^${GIT_CONSTANTS.GITDIR_PREFIX}\\s*(.+)$`, "m"));
|
|
1699
1761
|
if (gitdirMatch) {
|
|
1700
|
-
return
|
|
1762
|
+
return path5.resolve(worktreePath, gitdirMatch[1].trim());
|
|
1701
1763
|
}
|
|
1702
1764
|
throw new GitOperationError("resolve-git-dir", `Failed to parse gitdir from .git file at ${gitPath}`);
|
|
1703
1765
|
}
|
|
@@ -1711,7 +1773,7 @@ var WorktreeStatusService = class {
|
|
|
1711
1773
|
}
|
|
1712
1774
|
}
|
|
1713
1775
|
createGitInstance(worktreePath) {
|
|
1714
|
-
const key = `${
|
|
1776
|
+
const key = `${path5.resolve(worktreePath)}::${this.config.skipLfs ? "1" : "0"}`;
|
|
1715
1777
|
let git = this.gitInstances.get(key);
|
|
1716
1778
|
if (!git) {
|
|
1717
1779
|
git = this.config.skipLfs ? simpleGit3(worktreePath).env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit3(worktreePath);
|
|
@@ -1734,7 +1796,7 @@ var GitService = class {
|
|
|
1734
1796
|
this.config = config;
|
|
1735
1797
|
this.logger = logger ?? Logger.createDefault(void 0, config.debug);
|
|
1736
1798
|
this.bareRepoPath = this.config.bareRepoDir || getDefaultBareRepoDir(this.config.repoUrl);
|
|
1737
|
-
this.mainWorktreePath =
|
|
1799
|
+
this.mainWorktreePath = path6.join(this.config.worktreeDir, GIT_CONSTANTS.DEFAULT_BRANCH);
|
|
1738
1800
|
this.metadataService = new WorktreeMetadataService(this.logger);
|
|
1739
1801
|
this.statusService = new WorktreeStatusService({ skipLfs: this.config.skipLfs }, this.logger);
|
|
1740
1802
|
this.sparseCheckoutService = new SparseCheckoutService(this.logger);
|
|
@@ -1753,11 +1815,21 @@ var GitService = class {
|
|
|
1753
1815
|
getSparseCheckoutService() {
|
|
1754
1816
|
return this.sparseCheckoutService;
|
|
1755
1817
|
}
|
|
1818
|
+
getFetchTimeoutMs() {
|
|
1819
|
+
if (process.env.NODE_ENV === ENV_CONSTANTS.NODE_ENV_TEST) return 0;
|
|
1820
|
+
return this.config.fetchTimeoutMs ?? DEFAULT_CONFIG.FETCH_TIMEOUT_MS;
|
|
1821
|
+
}
|
|
1822
|
+
getCloneTimeoutMs() {
|
|
1823
|
+
if (process.env.NODE_ENV === ENV_CONSTANTS.NODE_ENV_TEST) return 0;
|
|
1824
|
+
return this.config.cloneTimeoutMs ?? DEFAULT_CONFIG.CLONE_TIMEOUT_MS;
|
|
1825
|
+
}
|
|
1756
1826
|
getCachedGit(dirPath, useLfsSkip = false) {
|
|
1757
|
-
const key = `${
|
|
1827
|
+
const key = `${path6.resolve(dirPath)}::${useLfsSkip ? "1" : "0"}`;
|
|
1758
1828
|
let git = this.gitInstances.get(key);
|
|
1759
1829
|
if (!git) {
|
|
1760
|
-
|
|
1830
|
+
const block = this.getFetchTimeoutMs();
|
|
1831
|
+
const base = block > 0 ? simpleGit4(dirPath, { timeout: { block } }) : simpleGit4(dirPath);
|
|
1832
|
+
git = useLfsSkip ? base.env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : base;
|
|
1761
1833
|
this.gitInstances.set(key, git);
|
|
1762
1834
|
}
|
|
1763
1835
|
return git;
|
|
@@ -1769,11 +1841,13 @@ var GitService = class {
|
|
|
1769
1841
|
async initialize() {
|
|
1770
1842
|
const { repoUrl } = this.config;
|
|
1771
1843
|
try {
|
|
1772
|
-
await fs4.access(
|
|
1844
|
+
await fs4.access(path6.join(this.bareRepoPath, "HEAD"));
|
|
1773
1845
|
} catch {
|
|
1774
1846
|
this.logger.info(`Cloning from "${repoUrl}" as bare repository into "${this.bareRepoPath}"...`);
|
|
1775
|
-
await fs4.mkdir(
|
|
1776
|
-
const
|
|
1847
|
+
await fs4.mkdir(path6.dirname(this.bareRepoPath), { recursive: true });
|
|
1848
|
+
const cloneBlock = this.getCloneTimeoutMs();
|
|
1849
|
+
const cloneBase = cloneBlock > 0 ? simpleGit4({ timeout: { block: cloneBlock } }) : simpleGit4();
|
|
1850
|
+
const cloneGit = this.isLfsSkipEnabled() ? cloneBase.env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : cloneBase;
|
|
1777
1851
|
await cloneGit.clone(repoUrl, this.bareRepoPath, ["--bare"]);
|
|
1778
1852
|
this.logger.info("\u2705 Clone successful.");
|
|
1779
1853
|
}
|
|
@@ -1790,17 +1864,17 @@ var GitService = class {
|
|
|
1790
1864
|
this.logger.info("Fetching remote branches...");
|
|
1791
1865
|
await bareGit.fetch(["--all"]);
|
|
1792
1866
|
this.defaultBranch = await this.detectDefaultBranch(bareGit);
|
|
1793
|
-
this.mainWorktreePath =
|
|
1867
|
+
this.mainWorktreePath = path6.join(this.config.worktreeDir, this.defaultBranch);
|
|
1794
1868
|
let needsMainWorktree = true;
|
|
1795
1869
|
try {
|
|
1796
1870
|
const worktrees = await this.getWorktreesFromBare(bareGit);
|
|
1797
|
-
needsMainWorktree = !worktrees.some((w) =>
|
|
1871
|
+
needsMainWorktree = !worktrees.some((w) => path6.resolve(w.path) === path6.resolve(this.mainWorktreePath));
|
|
1798
1872
|
} catch {
|
|
1799
1873
|
}
|
|
1800
1874
|
if (needsMainWorktree) {
|
|
1801
1875
|
this.logger.info(`Creating ${this.defaultBranch} worktree at "${this.mainWorktreePath}"...`);
|
|
1802
1876
|
await fs4.mkdir(this.config.worktreeDir, { recursive: true });
|
|
1803
|
-
const absoluteWorktreePath =
|
|
1877
|
+
const absoluteWorktreePath = path6.resolve(this.mainWorktreePath);
|
|
1804
1878
|
const branches = await bareGit.branch();
|
|
1805
1879
|
const defaultBranchExists = branches.all.includes(this.defaultBranch);
|
|
1806
1880
|
const useNoCheckoutMain = !!this.config.sparseCheckout;
|
|
@@ -1836,7 +1910,7 @@ var GitService = class {
|
|
|
1836
1910
|
}
|
|
1837
1911
|
const updatedWorktrees = await this.getWorktreesFromBare(bareGit);
|
|
1838
1912
|
const mainWorktreeRegistered = updatedWorktrees.some(
|
|
1839
|
-
(w) =>
|
|
1913
|
+
(w) => path6.resolve(w.path) === path6.resolve(this.mainWorktreePath)
|
|
1840
1914
|
);
|
|
1841
1915
|
if (!mainWorktreeRegistered) {
|
|
1842
1916
|
if (process.env.NODE_ENV !== ENV_CONSTANTS.NODE_ENV_TEST) {
|
|
@@ -1859,6 +1933,9 @@ var GitService = class {
|
|
|
1859
1933
|
getDefaultBranch() {
|
|
1860
1934
|
return this.defaultBranch;
|
|
1861
1935
|
}
|
|
1936
|
+
getBareRepoPath() {
|
|
1937
|
+
return this.bareRepoPath;
|
|
1938
|
+
}
|
|
1862
1939
|
async fetchAll() {
|
|
1863
1940
|
this.assertInitialized();
|
|
1864
1941
|
this.logger.info("Fetching latest data from remote...");
|
|
@@ -1916,7 +1993,7 @@ var GitService = class {
|
|
|
1916
1993
|
const existence = await Promise.all(
|
|
1917
1994
|
lfsFileList.map(async (f) => {
|
|
1918
1995
|
try {
|
|
1919
|
-
await fs4.access(
|
|
1996
|
+
await fs4.access(path6.join(worktreePath, f));
|
|
1920
1997
|
return f;
|
|
1921
1998
|
} catch {
|
|
1922
1999
|
return null;
|
|
@@ -1944,7 +2021,7 @@ var GitService = class {
|
|
|
1944
2021
|
let allDownloaded = true;
|
|
1945
2022
|
const notDownloaded = [];
|
|
1946
2023
|
for (const file of samplesToCheck) {
|
|
1947
|
-
const filePath =
|
|
2024
|
+
const filePath = path6.join(worktreePath, file);
|
|
1948
2025
|
try {
|
|
1949
2026
|
const handle = await fs4.open(filePath, "r");
|
|
1950
2027
|
try {
|
|
@@ -2033,12 +2110,12 @@ var GitService = class {
|
|
|
2033
2110
|
}
|
|
2034
2111
|
async addWorktree(branchName, worktreePath) {
|
|
2035
2112
|
const bareGit = this.getCachedGit(this.bareRepoPath, this.isLfsSkipEnabled());
|
|
2036
|
-
const absoluteWorktreePath =
|
|
2037
|
-
await fs4.mkdir(
|
|
2113
|
+
const absoluteWorktreePath = path6.resolve(worktreePath);
|
|
2114
|
+
await fs4.mkdir(path6.dirname(absoluteWorktreePath), { recursive: true });
|
|
2038
2115
|
try {
|
|
2039
2116
|
await fs4.access(absoluteWorktreePath);
|
|
2040
2117
|
const worktrees = await this.getWorktreesFromBare(bareGit);
|
|
2041
|
-
const isValidWorktree = worktrees.some((w) =>
|
|
2118
|
+
const isValidWorktree = worktrees.some((w) => path6.resolve(w.path) === absoluteWorktreePath);
|
|
2042
2119
|
if (isValidWorktree) {
|
|
2043
2120
|
this.logger.info(` - Worktree for '${branchName}' already exists at '${absoluteWorktreePath}'`);
|
|
2044
2121
|
return;
|
|
@@ -2083,7 +2160,7 @@ var GitService = class {
|
|
|
2083
2160
|
}
|
|
2084
2161
|
if (errorMessage.includes("already registered worktree")) {
|
|
2085
2162
|
const worktrees = await this.getWorktreesFromBare(bareGit);
|
|
2086
|
-
const existingWorktree = worktrees.find((w) =>
|
|
2163
|
+
const existingWorktree = worktrees.find((w) => path6.resolve(w.path) === absoluteWorktreePath);
|
|
2087
2164
|
if (existingWorktree && !existingWorktree.isPrunable) {
|
|
2088
2165
|
this.logger.info(` - Worktree for '${branchName}' was created by concurrent operation`);
|
|
2089
2166
|
return;
|
|
@@ -2129,7 +2206,7 @@ var GitService = class {
|
|
|
2129
2206
|
try {
|
|
2130
2207
|
await fs4.access(absoluteWorktreePath);
|
|
2131
2208
|
const worktrees = await this.getWorktreesFromBare(bareGit);
|
|
2132
|
-
const isValidWorktree = worktrees.some((w) =>
|
|
2209
|
+
const isValidWorktree = worktrees.some((w) => path6.resolve(w.path) === absoluteWorktreePath);
|
|
2133
2210
|
if (isValidWorktree) {
|
|
2134
2211
|
this.logger.info(` - Worktree for '${branchName}' already exists at '${absoluteWorktreePath}'`);
|
|
2135
2212
|
return;
|
|
@@ -2159,7 +2236,7 @@ var GitService = class {
|
|
|
2159
2236
|
const fallbackErrorMessage = getErrorMessage(fallbackError);
|
|
2160
2237
|
if (fallbackErrorMessage.includes("already registered worktree")) {
|
|
2161
2238
|
const worktrees = await this.getWorktreesFromBare(bareGit);
|
|
2162
|
-
const existingWorktree = worktrees.find((w) =>
|
|
2239
|
+
const existingWorktree = worktrees.find((w) => path6.resolve(w.path) === absoluteWorktreePath);
|
|
2163
2240
|
if (existingWorktree && !existingWorktree.isPrunable) {
|
|
2164
2241
|
this.logger.info(` - Worktree for '${branchName}' was created by concurrent operation during fallback`);
|
|
2165
2242
|
return;
|
|
@@ -2382,6 +2459,23 @@ var GitService = class {
|
|
|
2382
2459
|
return false;
|
|
2383
2460
|
}
|
|
2384
2461
|
}
|
|
2462
|
+
async getChangedPathsInRange(worktreePath, fromRef, toRef) {
|
|
2463
|
+
const worktreeGit = this.getCachedGit(worktreePath);
|
|
2464
|
+
try {
|
|
2465
|
+
const out = await worktreeGit.raw([
|
|
2466
|
+
"-c",
|
|
2467
|
+
"core.quotePath=false",
|
|
2468
|
+
"diff",
|
|
2469
|
+
"--name-only",
|
|
2470
|
+
"--no-renames",
|
|
2471
|
+
`${fromRef}..${toRef}`
|
|
2472
|
+
]);
|
|
2473
|
+
return out.split("\n").map((l) => l.replace(/\r$/, "")).filter((l) => l.length > 0);
|
|
2474
|
+
} catch (error) {
|
|
2475
|
+
this.logger.warn(`Failed to compute diff ${fromRef}..${toRef} in ${worktreePath}: ${getErrorMessage(error)}`);
|
|
2476
|
+
return null;
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2385
2479
|
async compareTreeContent(worktreePath, branch) {
|
|
2386
2480
|
const worktreeGit = this.getCachedGit(worktreePath);
|
|
2387
2481
|
try {
|
|
@@ -2466,7 +2560,7 @@ var GitService = class {
|
|
|
2466
2560
|
// src/services/path-resolution.service.ts
|
|
2467
2561
|
import { createHash } from "crypto";
|
|
2468
2562
|
import * as fs5 from "fs";
|
|
2469
|
-
import * as
|
|
2563
|
+
import * as path7 from "path";
|
|
2470
2564
|
var BRANCH_STEM_MAX = 80;
|
|
2471
2565
|
var BRANCH_HASH_LEN = 8;
|
|
2472
2566
|
var PathResolutionService = class {
|
|
@@ -2476,22 +2570,22 @@ var PathResolutionService = class {
|
|
|
2476
2570
|
return `${stem}-${hash}`;
|
|
2477
2571
|
}
|
|
2478
2572
|
getBranchWorktreePath(worktreeDir, branchName) {
|
|
2479
|
-
return
|
|
2573
|
+
return path7.join(worktreeDir, this.sanitizeBranchName(branchName));
|
|
2480
2574
|
}
|
|
2481
2575
|
resolveRealPath(inputPath) {
|
|
2482
|
-
const absolute =
|
|
2576
|
+
const absolute = path7.resolve(inputPath);
|
|
2483
2577
|
const missing = [];
|
|
2484
2578
|
let current = absolute;
|
|
2485
2579
|
while (!fs5.existsSync(current)) {
|
|
2486
|
-
const parent =
|
|
2580
|
+
const parent = path7.dirname(current);
|
|
2487
2581
|
if (parent === current) {
|
|
2488
2582
|
return absolute;
|
|
2489
2583
|
}
|
|
2490
|
-
missing.unshift(
|
|
2584
|
+
missing.unshift(path7.basename(current));
|
|
2491
2585
|
current = parent;
|
|
2492
2586
|
}
|
|
2493
2587
|
try {
|
|
2494
|
-
return
|
|
2588
|
+
return path7.join(fs5.realpathSync(current), ...missing);
|
|
2495
2589
|
} catch {
|
|
2496
2590
|
return absolute;
|
|
2497
2591
|
}
|
|
@@ -2501,7 +2595,7 @@ var PathResolutionService = class {
|
|
|
2501
2595
|
const a = fold(resolved);
|
|
2502
2596
|
const b = fold(resolvedBase);
|
|
2503
2597
|
if (a === b) return true;
|
|
2504
|
-
return a.length > b.length && a.charAt(b.length) ===
|
|
2598
|
+
return a.length > b.length && a.charAt(b.length) === path7.sep && a.startsWith(b);
|
|
2505
2599
|
}
|
|
2506
2600
|
normalizeWorktreePath(worktreePath, worktreeBaseDir) {
|
|
2507
2601
|
const resolved = this.resolveRealPath(worktreePath);
|
|
@@ -2509,7 +2603,7 @@ var PathResolutionService = class {
|
|
|
2509
2603
|
if (!this.isResolvedPathInsideBase(resolved, resolvedBase)) {
|
|
2510
2604
|
throw new Error(`Worktree path '${worktreePath}' is outside base directory '${worktreeBaseDir}'`);
|
|
2511
2605
|
}
|
|
2512
|
-
return
|
|
2606
|
+
return path7.relative(resolvedBase, resolved);
|
|
2513
2607
|
}
|
|
2514
2608
|
isPathInsideBaseDir(targetPath, baseDir) {
|
|
2515
2609
|
const resolved = this.resolveRealPath(targetPath);
|
|
@@ -2568,6 +2662,11 @@ var WorktreeSyncService = class {
|
|
|
2568
2662
|
this.logger.warn("\u26A0\uFE0F Sync already in progress, skipping...");
|
|
2569
2663
|
return { started: false, reason: "in_progress" };
|
|
2570
2664
|
}
|
|
2665
|
+
const release = await this.acquireBareLock();
|
|
2666
|
+
if (release === null) {
|
|
2667
|
+
this.logger.warn("\u26A0\uFE0F Another process holds the sync lock for this repo, skipping...");
|
|
2668
|
+
return { started: false, reason: "locked" };
|
|
2669
|
+
}
|
|
2571
2670
|
this.syncInProgress = true;
|
|
2572
2671
|
this.logger.info(`[${(/* @__PURE__ */ new Date()).toISOString()}] Starting worktree synchronization...`);
|
|
2573
2672
|
const totalTimer = new Timer();
|
|
@@ -2584,6 +2683,11 @@ var WorktreeSyncService = class {
|
|
|
2584
2683
|
this.gitService.setLfsSkipEnabled(false);
|
|
2585
2684
|
}
|
|
2586
2685
|
this.syncInProgress = false;
|
|
2686
|
+
try {
|
|
2687
|
+
await release();
|
|
2688
|
+
} catch (releaseError) {
|
|
2689
|
+
this.logger.warn(`Failed to release sync lock: ${getErrorMessage(releaseError)}`);
|
|
2690
|
+
}
|
|
2587
2691
|
this.logger.info(`[${(/* @__PURE__ */ new Date()).toISOString()}] Synchronization finished.
|
|
2588
2692
|
`);
|
|
2589
2693
|
if (this.config.debug) {
|
|
@@ -2595,6 +2699,39 @@ var WorktreeSyncService = class {
|
|
|
2595
2699
|
}
|
|
2596
2700
|
return { started: true };
|
|
2597
2701
|
}
|
|
2702
|
+
async acquireBareLock() {
|
|
2703
|
+
if (process.env.NODE_ENV === ENV_CONSTANTS.NODE_ENV_TEST) {
|
|
2704
|
+
return async () => {
|
|
2705
|
+
};
|
|
2706
|
+
}
|
|
2707
|
+
if (typeof this.gitService.getBareRepoPath !== "function") {
|
|
2708
|
+
return async () => {
|
|
2709
|
+
};
|
|
2710
|
+
}
|
|
2711
|
+
const barePath = this.gitService.getBareRepoPath();
|
|
2712
|
+
const lockTarget = path8.join(barePath, "HEAD");
|
|
2713
|
+
try {
|
|
2714
|
+
await fs6.access(lockTarget);
|
|
2715
|
+
} catch {
|
|
2716
|
+
return async () => {
|
|
2717
|
+
};
|
|
2718
|
+
}
|
|
2719
|
+
try {
|
|
2720
|
+
const release = await lockfile.lock(lockTarget, {
|
|
2721
|
+
stale: DEFAULT_CONFIG.LOCK_STALE_MS,
|
|
2722
|
+
update: DEFAULT_CONFIG.LOCK_UPDATE_MS,
|
|
2723
|
+
retries: 0,
|
|
2724
|
+
realpath: false
|
|
2725
|
+
});
|
|
2726
|
+
return release;
|
|
2727
|
+
} catch (error) {
|
|
2728
|
+
const code = error.code;
|
|
2729
|
+
if (code === "ELOCKED") {
|
|
2730
|
+
return null;
|
|
2731
|
+
}
|
|
2732
|
+
throw error;
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2598
2735
|
createRetryOptions(syncContext) {
|
|
2599
2736
|
return {
|
|
2600
2737
|
maxAttempts: this.config.retry?.maxAttempts ?? 3,
|
|
@@ -2770,12 +2907,12 @@ var WorktreeSyncService = class {
|
|
|
2770
2907
|
}
|
|
2771
2908
|
const reservedPaths = /* @__PURE__ */ new Map();
|
|
2772
2909
|
for (const w of worktrees) {
|
|
2773
|
-
reservedPaths.set(
|
|
2910
|
+
reservedPaths.set(path8.resolve(w.path), w.branch);
|
|
2774
2911
|
}
|
|
2775
2912
|
const plan = [];
|
|
2776
2913
|
for (const branchName of newBranches) {
|
|
2777
2914
|
const worktreePath = this.pathResolution.getBranchWorktreePath(this.config.worktreeDir, branchName);
|
|
2778
|
-
const resolved =
|
|
2915
|
+
const resolved = path8.resolve(worktreePath);
|
|
2779
2916
|
const conflict = reservedPaths.get(resolved);
|
|
2780
2917
|
if (conflict && conflict !== branchName) {
|
|
2781
2918
|
this.logger.error(
|
|
@@ -2980,12 +3117,12 @@ var WorktreeSyncService = class {
|
|
|
2980
3117
|
}
|
|
2981
3118
|
async updateExistingWorktrees(worktrees, remoteBranches) {
|
|
2982
3119
|
this.logger.info("Step 4: Checking for worktrees that need updates...");
|
|
2983
|
-
const divergedDir =
|
|
3120
|
+
const divergedDir = path8.join(this.config.worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
|
|
2984
3121
|
try {
|
|
2985
3122
|
const diverged = await fs6.readdir(divergedDir);
|
|
2986
3123
|
if (diverged.length > 0) {
|
|
2987
3124
|
this.logger.info(
|
|
2988
|
-
`\u{1F4E6} Note: ${diverged.length} diverged worktree(s) in ${
|
|
3125
|
+
`\u{1F4E6} Note: ${diverged.length} diverged worktree(s) in ${path8.relative(process.cwd(), divergedDir)}`
|
|
2989
3126
|
);
|
|
2990
3127
|
}
|
|
2991
3128
|
} catch {
|
|
@@ -3015,7 +3152,23 @@ var WorktreeSyncService = class {
|
|
|
3015
3152
|
return { action: "diverged", worktree };
|
|
3016
3153
|
}
|
|
3017
3154
|
const isBehind = await this.gitService.isWorktreeBehind(worktree.path);
|
|
3018
|
-
|
|
3155
|
+
if (!isBehind) return null;
|
|
3156
|
+
const sparseCfg = this.config.sparseCheckout;
|
|
3157
|
+
if (sparseCfg && sparseCfg.skipUpdateWhenOutsideSparse !== false) {
|
|
3158
|
+
const sparseService = this.gitService.getSparseCheckoutService();
|
|
3159
|
+
if (sparseService.resolveMode(sparseCfg) === "cone") {
|
|
3160
|
+
const diff = await this.gitService.getChangedPathsInRange(
|
|
3161
|
+
worktree.path,
|
|
3162
|
+
"HEAD",
|
|
3163
|
+
`origin/${worktree.branch}`
|
|
3164
|
+
);
|
|
3165
|
+
if (diff !== null && !sparseService.pathsTouchSparse(diff, sparseCfg)) {
|
|
3166
|
+
this.logger.info(`\u23ED\uFE0F Skipping '${worktree.branch}' - upstream changes outside sparse paths`);
|
|
3167
|
+
return null;
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
return { action: "update", worktree };
|
|
3019
3172
|
})
|
|
3020
3173
|
)
|
|
3021
3174
|
);
|
|
@@ -3093,13 +3246,13 @@ var WorktreeSyncService = class {
|
|
|
3093
3246
|
}
|
|
3094
3247
|
async cleanupOrphanedDirectories(worktrees) {
|
|
3095
3248
|
try {
|
|
3096
|
-
const worktreeRelativePaths = worktrees.map((w) =>
|
|
3249
|
+
const worktreeRelativePaths = worktrees.map((w) => path8.relative(this.config.worktreeDir, w.path));
|
|
3097
3250
|
const allDirs = await fs6.readdir(this.config.worktreeDir);
|
|
3098
3251
|
const regularDirs = allDirs.filter((dir) => !dir.startsWith("."));
|
|
3099
3252
|
const orphanedDirs = [];
|
|
3100
3253
|
for (const dir of regularDirs) {
|
|
3101
3254
|
const isPartOfWorktree = worktreeRelativePaths.some((worktreePath) => {
|
|
3102
|
-
return worktreePath === dir || worktreePath.startsWith(dir +
|
|
3255
|
+
return worktreePath === dir || worktreePath.startsWith(dir + path8.sep);
|
|
3103
3256
|
});
|
|
3104
3257
|
if (!isPartOfWorktree) {
|
|
3105
3258
|
orphanedDirs.push(dir);
|
|
@@ -3108,7 +3261,7 @@ var WorktreeSyncService = class {
|
|
|
3108
3261
|
if (orphanedDirs.length > 0) {
|
|
3109
3262
|
this.logger.info(`Found ${orphanedDirs.length} orphaned directories: ${orphanedDirs.join(", ")}`);
|
|
3110
3263
|
for (const dir of orphanedDirs) {
|
|
3111
|
-
const dirPath =
|
|
3264
|
+
const dirPath = path8.join(this.config.worktreeDir, dir);
|
|
3112
3265
|
try {
|
|
3113
3266
|
const stat4 = await fs6.stat(dirPath);
|
|
3114
3267
|
if (stat4.isDirectory()) {
|
|
@@ -3142,7 +3295,7 @@ var WorktreeSyncService = class {
|
|
|
3142
3295
|
} else {
|
|
3143
3296
|
this.logger.info(`\u{1F512} Branch '${worktree.branch}' has diverged with local changes. Moving to diverged...`);
|
|
3144
3297
|
const divergedPath = await this.divergeWorktree(worktree.path, worktree.branch);
|
|
3145
|
-
const relativePath =
|
|
3298
|
+
const relativePath = path8.relative(process.cwd(), divergedPath);
|
|
3146
3299
|
this.logger.info(` Moved to: ${relativePath}`);
|
|
3147
3300
|
this.logger.info(` Your local changes are preserved. To review:`);
|
|
3148
3301
|
this.logger.info(` cd ${relativePath}`);
|
|
@@ -3166,12 +3319,12 @@ var WorktreeSyncService = class {
|
|
|
3166
3319
|
}
|
|
3167
3320
|
}
|
|
3168
3321
|
async divergeWorktree(worktreePath, branchName) {
|
|
3169
|
-
const divergedBaseDir =
|
|
3322
|
+
const divergedBaseDir = path8.join(this.config.worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
|
|
3170
3323
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
3171
3324
|
const uniqueSuffix = Date.now().toString(36) + Math.random().toString(36).substring(2, 7);
|
|
3172
3325
|
const safeBranchName = this.pathResolution.sanitizeBranchName(branchName);
|
|
3173
3326
|
const divergedName = `${timestamp}-${safeBranchName}-${uniqueSuffix}`;
|
|
3174
|
-
const divergedPath =
|
|
3327
|
+
const divergedPath = path8.join(divergedBaseDir, divergedName);
|
|
3175
3328
|
await fs6.mkdir(divergedBaseDir, { recursive: true });
|
|
3176
3329
|
try {
|
|
3177
3330
|
await fs6.rename(worktreePath, divergedPath);
|
|
@@ -3198,7 +3351,7 @@ var WorktreeSyncService = class {
|
|
|
3198
3351
|
Original worktree location: ${worktreePath}`
|
|
3199
3352
|
};
|
|
3200
3353
|
await fs6.writeFile(
|
|
3201
|
-
|
|
3354
|
+
path8.join(divergedPath, METADATA_CONSTANTS.DIVERGED_INFO_FILE),
|
|
3202
3355
|
JSON.stringify(metadata, null, 2)
|
|
3203
3356
|
);
|
|
3204
3357
|
return divergedPath;
|
|
@@ -3253,9 +3406,9 @@ var RepositoryContext = class {
|
|
|
3253
3406
|
discoveryCache = /* @__PURE__ */ new Map();
|
|
3254
3407
|
async loadConfig(configPath, options = {}) {
|
|
3255
3408
|
const setDefaultCurrent = options.setDefaultCurrent ?? true;
|
|
3256
|
-
const absolutePath =
|
|
3409
|
+
const absolutePath = path9.resolve(configPath);
|
|
3257
3410
|
const configFile = await this.configLoader.loadConfigFile(absolutePath);
|
|
3258
|
-
const configDir =
|
|
3411
|
+
const configDir = path9.dirname(absolutePath);
|
|
3259
3412
|
const globalDefaults = configFile.defaults;
|
|
3260
3413
|
const resolvedAll = [];
|
|
3261
3414
|
for (const repo of configFile.repositories) {
|
|
@@ -3292,7 +3445,7 @@ var RepositoryContext = class {
|
|
|
3292
3445
|
return configFile.repositories;
|
|
3293
3446
|
}
|
|
3294
3447
|
async detectFromPath(dirPath) {
|
|
3295
|
-
const absolutePath =
|
|
3448
|
+
const absolutePath = path9.resolve(dirPath);
|
|
3296
3449
|
const cached = this.discoveryCache.get(absolutePath);
|
|
3297
3450
|
if (cached && await this.isCacheFresh(cached)) {
|
|
3298
3451
|
return cached.result;
|
|
@@ -3311,8 +3464,8 @@ var RepositoryContext = class {
|
|
|
3311
3464
|
const { result, adminDir } = await this.detectFromPathUncached(absolutePath);
|
|
3312
3465
|
if (result.isWorktree && result.bareRepoPath && adminDir) {
|
|
3313
3466
|
const [worktreeHeadMtimeMs, worktreesDirMtimeMs] = await Promise.all([
|
|
3314
|
-
safeMtimeMs(
|
|
3315
|
-
safeMtimeMs(
|
|
3467
|
+
safeMtimeMs(path9.join(adminDir, "HEAD")),
|
|
3468
|
+
safeMtimeMs(path9.join(result.bareRepoPath, "worktrees"))
|
|
3316
3469
|
]);
|
|
3317
3470
|
this.discoveryCache.set(absolutePath, {
|
|
3318
3471
|
result,
|
|
@@ -3344,8 +3497,8 @@ var RepositoryContext = class {
|
|
|
3344
3497
|
return this.discoveryCache.size;
|
|
3345
3498
|
}
|
|
3346
3499
|
async discoverSiblingRepositories(currentBareRepoPath) {
|
|
3347
|
-
const repoDir =
|
|
3348
|
-
const workspaceRoot =
|
|
3500
|
+
const repoDir = path9.dirname(currentBareRepoPath);
|
|
3501
|
+
const workspaceRoot = path9.dirname(repoDir);
|
|
3349
3502
|
if (workspaceRoot === repoDir) return [];
|
|
3350
3503
|
let entries;
|
|
3351
3504
|
try {
|
|
@@ -3362,15 +3515,15 @@ var RepositoryContext = class {
|
|
|
3362
3515
|
const results = [];
|
|
3363
3516
|
await Promise.all(
|
|
3364
3517
|
entries.map(async (entry) => {
|
|
3365
|
-
const candidate =
|
|
3366
|
-
const bareCandidate =
|
|
3518
|
+
const candidate = path9.join(workspaceRoot, entry);
|
|
3519
|
+
const bareCandidate = path9.join(candidate, GIT_CONSTANTS.BARE_DIR_NAME);
|
|
3367
3520
|
try {
|
|
3368
3521
|
const stat4 = await fs7.stat(bareCandidate);
|
|
3369
3522
|
if (!stat4.isDirectory()) return;
|
|
3370
3523
|
} catch {
|
|
3371
3524
|
return;
|
|
3372
3525
|
}
|
|
3373
|
-
const resolvedBare =
|
|
3526
|
+
const resolvedBare = path9.resolve(bareCandidate);
|
|
3374
3527
|
const matchedName = configBares.get(normalizePathForCompare(resolvedBare));
|
|
3375
3528
|
results.push({
|
|
3376
3529
|
name: matchedName ?? entry,
|
|
@@ -3392,8 +3545,8 @@ var RepositoryContext = class {
|
|
|
3392
3545
|
if (Date.now() - cached.cachedAt >= DISCOVERY_CACHE_TTL_MS) return false;
|
|
3393
3546
|
if (!cached.worktreeAdminDir || !cached.result.bareRepoPath) return true;
|
|
3394
3547
|
const [currentHeadMtime, currentWorktreesDirMtime] = await Promise.all([
|
|
3395
|
-
safeMtimeMs(
|
|
3396
|
-
safeMtimeMs(
|
|
3548
|
+
safeMtimeMs(path9.join(cached.worktreeAdminDir, "HEAD")),
|
|
3549
|
+
safeMtimeMs(path9.join(cached.result.bareRepoPath, "worktrees"))
|
|
3397
3550
|
]);
|
|
3398
3551
|
return currentHeadMtime === cached.worktreeHeadMtimeMs && currentWorktreesDirMtime === cached.worktreesDirMtimeMs;
|
|
3399
3552
|
}
|
|
@@ -3434,13 +3587,13 @@ var RepositoryContext = class {
|
|
|
3434
3587
|
return unsupported("Invalid .git file format (missing gitdir line)");
|
|
3435
3588
|
}
|
|
3436
3589
|
const gitdir = gitdirMatch[1].trim();
|
|
3437
|
-
const resolvedGitdir =
|
|
3590
|
+
const resolvedGitdir = path9.isAbsolute(gitdir) ? gitdir : path9.resolve(worktreeRoot, gitdir);
|
|
3438
3591
|
const worktreesMatch = resolvedGitdir.match(/^(.+?)[/\\]worktrees[/\\][^/\\]+$/);
|
|
3439
3592
|
if (!worktreesMatch) {
|
|
3440
3593
|
return unsupported("gitdir does not follow worktree structure (missing /worktrees/<name>)");
|
|
3441
3594
|
}
|
|
3442
|
-
const bareRepoPath =
|
|
3443
|
-
const adminDir =
|
|
3595
|
+
const bareRepoPath = path9.resolve(worktreesMatch[1]);
|
|
3596
|
+
const adminDir = path9.resolve(resolvedGitdir);
|
|
3444
3597
|
let repoUrl = null;
|
|
3445
3598
|
let worktrees = [];
|
|
3446
3599
|
let currentBranch = null;
|
|
@@ -3481,7 +3634,7 @@ var RepositoryContext = class {
|
|
|
3481
3634
|
adminDir
|
|
3482
3635
|
};
|
|
3483
3636
|
}
|
|
3484
|
-
const worktreeDir =
|
|
3637
|
+
const worktreeDir = path9.dirname(worktreeRoot);
|
|
3485
3638
|
const noUrlReason = "no remote origin URL detected";
|
|
3486
3639
|
const capabilities = {
|
|
3487
3640
|
listWorktrees: { available: true },
|
|
@@ -3517,7 +3670,7 @@ var RepositoryContext = class {
|
|
|
3517
3670
|
cronSchedule: DEFAULT_CONFIG.CRON_SCHEDULE,
|
|
3518
3671
|
runOnce: true
|
|
3519
3672
|
};
|
|
3520
|
-
const detectedKey = `${AUTO_DETECT_PREFIX}${
|
|
3673
|
+
const detectedKey = `${AUTO_DETECT_PREFIX}${path9.basename(bareRepoPath)}@${bareRepoPath}`;
|
|
3521
3674
|
if (!this.repos.has(detectedKey)) {
|
|
3522
3675
|
this.repos.set(detectedKey, {
|
|
3523
3676
|
name: detectedKey,
|
|
@@ -3609,7 +3762,7 @@ function parseWorktreeList(output, currentPath) {
|
|
|
3609
3762
|
const foldedCurrent = normalizePathForCompare(currentPath);
|
|
3610
3763
|
const results = [];
|
|
3611
3764
|
for (const wt of parseWorktreeListPorcelain(output)) {
|
|
3612
|
-
const resolved =
|
|
3765
|
+
const resolved = path9.resolve(wt.path);
|
|
3613
3766
|
const branch = wt.branch ?? (wt.detached ? `(detached ${(wt.head ?? "").slice(0, 7)})` : null);
|
|
3614
3767
|
if (!branch) continue;
|
|
3615
3768
|
results.push({
|
|
@@ -3629,10 +3782,10 @@ async function safeMtimeMs(filePath) {
|
|
|
3629
3782
|
}
|
|
3630
3783
|
}
|
|
3631
3784
|
async function findWorktreeRoot(startPath) {
|
|
3632
|
-
let current =
|
|
3633
|
-
const root =
|
|
3785
|
+
let current = path9.resolve(startPath);
|
|
3786
|
+
const root = path9.parse(current).root;
|
|
3634
3787
|
while (true) {
|
|
3635
|
-
const gitPath =
|
|
3788
|
+
const gitPath = path9.join(current, ".git");
|
|
3636
3789
|
try {
|
|
3637
3790
|
const content = await fs7.readFile(gitPath, "utf-8");
|
|
3638
3791
|
return { kind: "worktree-file", worktreeRoot: current, gitFileContent: content };
|
|
@@ -3646,7 +3799,7 @@ async function findWorktreeRoot(startPath) {
|
|
|
3646
3799
|
}
|
|
3647
3800
|
}
|
|
3648
3801
|
if (current === root) return null;
|
|
3649
|
-
const parent =
|
|
3802
|
+
const parent = path9.dirname(current);
|
|
3650
3803
|
if (parent === current) return null;
|
|
3651
3804
|
current = parent;
|
|
3652
3805
|
}
|
|
@@ -3657,7 +3810,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
3657
3810
|
import { z } from "zod";
|
|
3658
3811
|
|
|
3659
3812
|
// src/mcp/handlers.ts
|
|
3660
|
-
import * as
|
|
3813
|
+
import * as path10 from "path";
|
|
3661
3814
|
import pLimit2 from "p-limit";
|
|
3662
3815
|
|
|
3663
3816
|
// src/utils/disk-space.ts
|
|
@@ -3854,7 +4007,7 @@ async function getReadyService(ctx, repoName, options = {}) {
|
|
|
3854
4007
|
}
|
|
3855
4008
|
async function ensureRepoWorktreePath(ctx, params, git) {
|
|
3856
4009
|
await ensurePathBelongsToRepo(ctx, params.path, params.repoName, git);
|
|
3857
|
-
return
|
|
4010
|
+
return path10.resolve(params.path);
|
|
3858
4011
|
}
|
|
3859
4012
|
async function ensurePathBelongsToRepo(ctx, targetPath, repoName, git) {
|
|
3860
4013
|
const discovered = ctx.getDiscoveredContext(repoName);
|
|
@@ -3915,7 +4068,7 @@ async function handleListWorktrees(ctx, params, _extra) {
|
|
|
3915
4068
|
const results = await Promise.all(
|
|
3916
4069
|
worktrees.map(
|
|
3917
4070
|
(wt) => limit(async () => {
|
|
3918
|
-
const resolvedPath =
|
|
4071
|
+
const resolvedPath = path10.resolve(wt.path);
|
|
3919
4072
|
const isCurrent = currentPath !== null && pathsEqual(wt.path, currentPath);
|
|
3920
4073
|
const [status, divergence, metadata, sizeBytes] = await Promise.all([
|
|
3921
4074
|
git.getFullWorktreeStatus(wt.path, false).catch(() => null),
|
|
@@ -3995,7 +4148,7 @@ async function handleCreateWorktree(ctx, params, _extra) {
|
|
|
3995
4148
|
return formatToolResponse({
|
|
3996
4149
|
success: true,
|
|
3997
4150
|
branchName,
|
|
3998
|
-
worktreePath:
|
|
4151
|
+
worktreePath: path10.resolve(worktreePath),
|
|
3999
4152
|
created,
|
|
4000
4153
|
pushed
|
|
4001
4154
|
});
|
|
@@ -4083,7 +4236,7 @@ async function handleLoadConfig(ctx, params, _extra) {
|
|
|
4083
4236
|
}
|
|
4084
4237
|
await ctx.loadConfig(configPath);
|
|
4085
4238
|
return formatToolResponse({
|
|
4086
|
-
configPath:
|
|
4239
|
+
configPath: path10.resolve(configPath),
|
|
4087
4240
|
currentRepository: ctx.getCurrentRepo(),
|
|
4088
4241
|
repositories: ctx.getRepositoryList()
|
|
4089
4242
|
});
|