sync-worktrees 3.4.0 → 3.6.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 +257 -114
- package/dist/index.js.map +4 -4
- package/dist/mcp-server.js +198 -88
- package/dist/mcp-server.js.map +3 -3
- package/package.json +1 -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
|
|
@@ -27,7 +27,8 @@ var GIT_CONSTANTS = {
|
|
|
27
27
|
REMOTES: "refs/remotes/origin",
|
|
28
28
|
REMOTES_ORIGIN: "refs/remotes/origin/*"
|
|
29
29
|
},
|
|
30
|
-
FETCH_CONFIG: "+refs/heads/*:refs/remotes/origin/*"
|
|
30
|
+
FETCH_CONFIG: "+refs/heads/*:refs/remotes/origin/*",
|
|
31
|
+
PROGRESS_BUCKET_PERCENT: 25
|
|
31
32
|
};
|
|
32
33
|
var GIT_OPERATIONS = {
|
|
33
34
|
MERGE_HEAD: "MERGE_HEAD",
|
|
@@ -202,9 +203,9 @@ var WorktreeError = class extends SyncWorktreesError {
|
|
|
202
203
|
}
|
|
203
204
|
};
|
|
204
205
|
var WorktreeNotCleanError = class extends WorktreeError {
|
|
205
|
-
constructor(
|
|
206
|
-
super(`Worktree at '${
|
|
207
|
-
this.path =
|
|
206
|
+
constructor(path11, reasons) {
|
|
207
|
+
super(`Worktree at '${path11}' is not clean: ${reasons.join(", ")}`, "NOT_CLEAN");
|
|
208
|
+
this.path = path11;
|
|
208
209
|
this.reasons = reasons;
|
|
209
210
|
}
|
|
210
211
|
};
|
|
@@ -755,7 +756,7 @@ function defaultConsoleOutput(msg, level) {
|
|
|
755
756
|
|
|
756
757
|
// src/services/worktree-sync.service.ts
|
|
757
758
|
import * as fs6 from "fs/promises";
|
|
758
|
-
import * as
|
|
759
|
+
import * as path8 from "path";
|
|
759
760
|
import pLimit from "p-limit";
|
|
760
761
|
import * as lockfile from "proper-lockfile";
|
|
761
762
|
|
|
@@ -1007,7 +1008,7 @@ function formatTimingTable(totalDuration, phaseResults, repoName) {
|
|
|
1007
1008
|
|
|
1008
1009
|
// src/services/git.service.ts
|
|
1009
1010
|
import * as fs4 from "fs/promises";
|
|
1010
|
-
import * as
|
|
1011
|
+
import * as path6 from "path";
|
|
1011
1012
|
import simpleGit4 from "simple-git";
|
|
1012
1013
|
|
|
1013
1014
|
// src/utils/worktree-list-parser.ts
|
|
@@ -1052,11 +1053,13 @@ function parseWorktreeListPorcelain(output) {
|
|
|
1052
1053
|
}
|
|
1053
1054
|
|
|
1054
1055
|
// src/services/sparse-checkout.service.ts
|
|
1056
|
+
import * as path3 from "path";
|
|
1055
1057
|
import simpleGit from "simple-git";
|
|
1056
1058
|
var SparseCheckoutService = class {
|
|
1057
1059
|
logger;
|
|
1058
1060
|
gitFactory;
|
|
1059
1061
|
warnedConfigs = /* @__PURE__ */ new WeakSet();
|
|
1062
|
+
matcherCache = /* @__PURE__ */ new WeakMap();
|
|
1060
1063
|
constructor(logger, gitFactory) {
|
|
1061
1064
|
this.logger = logger ?? Logger.createDefault();
|
|
1062
1065
|
this.gitFactory = gitFactory ?? ((p) => simpleGit(p));
|
|
@@ -1140,11 +1143,66 @@ var SparseCheckoutService = class {
|
|
|
1140
1143
|
const bt = b.map((x) => x.trim());
|
|
1141
1144
|
return at.every((v, i) => v === bt[i]);
|
|
1142
1145
|
}
|
|
1146
|
+
/**
|
|
1147
|
+
* Decide whether a list of changed file paths intersects the sparse-checkout
|
|
1148
|
+
* set defined by `cfg`. Used to skip fast-forward updates when upstream
|
|
1149
|
+
* commits only touch files outside the materialized worktree.
|
|
1150
|
+
*
|
|
1151
|
+
* Cone mode materializes:
|
|
1152
|
+
* - all files at the repository root,
|
|
1153
|
+
* - all files directly inside every ancestor of an included directory
|
|
1154
|
+
* (e.g. include `tools/build` keeps `tools/foo.txt` checked out too),
|
|
1155
|
+
* - everything inside an included directory.
|
|
1156
|
+
* We mirror those rules here. Missing the ancestor-files case would let
|
|
1157
|
+
* stale files linger when only those parent files change upstream.
|
|
1158
|
+
*
|
|
1159
|
+
* No-cone mode: gitignore-style matching with negation is non-trivial and
|
|
1160
|
+
* not implemented here yet. We return `true` so the caller falls back to
|
|
1161
|
+
* the safe behavior of always running the update.
|
|
1162
|
+
*
|
|
1163
|
+
* The matcher derived from `cfg` is cached on the cfg object identity
|
|
1164
|
+
* (WeakMap), so callers should reuse the same `cfg` reference across
|
|
1165
|
+
* invocations to benefit from the cache.
|
|
1166
|
+
*/
|
|
1167
|
+
pathsTouchSparse(changedPaths, cfg) {
|
|
1168
|
+
if (changedPaths.length === 0) return false;
|
|
1169
|
+
const matcher = this.getMatcher(cfg);
|
|
1170
|
+
if (matcher.mode === "no-cone") return true;
|
|
1171
|
+
if (matcher.patterns.length === 0) return true;
|
|
1172
|
+
return changedPaths.some((p) => {
|
|
1173
|
+
if (!p.includes("/")) return true;
|
|
1174
|
+
for (const pat of matcher.patterns) {
|
|
1175
|
+
if (p === pat || p.startsWith(pat + "/")) return true;
|
|
1176
|
+
}
|
|
1177
|
+
return matcher.ancestorDirs.has(path3.posix.dirname(p));
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
getMatcher(cfg) {
|
|
1181
|
+
const cached = this.matcherCache.get(cfg);
|
|
1182
|
+
if (cached) return cached;
|
|
1183
|
+
const mode = this.resolveMode(cfg);
|
|
1184
|
+
if (mode === "no-cone") {
|
|
1185
|
+
const matcher2 = { mode, patterns: [], ancestorDirs: /* @__PURE__ */ new Set() };
|
|
1186
|
+
this.matcherCache.set(cfg, matcher2);
|
|
1187
|
+
return matcher2;
|
|
1188
|
+
}
|
|
1189
|
+
const patterns = cfg.include.map((p) => p.trim()).filter((p) => p.length > 0).map((p) => p.endsWith("/") ? p.slice(0, -1) : p);
|
|
1190
|
+
const ancestorDirs = /* @__PURE__ */ new Set();
|
|
1191
|
+
for (const pat of patterns) {
|
|
1192
|
+
const parts = pat.split("/");
|
|
1193
|
+
for (let i = 1; i < parts.length; i++) {
|
|
1194
|
+
ancestorDirs.add(parts.slice(0, i).join("/"));
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
const matcher = { mode, patterns, ancestorDirs };
|
|
1198
|
+
this.matcherCache.set(cfg, matcher);
|
|
1199
|
+
return matcher;
|
|
1200
|
+
}
|
|
1143
1201
|
};
|
|
1144
1202
|
|
|
1145
1203
|
// src/services/worktree-metadata.service.ts
|
|
1146
1204
|
import * as fs2 from "fs/promises";
|
|
1147
|
-
import * as
|
|
1205
|
+
import * as path4 from "path";
|
|
1148
1206
|
import simpleGit2 from "simple-git";
|
|
1149
1207
|
var WorktreeMetadataService = class {
|
|
1150
1208
|
logger;
|
|
@@ -1157,7 +1215,7 @@ var WorktreeMetadataService = class {
|
|
|
1157
1215
|
* For example: /worktrees/fix/test-branch -> test-branch (not fix/test-branch)
|
|
1158
1216
|
*/
|
|
1159
1217
|
getWorktreeDirectoryName(worktreePath) {
|
|
1160
|
-
return
|
|
1218
|
+
return path4.basename(worktreePath);
|
|
1161
1219
|
}
|
|
1162
1220
|
async getMetadataPath(bareRepoPath, worktreeName) {
|
|
1163
1221
|
if (worktreeName.includes("/") || worktreeName.includes("\\")) {
|
|
@@ -1165,7 +1223,7 @@ var WorktreeMetadataService = class {
|
|
|
1165
1223
|
`getMetadataPath requires a filesystem-safe worktree directory name, got '${worktreeName}'. Use getMetadataPathFromWorktreePath when starting from a raw branch name.`
|
|
1166
1224
|
);
|
|
1167
1225
|
}
|
|
1168
|
-
return
|
|
1226
|
+
return path4.join(
|
|
1169
1227
|
bareRepoPath,
|
|
1170
1228
|
METADATA_CONSTANTS.WORKTREE_METADATA_PATH,
|
|
1171
1229
|
worktreeName,
|
|
@@ -1178,7 +1236,7 @@ var WorktreeMetadataService = class {
|
|
|
1178
1236
|
}
|
|
1179
1237
|
async saveMetadata(bareRepoPath, worktreeName, metadata) {
|
|
1180
1238
|
const metadataPath = await this.getMetadataPath(bareRepoPath, worktreeName);
|
|
1181
|
-
await fs2.mkdir(
|
|
1239
|
+
await fs2.mkdir(path4.dirname(metadataPath), { recursive: true });
|
|
1182
1240
|
const tmpPath = `${metadataPath}.${process.pid}.${Date.now()}.tmp`;
|
|
1183
1241
|
let renamed = false;
|
|
1184
1242
|
try {
|
|
@@ -1371,7 +1429,7 @@ var WorktreeMetadataService = class {
|
|
|
1371
1429
|
|
|
1372
1430
|
// src/services/worktree-status.service.ts
|
|
1373
1431
|
import * as fs3 from "fs/promises";
|
|
1374
|
-
import * as
|
|
1432
|
+
import * as path5 from "path";
|
|
1375
1433
|
import simpleGit3 from "simple-git";
|
|
1376
1434
|
var OPERATION_FILES = [
|
|
1377
1435
|
{ file: GIT_OPERATIONS.MERGE_HEAD, type: "merge" },
|
|
@@ -1574,7 +1632,7 @@ var WorktreeStatusService = class {
|
|
|
1574
1632
|
async detectOperationFile(gitDir) {
|
|
1575
1633
|
const results = await Promise.all(
|
|
1576
1634
|
OPERATION_FILES.map(
|
|
1577
|
-
({ file }) => fs3.access(
|
|
1635
|
+
({ file }) => fs3.access(path5.join(gitDir, file)).then(
|
|
1578
1636
|
() => true,
|
|
1579
1637
|
() => false
|
|
1580
1638
|
)
|
|
@@ -1695,14 +1753,14 @@ var WorktreeStatusService = class {
|
|
|
1695
1753
|
}
|
|
1696
1754
|
}
|
|
1697
1755
|
async resolveGitDir(worktreePath) {
|
|
1698
|
-
const gitPath =
|
|
1756
|
+
const gitPath = path5.join(worktreePath, PATH_CONSTANTS.GIT_DIR);
|
|
1699
1757
|
try {
|
|
1700
1758
|
const stat4 = await fs3.stat(gitPath);
|
|
1701
1759
|
if (stat4.isFile()) {
|
|
1702
1760
|
const content = await fs3.readFile(gitPath, "utf-8");
|
|
1703
1761
|
const gitdirMatch = content.match(new RegExp(`^${GIT_CONSTANTS.GITDIR_PREFIX}\\s*(.+)$`, "m"));
|
|
1704
1762
|
if (gitdirMatch) {
|
|
1705
|
-
return
|
|
1763
|
+
return path5.resolve(worktreePath, gitdirMatch[1].trim());
|
|
1706
1764
|
}
|
|
1707
1765
|
throw new GitOperationError("resolve-git-dir", `Failed to parse gitdir from .git file at ${gitPath}`);
|
|
1708
1766
|
}
|
|
@@ -1716,7 +1774,7 @@ var WorktreeStatusService = class {
|
|
|
1716
1774
|
}
|
|
1717
1775
|
}
|
|
1718
1776
|
createGitInstance(worktreePath) {
|
|
1719
|
-
const key = `${
|
|
1777
|
+
const key = `${path5.resolve(worktreePath)}::${this.config.skipLfs ? "1" : "0"}`;
|
|
1720
1778
|
let git = this.gitInstances.get(key);
|
|
1721
1779
|
if (!git) {
|
|
1722
1780
|
git = this.config.skipLfs ? simpleGit3(worktreePath).env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit3(worktreePath);
|
|
@@ -1739,7 +1797,7 @@ var GitService = class {
|
|
|
1739
1797
|
this.config = config;
|
|
1740
1798
|
this.logger = logger ?? Logger.createDefault(void 0, config.debug);
|
|
1741
1799
|
this.bareRepoPath = this.config.bareRepoDir || getDefaultBareRepoDir(this.config.repoUrl);
|
|
1742
|
-
this.mainWorktreePath =
|
|
1800
|
+
this.mainWorktreePath = path6.join(this.config.worktreeDir, GIT_CONSTANTS.DEFAULT_BRANCH);
|
|
1743
1801
|
this.metadataService = new WorktreeMetadataService(this.logger);
|
|
1744
1802
|
this.statusService = new WorktreeStatusService({ skipLfs: this.config.skipLfs }, this.logger);
|
|
1745
1803
|
this.sparseCheckoutService = new SparseCheckoutService(this.logger);
|
|
@@ -1767,16 +1825,36 @@ var GitService = class {
|
|
|
1767
1825
|
return this.config.cloneTimeoutMs ?? DEFAULT_CONFIG.CLONE_TIMEOUT_MS;
|
|
1768
1826
|
}
|
|
1769
1827
|
getCachedGit(dirPath, useLfsSkip = false) {
|
|
1770
|
-
const key = `${
|
|
1828
|
+
const key = `${path6.resolve(dirPath)}::${useLfsSkip ? "1" : "0"}`;
|
|
1771
1829
|
let git = this.gitInstances.get(key);
|
|
1772
1830
|
if (!git) {
|
|
1773
|
-
const
|
|
1774
|
-
const base = block > 0 ? simpleGit4(dirPath, { timeout: { block } }) : simpleGit4(dirPath);
|
|
1831
|
+
const base = simpleGit4(dirPath, this.buildSimpleGitOptions(this.getFetchTimeoutMs()));
|
|
1775
1832
|
git = useLfsSkip ? base.env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : base;
|
|
1776
1833
|
this.gitInstances.set(key, git);
|
|
1777
1834
|
}
|
|
1778
1835
|
return git;
|
|
1779
1836
|
}
|
|
1837
|
+
buildSimpleGitOptions(blockMs) {
|
|
1838
|
+
const options = { progress: this.makeProgressHandler() };
|
|
1839
|
+
if (blockMs > 0) options.timeout = { block: blockMs };
|
|
1840
|
+
return options;
|
|
1841
|
+
}
|
|
1842
|
+
makeProgressHandler() {
|
|
1843
|
+
const lastBucket = /* @__PURE__ */ new Map();
|
|
1844
|
+
return (event) => {
|
|
1845
|
+
if (event.method !== "fetch" && event.method !== "clone" && event.method !== "pull") return;
|
|
1846
|
+
const key = `${event.method}:${event.stage}`;
|
|
1847
|
+
const bucket = Math.floor(event.progress / GIT_CONSTANTS.PROGRESS_BUCKET_PERCENT);
|
|
1848
|
+
let last = lastBucket.get(key) ?? -1;
|
|
1849
|
+
if (bucket < last) {
|
|
1850
|
+
last = -1;
|
|
1851
|
+
}
|
|
1852
|
+
if (bucket <= last && event.progress < 100) return;
|
|
1853
|
+
lastBucket.set(key, bucket);
|
|
1854
|
+
const total = event.total > 0 ? `${event.processed}/${event.total}` : `${event.processed}`;
|
|
1855
|
+
this.logger.info(` \u21B3 ${event.method} ${event.stage}: ${event.progress}% (${total})`);
|
|
1856
|
+
};
|
|
1857
|
+
}
|
|
1780
1858
|
updateLogger(logger) {
|
|
1781
1859
|
this.logger = logger;
|
|
1782
1860
|
this.sparseCheckoutService.updateLogger(logger);
|
|
@@ -1784,14 +1862,13 @@ var GitService = class {
|
|
|
1784
1862
|
async initialize() {
|
|
1785
1863
|
const { repoUrl } = this.config;
|
|
1786
1864
|
try {
|
|
1787
|
-
await fs4.access(
|
|
1865
|
+
await fs4.access(path6.join(this.bareRepoPath, "HEAD"));
|
|
1788
1866
|
} catch {
|
|
1789
1867
|
this.logger.info(`Cloning from "${repoUrl}" as bare repository into "${this.bareRepoPath}"...`);
|
|
1790
|
-
await fs4.mkdir(
|
|
1791
|
-
const
|
|
1792
|
-
const cloneBase = cloneBlock > 0 ? simpleGit4({ timeout: { block: cloneBlock } }) : simpleGit4();
|
|
1868
|
+
await fs4.mkdir(path6.dirname(this.bareRepoPath), { recursive: true });
|
|
1869
|
+
const cloneBase = simpleGit4(this.buildSimpleGitOptions(this.getCloneTimeoutMs()));
|
|
1793
1870
|
const cloneGit = this.isLfsSkipEnabled() ? cloneBase.env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : cloneBase;
|
|
1794
|
-
await cloneGit.clone(repoUrl, this.bareRepoPath, ["--bare"]);
|
|
1871
|
+
await cloneGit.clone(repoUrl, this.bareRepoPath, ["--bare", "--progress"]);
|
|
1795
1872
|
this.logger.info("\u2705 Clone successful.");
|
|
1796
1873
|
}
|
|
1797
1874
|
const bareGit = this.getCachedGit(this.bareRepoPath);
|
|
@@ -1805,19 +1882,19 @@ var GitService = class {
|
|
|
1805
1882
|
await bareGit.addConfig("remote.origin.fetch", "+refs/heads/*:refs/remotes/origin/*");
|
|
1806
1883
|
}
|
|
1807
1884
|
this.logger.info("Fetching remote branches...");
|
|
1808
|
-
await bareGit.fetch(["--all"]);
|
|
1885
|
+
await bareGit.fetch(["--all", "--progress"]);
|
|
1809
1886
|
this.defaultBranch = await this.detectDefaultBranch(bareGit);
|
|
1810
|
-
this.mainWorktreePath =
|
|
1887
|
+
this.mainWorktreePath = path6.join(this.config.worktreeDir, this.defaultBranch);
|
|
1811
1888
|
let needsMainWorktree = true;
|
|
1812
1889
|
try {
|
|
1813
1890
|
const worktrees = await this.getWorktreesFromBare(bareGit);
|
|
1814
|
-
needsMainWorktree = !worktrees.some((w) =>
|
|
1891
|
+
needsMainWorktree = !worktrees.some((w) => path6.resolve(w.path) === path6.resolve(this.mainWorktreePath));
|
|
1815
1892
|
} catch {
|
|
1816
1893
|
}
|
|
1817
1894
|
if (needsMainWorktree) {
|
|
1818
1895
|
this.logger.info(`Creating ${this.defaultBranch} worktree at "${this.mainWorktreePath}"...`);
|
|
1819
1896
|
await fs4.mkdir(this.config.worktreeDir, { recursive: true });
|
|
1820
|
-
const absoluteWorktreePath =
|
|
1897
|
+
const absoluteWorktreePath = path6.resolve(this.mainWorktreePath);
|
|
1821
1898
|
const branches = await bareGit.branch();
|
|
1822
1899
|
const defaultBranchExists = branches.all.includes(this.defaultBranch);
|
|
1823
1900
|
const useNoCheckoutMain = !!this.config.sparseCheckout;
|
|
@@ -1853,7 +1930,7 @@ var GitService = class {
|
|
|
1853
1930
|
}
|
|
1854
1931
|
const updatedWorktrees = await this.getWorktreesFromBare(bareGit);
|
|
1855
1932
|
const mainWorktreeRegistered = updatedWorktrees.some(
|
|
1856
|
-
(w) =>
|
|
1933
|
+
(w) => path6.resolve(w.path) === path6.resolve(this.mainWorktreePath)
|
|
1857
1934
|
);
|
|
1858
1935
|
if (!mainWorktreeRegistered) {
|
|
1859
1936
|
if (process.env.NODE_ENV !== ENV_CONSTANTS.NODE_ENV_TEST) {
|
|
@@ -1883,12 +1960,12 @@ var GitService = class {
|
|
|
1883
1960
|
this.assertInitialized();
|
|
1884
1961
|
this.logger.info("Fetching latest data from remote...");
|
|
1885
1962
|
const git = this.getCachedGit(this.mainWorktreePath, this.isLfsSkipEnabled());
|
|
1886
|
-
await git.fetch(["--all", "--prune"]);
|
|
1963
|
+
await git.fetch(["--all", "--prune", "--progress"]);
|
|
1887
1964
|
}
|
|
1888
1965
|
async fetchBranch(branchName) {
|
|
1889
1966
|
this.assertInitialized();
|
|
1890
1967
|
const git = this.getCachedGit(this.mainWorktreePath, this.isLfsSkipEnabled());
|
|
1891
|
-
await git.fetch(["origin", branchName, "--prune"]);
|
|
1968
|
+
await git.fetch(["origin", branchName, "--prune", "--progress"]);
|
|
1892
1969
|
}
|
|
1893
1970
|
assertInitialized() {
|
|
1894
1971
|
if (!this.git) {
|
|
@@ -1936,7 +2013,7 @@ var GitService = class {
|
|
|
1936
2013
|
const existence = await Promise.all(
|
|
1937
2014
|
lfsFileList.map(async (f) => {
|
|
1938
2015
|
try {
|
|
1939
|
-
await fs4.access(
|
|
2016
|
+
await fs4.access(path6.join(worktreePath, f));
|
|
1940
2017
|
return f;
|
|
1941
2018
|
} catch {
|
|
1942
2019
|
return null;
|
|
@@ -1964,7 +2041,7 @@ var GitService = class {
|
|
|
1964
2041
|
let allDownloaded = true;
|
|
1965
2042
|
const notDownloaded = [];
|
|
1966
2043
|
for (const file of samplesToCheck) {
|
|
1967
|
-
const filePath =
|
|
2044
|
+
const filePath = path6.join(worktreePath, file);
|
|
1968
2045
|
try {
|
|
1969
2046
|
const handle = await fs4.open(filePath, "r");
|
|
1970
2047
|
try {
|
|
@@ -2053,12 +2130,12 @@ var GitService = class {
|
|
|
2053
2130
|
}
|
|
2054
2131
|
async addWorktree(branchName, worktreePath) {
|
|
2055
2132
|
const bareGit = this.getCachedGit(this.bareRepoPath, this.isLfsSkipEnabled());
|
|
2056
|
-
const absoluteWorktreePath =
|
|
2057
|
-
await fs4.mkdir(
|
|
2133
|
+
const absoluteWorktreePath = path6.resolve(worktreePath);
|
|
2134
|
+
await fs4.mkdir(path6.dirname(absoluteWorktreePath), { recursive: true });
|
|
2058
2135
|
try {
|
|
2059
2136
|
await fs4.access(absoluteWorktreePath);
|
|
2060
2137
|
const worktrees = await this.getWorktreesFromBare(bareGit);
|
|
2061
|
-
const isValidWorktree = worktrees.some((w) =>
|
|
2138
|
+
const isValidWorktree = worktrees.some((w) => path6.resolve(w.path) === absoluteWorktreePath);
|
|
2062
2139
|
if (isValidWorktree) {
|
|
2063
2140
|
this.logger.info(` - Worktree for '${branchName}' already exists at '${absoluteWorktreePath}'`);
|
|
2064
2141
|
return;
|
|
@@ -2103,7 +2180,7 @@ var GitService = class {
|
|
|
2103
2180
|
}
|
|
2104
2181
|
if (errorMessage.includes("already registered worktree")) {
|
|
2105
2182
|
const worktrees = await this.getWorktreesFromBare(bareGit);
|
|
2106
|
-
const existingWorktree = worktrees.find((w) =>
|
|
2183
|
+
const existingWorktree = worktrees.find((w) => path6.resolve(w.path) === absoluteWorktreePath);
|
|
2107
2184
|
if (existingWorktree && !existingWorktree.isPrunable) {
|
|
2108
2185
|
this.logger.info(` - Worktree for '${branchName}' was created by concurrent operation`);
|
|
2109
2186
|
return;
|
|
@@ -2149,7 +2226,7 @@ var GitService = class {
|
|
|
2149
2226
|
try {
|
|
2150
2227
|
await fs4.access(absoluteWorktreePath);
|
|
2151
2228
|
const worktrees = await this.getWorktreesFromBare(bareGit);
|
|
2152
|
-
const isValidWorktree = worktrees.some((w) =>
|
|
2229
|
+
const isValidWorktree = worktrees.some((w) => path6.resolve(w.path) === absoluteWorktreePath);
|
|
2153
2230
|
if (isValidWorktree) {
|
|
2154
2231
|
this.logger.info(` - Worktree for '${branchName}' already exists at '${absoluteWorktreePath}'`);
|
|
2155
2232
|
return;
|
|
@@ -2179,7 +2256,7 @@ var GitService = class {
|
|
|
2179
2256
|
const fallbackErrorMessage = getErrorMessage(fallbackError);
|
|
2180
2257
|
if (fallbackErrorMessage.includes("already registered worktree")) {
|
|
2181
2258
|
const worktrees = await this.getWorktreesFromBare(bareGit);
|
|
2182
|
-
const existingWorktree = worktrees.find((w) =>
|
|
2259
|
+
const existingWorktree = worktrees.find((w) => path6.resolve(w.path) === absoluteWorktreePath);
|
|
2183
2260
|
if (existingWorktree && !existingWorktree.isPrunable) {
|
|
2184
2261
|
this.logger.info(` - Worktree for '${branchName}' was created by concurrent operation during fallback`);
|
|
2185
2262
|
return;
|
|
@@ -2402,6 +2479,23 @@ var GitService = class {
|
|
|
2402
2479
|
return false;
|
|
2403
2480
|
}
|
|
2404
2481
|
}
|
|
2482
|
+
async getChangedPathsInRange(worktreePath, fromRef, toRef) {
|
|
2483
|
+
const worktreeGit = this.getCachedGit(worktreePath);
|
|
2484
|
+
try {
|
|
2485
|
+
const out = await worktreeGit.raw([
|
|
2486
|
+
"-c",
|
|
2487
|
+
"core.quotePath=false",
|
|
2488
|
+
"diff",
|
|
2489
|
+
"--name-only",
|
|
2490
|
+
"--no-renames",
|
|
2491
|
+
`${fromRef}..${toRef}`
|
|
2492
|
+
]);
|
|
2493
|
+
return out.split("\n").map((l) => l.replace(/\r$/, "")).filter((l) => l.length > 0);
|
|
2494
|
+
} catch (error) {
|
|
2495
|
+
this.logger.warn(`Failed to compute diff ${fromRef}..${toRef} in ${worktreePath}: ${getErrorMessage(error)}`);
|
|
2496
|
+
return null;
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2405
2499
|
async compareTreeContent(worktreePath, branch) {
|
|
2406
2500
|
const worktreeGit = this.getCachedGit(worktreePath);
|
|
2407
2501
|
try {
|
|
@@ -2486,7 +2580,7 @@ var GitService = class {
|
|
|
2486
2580
|
// src/services/path-resolution.service.ts
|
|
2487
2581
|
import { createHash } from "crypto";
|
|
2488
2582
|
import * as fs5 from "fs";
|
|
2489
|
-
import * as
|
|
2583
|
+
import * as path7 from "path";
|
|
2490
2584
|
var BRANCH_STEM_MAX = 80;
|
|
2491
2585
|
var BRANCH_HASH_LEN = 8;
|
|
2492
2586
|
var PathResolutionService = class {
|
|
@@ -2496,22 +2590,22 @@ var PathResolutionService = class {
|
|
|
2496
2590
|
return `${stem}-${hash}`;
|
|
2497
2591
|
}
|
|
2498
2592
|
getBranchWorktreePath(worktreeDir, branchName) {
|
|
2499
|
-
return
|
|
2593
|
+
return path7.join(worktreeDir, this.sanitizeBranchName(branchName));
|
|
2500
2594
|
}
|
|
2501
2595
|
resolveRealPath(inputPath) {
|
|
2502
|
-
const absolute =
|
|
2596
|
+
const absolute = path7.resolve(inputPath);
|
|
2503
2597
|
const missing = [];
|
|
2504
2598
|
let current = absolute;
|
|
2505
2599
|
while (!fs5.existsSync(current)) {
|
|
2506
|
-
const parent =
|
|
2600
|
+
const parent = path7.dirname(current);
|
|
2507
2601
|
if (parent === current) {
|
|
2508
2602
|
return absolute;
|
|
2509
2603
|
}
|
|
2510
|
-
missing.unshift(
|
|
2604
|
+
missing.unshift(path7.basename(current));
|
|
2511
2605
|
current = parent;
|
|
2512
2606
|
}
|
|
2513
2607
|
try {
|
|
2514
|
-
return
|
|
2608
|
+
return path7.join(fs5.realpathSync(current), ...missing);
|
|
2515
2609
|
} catch {
|
|
2516
2610
|
return absolute;
|
|
2517
2611
|
}
|
|
@@ -2521,7 +2615,7 @@ var PathResolutionService = class {
|
|
|
2521
2615
|
const a = fold(resolved);
|
|
2522
2616
|
const b = fold(resolvedBase);
|
|
2523
2617
|
if (a === b) return true;
|
|
2524
|
-
return a.length > b.length && a.charAt(b.length) ===
|
|
2618
|
+
return a.length > b.length && a.charAt(b.length) === path7.sep && a.startsWith(b);
|
|
2525
2619
|
}
|
|
2526
2620
|
normalizeWorktreePath(worktreePath, worktreeBaseDir) {
|
|
2527
2621
|
const resolved = this.resolveRealPath(worktreePath);
|
|
@@ -2529,7 +2623,7 @@ var PathResolutionService = class {
|
|
|
2529
2623
|
if (!this.isResolvedPathInsideBase(resolved, resolvedBase)) {
|
|
2530
2624
|
throw new Error(`Worktree path '${worktreePath}' is outside base directory '${worktreeBaseDir}'`);
|
|
2531
2625
|
}
|
|
2532
|
-
return
|
|
2626
|
+
return path7.relative(resolvedBase, resolved);
|
|
2533
2627
|
}
|
|
2534
2628
|
isPathInsideBaseDir(targetPath, baseDir) {
|
|
2535
2629
|
const resolved = this.resolveRealPath(targetPath);
|
|
@@ -2635,7 +2729,7 @@ var WorktreeSyncService = class {
|
|
|
2635
2729
|
};
|
|
2636
2730
|
}
|
|
2637
2731
|
const barePath = this.gitService.getBareRepoPath();
|
|
2638
|
-
const lockTarget =
|
|
2732
|
+
const lockTarget = path8.join(barePath, "HEAD");
|
|
2639
2733
|
try {
|
|
2640
2734
|
await fs6.access(lockTarget);
|
|
2641
2735
|
} catch {
|
|
@@ -2833,12 +2927,12 @@ var WorktreeSyncService = class {
|
|
|
2833
2927
|
}
|
|
2834
2928
|
const reservedPaths = /* @__PURE__ */ new Map();
|
|
2835
2929
|
for (const w of worktrees) {
|
|
2836
|
-
reservedPaths.set(
|
|
2930
|
+
reservedPaths.set(path8.resolve(w.path), w.branch);
|
|
2837
2931
|
}
|
|
2838
2932
|
const plan = [];
|
|
2839
2933
|
for (const branchName of newBranches) {
|
|
2840
2934
|
const worktreePath = this.pathResolution.getBranchWorktreePath(this.config.worktreeDir, branchName);
|
|
2841
|
-
const resolved =
|
|
2935
|
+
const resolved = path8.resolve(worktreePath);
|
|
2842
2936
|
const conflict = reservedPaths.get(resolved);
|
|
2843
2937
|
if (conflict && conflict !== branchName) {
|
|
2844
2938
|
this.logger.error(
|
|
@@ -3043,12 +3137,12 @@ var WorktreeSyncService = class {
|
|
|
3043
3137
|
}
|
|
3044
3138
|
async updateExistingWorktrees(worktrees, remoteBranches) {
|
|
3045
3139
|
this.logger.info("Step 4: Checking for worktrees that need updates...");
|
|
3046
|
-
const divergedDir =
|
|
3140
|
+
const divergedDir = path8.join(this.config.worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
|
|
3047
3141
|
try {
|
|
3048
3142
|
const diverged = await fs6.readdir(divergedDir);
|
|
3049
3143
|
if (diverged.length > 0) {
|
|
3050
3144
|
this.logger.info(
|
|
3051
|
-
`\u{1F4E6} Note: ${diverged.length} diverged worktree(s) in ${
|
|
3145
|
+
`\u{1F4E6} Note: ${diverged.length} diverged worktree(s) in ${path8.relative(process.cwd(), divergedDir)}`
|
|
3052
3146
|
);
|
|
3053
3147
|
}
|
|
3054
3148
|
} catch {
|
|
@@ -3078,7 +3172,23 @@ var WorktreeSyncService = class {
|
|
|
3078
3172
|
return { action: "diverged", worktree };
|
|
3079
3173
|
}
|
|
3080
3174
|
const isBehind = await this.gitService.isWorktreeBehind(worktree.path);
|
|
3081
|
-
|
|
3175
|
+
if (!isBehind) return null;
|
|
3176
|
+
const sparseCfg = this.config.sparseCheckout;
|
|
3177
|
+
if (sparseCfg && sparseCfg.skipUpdateWhenOutsideSparse !== false) {
|
|
3178
|
+
const sparseService = this.gitService.getSparseCheckoutService();
|
|
3179
|
+
if (sparseService.resolveMode(sparseCfg) === "cone") {
|
|
3180
|
+
const diff = await this.gitService.getChangedPathsInRange(
|
|
3181
|
+
worktree.path,
|
|
3182
|
+
"HEAD",
|
|
3183
|
+
`origin/${worktree.branch}`
|
|
3184
|
+
);
|
|
3185
|
+
if (diff !== null && !sparseService.pathsTouchSparse(diff, sparseCfg)) {
|
|
3186
|
+
this.logger.info(`\u23ED\uFE0F Skipping '${worktree.branch}' - upstream changes outside sparse paths`);
|
|
3187
|
+
return null;
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
return { action: "update", worktree };
|
|
3082
3192
|
})
|
|
3083
3193
|
)
|
|
3084
3194
|
);
|
|
@@ -3156,13 +3266,13 @@ var WorktreeSyncService = class {
|
|
|
3156
3266
|
}
|
|
3157
3267
|
async cleanupOrphanedDirectories(worktrees) {
|
|
3158
3268
|
try {
|
|
3159
|
-
const worktreeRelativePaths = worktrees.map((w) =>
|
|
3269
|
+
const worktreeRelativePaths = worktrees.map((w) => path8.relative(this.config.worktreeDir, w.path));
|
|
3160
3270
|
const allDirs = await fs6.readdir(this.config.worktreeDir);
|
|
3161
3271
|
const regularDirs = allDirs.filter((dir) => !dir.startsWith("."));
|
|
3162
3272
|
const orphanedDirs = [];
|
|
3163
3273
|
for (const dir of regularDirs) {
|
|
3164
3274
|
const isPartOfWorktree = worktreeRelativePaths.some((worktreePath) => {
|
|
3165
|
-
return worktreePath === dir || worktreePath.startsWith(dir +
|
|
3275
|
+
return worktreePath === dir || worktreePath.startsWith(dir + path8.sep);
|
|
3166
3276
|
});
|
|
3167
3277
|
if (!isPartOfWorktree) {
|
|
3168
3278
|
orphanedDirs.push(dir);
|
|
@@ -3171,7 +3281,7 @@ var WorktreeSyncService = class {
|
|
|
3171
3281
|
if (orphanedDirs.length > 0) {
|
|
3172
3282
|
this.logger.info(`Found ${orphanedDirs.length} orphaned directories: ${orphanedDirs.join(", ")}`);
|
|
3173
3283
|
for (const dir of orphanedDirs) {
|
|
3174
|
-
const dirPath =
|
|
3284
|
+
const dirPath = path8.join(this.config.worktreeDir, dir);
|
|
3175
3285
|
try {
|
|
3176
3286
|
const stat4 = await fs6.stat(dirPath);
|
|
3177
3287
|
if (stat4.isDirectory()) {
|
|
@@ -3205,7 +3315,7 @@ var WorktreeSyncService = class {
|
|
|
3205
3315
|
} else {
|
|
3206
3316
|
this.logger.info(`\u{1F512} Branch '${worktree.branch}' has diverged with local changes. Moving to diverged...`);
|
|
3207
3317
|
const divergedPath = await this.divergeWorktree(worktree.path, worktree.branch);
|
|
3208
|
-
const relativePath =
|
|
3318
|
+
const relativePath = path8.relative(process.cwd(), divergedPath);
|
|
3209
3319
|
this.logger.info(` Moved to: ${relativePath}`);
|
|
3210
3320
|
this.logger.info(` Your local changes are preserved. To review:`);
|
|
3211
3321
|
this.logger.info(` cd ${relativePath}`);
|
|
@@ -3229,12 +3339,12 @@ var WorktreeSyncService = class {
|
|
|
3229
3339
|
}
|
|
3230
3340
|
}
|
|
3231
3341
|
async divergeWorktree(worktreePath, branchName) {
|
|
3232
|
-
const divergedBaseDir =
|
|
3342
|
+
const divergedBaseDir = path8.join(this.config.worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
|
|
3233
3343
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
3234
3344
|
const uniqueSuffix = Date.now().toString(36) + Math.random().toString(36).substring(2, 7);
|
|
3235
3345
|
const safeBranchName = this.pathResolution.sanitizeBranchName(branchName);
|
|
3236
3346
|
const divergedName = `${timestamp}-${safeBranchName}-${uniqueSuffix}`;
|
|
3237
|
-
const divergedPath =
|
|
3347
|
+
const divergedPath = path8.join(divergedBaseDir, divergedName);
|
|
3238
3348
|
await fs6.mkdir(divergedBaseDir, { recursive: true });
|
|
3239
3349
|
try {
|
|
3240
3350
|
await fs6.rename(worktreePath, divergedPath);
|
|
@@ -3261,7 +3371,7 @@ var WorktreeSyncService = class {
|
|
|
3261
3371
|
Original worktree location: ${worktreePath}`
|
|
3262
3372
|
};
|
|
3263
3373
|
await fs6.writeFile(
|
|
3264
|
-
|
|
3374
|
+
path8.join(divergedPath, METADATA_CONSTANTS.DIVERGED_INFO_FILE),
|
|
3265
3375
|
JSON.stringify(metadata, null, 2)
|
|
3266
3376
|
);
|
|
3267
3377
|
return divergedPath;
|
|
@@ -3316,9 +3426,9 @@ var RepositoryContext = class {
|
|
|
3316
3426
|
discoveryCache = /* @__PURE__ */ new Map();
|
|
3317
3427
|
async loadConfig(configPath, options = {}) {
|
|
3318
3428
|
const setDefaultCurrent = options.setDefaultCurrent ?? true;
|
|
3319
|
-
const absolutePath =
|
|
3429
|
+
const absolutePath = path9.resolve(configPath);
|
|
3320
3430
|
const configFile = await this.configLoader.loadConfigFile(absolutePath);
|
|
3321
|
-
const configDir =
|
|
3431
|
+
const configDir = path9.dirname(absolutePath);
|
|
3322
3432
|
const globalDefaults = configFile.defaults;
|
|
3323
3433
|
const resolvedAll = [];
|
|
3324
3434
|
for (const repo of configFile.repositories) {
|
|
@@ -3355,7 +3465,7 @@ var RepositoryContext = class {
|
|
|
3355
3465
|
return configFile.repositories;
|
|
3356
3466
|
}
|
|
3357
3467
|
async detectFromPath(dirPath) {
|
|
3358
|
-
const absolutePath =
|
|
3468
|
+
const absolutePath = path9.resolve(dirPath);
|
|
3359
3469
|
const cached = this.discoveryCache.get(absolutePath);
|
|
3360
3470
|
if (cached && await this.isCacheFresh(cached)) {
|
|
3361
3471
|
return cached.result;
|
|
@@ -3374,8 +3484,8 @@ var RepositoryContext = class {
|
|
|
3374
3484
|
const { result, adminDir } = await this.detectFromPathUncached(absolutePath);
|
|
3375
3485
|
if (result.isWorktree && result.bareRepoPath && adminDir) {
|
|
3376
3486
|
const [worktreeHeadMtimeMs, worktreesDirMtimeMs] = await Promise.all([
|
|
3377
|
-
safeMtimeMs(
|
|
3378
|
-
safeMtimeMs(
|
|
3487
|
+
safeMtimeMs(path9.join(adminDir, "HEAD")),
|
|
3488
|
+
safeMtimeMs(path9.join(result.bareRepoPath, "worktrees"))
|
|
3379
3489
|
]);
|
|
3380
3490
|
this.discoveryCache.set(absolutePath, {
|
|
3381
3491
|
result,
|
|
@@ -3407,8 +3517,8 @@ var RepositoryContext = class {
|
|
|
3407
3517
|
return this.discoveryCache.size;
|
|
3408
3518
|
}
|
|
3409
3519
|
async discoverSiblingRepositories(currentBareRepoPath) {
|
|
3410
|
-
const repoDir =
|
|
3411
|
-
const workspaceRoot =
|
|
3520
|
+
const repoDir = path9.dirname(currentBareRepoPath);
|
|
3521
|
+
const workspaceRoot = path9.dirname(repoDir);
|
|
3412
3522
|
if (workspaceRoot === repoDir) return [];
|
|
3413
3523
|
let entries;
|
|
3414
3524
|
try {
|
|
@@ -3425,15 +3535,15 @@ var RepositoryContext = class {
|
|
|
3425
3535
|
const results = [];
|
|
3426
3536
|
await Promise.all(
|
|
3427
3537
|
entries.map(async (entry) => {
|
|
3428
|
-
const candidate =
|
|
3429
|
-
const bareCandidate =
|
|
3538
|
+
const candidate = path9.join(workspaceRoot, entry);
|
|
3539
|
+
const bareCandidate = path9.join(candidate, GIT_CONSTANTS.BARE_DIR_NAME);
|
|
3430
3540
|
try {
|
|
3431
3541
|
const stat4 = await fs7.stat(bareCandidate);
|
|
3432
3542
|
if (!stat4.isDirectory()) return;
|
|
3433
3543
|
} catch {
|
|
3434
3544
|
return;
|
|
3435
3545
|
}
|
|
3436
|
-
const resolvedBare =
|
|
3546
|
+
const resolvedBare = path9.resolve(bareCandidate);
|
|
3437
3547
|
const matchedName = configBares.get(normalizePathForCompare(resolvedBare));
|
|
3438
3548
|
results.push({
|
|
3439
3549
|
name: matchedName ?? entry,
|
|
@@ -3455,8 +3565,8 @@ var RepositoryContext = class {
|
|
|
3455
3565
|
if (Date.now() - cached.cachedAt >= DISCOVERY_CACHE_TTL_MS) return false;
|
|
3456
3566
|
if (!cached.worktreeAdminDir || !cached.result.bareRepoPath) return true;
|
|
3457
3567
|
const [currentHeadMtime, currentWorktreesDirMtime] = await Promise.all([
|
|
3458
|
-
safeMtimeMs(
|
|
3459
|
-
safeMtimeMs(
|
|
3568
|
+
safeMtimeMs(path9.join(cached.worktreeAdminDir, "HEAD")),
|
|
3569
|
+
safeMtimeMs(path9.join(cached.result.bareRepoPath, "worktrees"))
|
|
3460
3570
|
]);
|
|
3461
3571
|
return currentHeadMtime === cached.worktreeHeadMtimeMs && currentWorktreesDirMtime === cached.worktreesDirMtimeMs;
|
|
3462
3572
|
}
|
|
@@ -3497,13 +3607,13 @@ var RepositoryContext = class {
|
|
|
3497
3607
|
return unsupported("Invalid .git file format (missing gitdir line)");
|
|
3498
3608
|
}
|
|
3499
3609
|
const gitdir = gitdirMatch[1].trim();
|
|
3500
|
-
const resolvedGitdir =
|
|
3610
|
+
const resolvedGitdir = path9.isAbsolute(gitdir) ? gitdir : path9.resolve(worktreeRoot, gitdir);
|
|
3501
3611
|
const worktreesMatch = resolvedGitdir.match(/^(.+?)[/\\]worktrees[/\\][^/\\]+$/);
|
|
3502
3612
|
if (!worktreesMatch) {
|
|
3503
3613
|
return unsupported("gitdir does not follow worktree structure (missing /worktrees/<name>)");
|
|
3504
3614
|
}
|
|
3505
|
-
const bareRepoPath =
|
|
3506
|
-
const adminDir =
|
|
3615
|
+
const bareRepoPath = path9.resolve(worktreesMatch[1]);
|
|
3616
|
+
const adminDir = path9.resolve(resolvedGitdir);
|
|
3507
3617
|
let repoUrl = null;
|
|
3508
3618
|
let worktrees = [];
|
|
3509
3619
|
let currentBranch = null;
|
|
@@ -3544,7 +3654,7 @@ var RepositoryContext = class {
|
|
|
3544
3654
|
adminDir
|
|
3545
3655
|
};
|
|
3546
3656
|
}
|
|
3547
|
-
const worktreeDir =
|
|
3657
|
+
const worktreeDir = path9.dirname(worktreeRoot);
|
|
3548
3658
|
const noUrlReason = "no remote origin URL detected";
|
|
3549
3659
|
const capabilities = {
|
|
3550
3660
|
listWorktrees: { available: true },
|
|
@@ -3580,7 +3690,7 @@ var RepositoryContext = class {
|
|
|
3580
3690
|
cronSchedule: DEFAULT_CONFIG.CRON_SCHEDULE,
|
|
3581
3691
|
runOnce: true
|
|
3582
3692
|
};
|
|
3583
|
-
const detectedKey = `${AUTO_DETECT_PREFIX}${
|
|
3693
|
+
const detectedKey = `${AUTO_DETECT_PREFIX}${path9.basename(bareRepoPath)}@${bareRepoPath}`;
|
|
3584
3694
|
if (!this.repos.has(detectedKey)) {
|
|
3585
3695
|
this.repos.set(detectedKey, {
|
|
3586
3696
|
name: detectedKey,
|
|
@@ -3672,7 +3782,7 @@ function parseWorktreeList(output, currentPath) {
|
|
|
3672
3782
|
const foldedCurrent = normalizePathForCompare(currentPath);
|
|
3673
3783
|
const results = [];
|
|
3674
3784
|
for (const wt of parseWorktreeListPorcelain(output)) {
|
|
3675
|
-
const resolved =
|
|
3785
|
+
const resolved = path9.resolve(wt.path);
|
|
3676
3786
|
const branch = wt.branch ?? (wt.detached ? `(detached ${(wt.head ?? "").slice(0, 7)})` : null);
|
|
3677
3787
|
if (!branch) continue;
|
|
3678
3788
|
results.push({
|
|
@@ -3692,10 +3802,10 @@ async function safeMtimeMs(filePath) {
|
|
|
3692
3802
|
}
|
|
3693
3803
|
}
|
|
3694
3804
|
async function findWorktreeRoot(startPath) {
|
|
3695
|
-
let current =
|
|
3696
|
-
const root =
|
|
3805
|
+
let current = path9.resolve(startPath);
|
|
3806
|
+
const root = path9.parse(current).root;
|
|
3697
3807
|
while (true) {
|
|
3698
|
-
const gitPath =
|
|
3808
|
+
const gitPath = path9.join(current, ".git");
|
|
3699
3809
|
try {
|
|
3700
3810
|
const content = await fs7.readFile(gitPath, "utf-8");
|
|
3701
3811
|
return { kind: "worktree-file", worktreeRoot: current, gitFileContent: content };
|
|
@@ -3709,7 +3819,7 @@ async function findWorktreeRoot(startPath) {
|
|
|
3709
3819
|
}
|
|
3710
3820
|
}
|
|
3711
3821
|
if (current === root) return null;
|
|
3712
|
-
const parent =
|
|
3822
|
+
const parent = path9.dirname(current);
|
|
3713
3823
|
if (parent === current) return null;
|
|
3714
3824
|
current = parent;
|
|
3715
3825
|
}
|
|
@@ -3720,7 +3830,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
3720
3830
|
import { z } from "zod";
|
|
3721
3831
|
|
|
3722
3832
|
// src/mcp/handlers.ts
|
|
3723
|
-
import * as
|
|
3833
|
+
import * as path10 from "path";
|
|
3724
3834
|
import pLimit2 from "p-limit";
|
|
3725
3835
|
|
|
3726
3836
|
// src/utils/disk-space.ts
|
|
@@ -3917,7 +4027,7 @@ async function getReadyService(ctx, repoName, options = {}) {
|
|
|
3917
4027
|
}
|
|
3918
4028
|
async function ensureRepoWorktreePath(ctx, params, git) {
|
|
3919
4029
|
await ensurePathBelongsToRepo(ctx, params.path, params.repoName, git);
|
|
3920
|
-
return
|
|
4030
|
+
return path10.resolve(params.path);
|
|
3921
4031
|
}
|
|
3922
4032
|
async function ensurePathBelongsToRepo(ctx, targetPath, repoName, git) {
|
|
3923
4033
|
const discovered = ctx.getDiscoveredContext(repoName);
|
|
@@ -3978,7 +4088,7 @@ async function handleListWorktrees(ctx, params, _extra) {
|
|
|
3978
4088
|
const results = await Promise.all(
|
|
3979
4089
|
worktrees.map(
|
|
3980
4090
|
(wt) => limit(async () => {
|
|
3981
|
-
const resolvedPath =
|
|
4091
|
+
const resolvedPath = path10.resolve(wt.path);
|
|
3982
4092
|
const isCurrent = currentPath !== null && pathsEqual(wt.path, currentPath);
|
|
3983
4093
|
const [status, divergence, metadata, sizeBytes] = await Promise.all([
|
|
3984
4094
|
git.getFullWorktreeStatus(wt.path, false).catch(() => null),
|
|
@@ -4058,7 +4168,7 @@ async function handleCreateWorktree(ctx, params, _extra) {
|
|
|
4058
4168
|
return formatToolResponse({
|
|
4059
4169
|
success: true,
|
|
4060
4170
|
branchName,
|
|
4061
|
-
worktreePath:
|
|
4171
|
+
worktreePath: path10.resolve(worktreePath),
|
|
4062
4172
|
created,
|
|
4063
4173
|
pushed
|
|
4064
4174
|
});
|
|
@@ -4146,7 +4256,7 @@ async function handleLoadConfig(ctx, params, _extra) {
|
|
|
4146
4256
|
}
|
|
4147
4257
|
await ctx.loadConfig(configPath);
|
|
4148
4258
|
return formatToolResponse({
|
|
4149
|
-
configPath:
|
|
4259
|
+
configPath: path10.resolve(configPath),
|
|
4150
4260
|
currentRepository: ctx.getCurrentRepo(),
|
|
4151
4261
|
repositories: ctx.getRepositoryList()
|
|
4152
4262
|
});
|