sync-worktrees 3.4.0 → 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 +228 -105
- package/dist/index.js.map +4 -4
- package/dist/mcp-server.js +169 -79
- 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
|
|
@@ -202,9 +202,9 @@ var WorktreeError = class extends SyncWorktreesError {
|
|
|
202
202
|
}
|
|
203
203
|
};
|
|
204
204
|
var WorktreeNotCleanError = class extends WorktreeError {
|
|
205
|
-
constructor(
|
|
206
|
-
super(`Worktree at '${
|
|
207
|
-
this.path =
|
|
205
|
+
constructor(path11, reasons) {
|
|
206
|
+
super(`Worktree at '${path11}' is not clean: ${reasons.join(", ")}`, "NOT_CLEAN");
|
|
207
|
+
this.path = path11;
|
|
208
208
|
this.reasons = reasons;
|
|
209
209
|
}
|
|
210
210
|
};
|
|
@@ -755,7 +755,7 @@ function defaultConsoleOutput(msg, level) {
|
|
|
755
755
|
|
|
756
756
|
// src/services/worktree-sync.service.ts
|
|
757
757
|
import * as fs6 from "fs/promises";
|
|
758
|
-
import * as
|
|
758
|
+
import * as path8 from "path";
|
|
759
759
|
import pLimit from "p-limit";
|
|
760
760
|
import * as lockfile from "proper-lockfile";
|
|
761
761
|
|
|
@@ -1007,7 +1007,7 @@ function formatTimingTable(totalDuration, phaseResults, repoName) {
|
|
|
1007
1007
|
|
|
1008
1008
|
// src/services/git.service.ts
|
|
1009
1009
|
import * as fs4 from "fs/promises";
|
|
1010
|
-
import * as
|
|
1010
|
+
import * as path6 from "path";
|
|
1011
1011
|
import simpleGit4 from "simple-git";
|
|
1012
1012
|
|
|
1013
1013
|
// src/utils/worktree-list-parser.ts
|
|
@@ -1052,11 +1052,13 @@ function parseWorktreeListPorcelain(output) {
|
|
|
1052
1052
|
}
|
|
1053
1053
|
|
|
1054
1054
|
// src/services/sparse-checkout.service.ts
|
|
1055
|
+
import * as path3 from "path";
|
|
1055
1056
|
import simpleGit from "simple-git";
|
|
1056
1057
|
var SparseCheckoutService = class {
|
|
1057
1058
|
logger;
|
|
1058
1059
|
gitFactory;
|
|
1059
1060
|
warnedConfigs = /* @__PURE__ */ new WeakSet();
|
|
1061
|
+
matcherCache = /* @__PURE__ */ new WeakMap();
|
|
1060
1062
|
constructor(logger, gitFactory) {
|
|
1061
1063
|
this.logger = logger ?? Logger.createDefault();
|
|
1062
1064
|
this.gitFactory = gitFactory ?? ((p) => simpleGit(p));
|
|
@@ -1140,11 +1142,66 @@ var SparseCheckoutService = class {
|
|
|
1140
1142
|
const bt = b.map((x) => x.trim());
|
|
1141
1143
|
return at.every((v, i) => v === bt[i]);
|
|
1142
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
|
+
}
|
|
1143
1200
|
};
|
|
1144
1201
|
|
|
1145
1202
|
// src/services/worktree-metadata.service.ts
|
|
1146
1203
|
import * as fs2 from "fs/promises";
|
|
1147
|
-
import * as
|
|
1204
|
+
import * as path4 from "path";
|
|
1148
1205
|
import simpleGit2 from "simple-git";
|
|
1149
1206
|
var WorktreeMetadataService = class {
|
|
1150
1207
|
logger;
|
|
@@ -1157,7 +1214,7 @@ var WorktreeMetadataService = class {
|
|
|
1157
1214
|
* For example: /worktrees/fix/test-branch -> test-branch (not fix/test-branch)
|
|
1158
1215
|
*/
|
|
1159
1216
|
getWorktreeDirectoryName(worktreePath) {
|
|
1160
|
-
return
|
|
1217
|
+
return path4.basename(worktreePath);
|
|
1161
1218
|
}
|
|
1162
1219
|
async getMetadataPath(bareRepoPath, worktreeName) {
|
|
1163
1220
|
if (worktreeName.includes("/") || worktreeName.includes("\\")) {
|
|
@@ -1165,7 +1222,7 @@ var WorktreeMetadataService = class {
|
|
|
1165
1222
|
`getMetadataPath requires a filesystem-safe worktree directory name, got '${worktreeName}'. Use getMetadataPathFromWorktreePath when starting from a raw branch name.`
|
|
1166
1223
|
);
|
|
1167
1224
|
}
|
|
1168
|
-
return
|
|
1225
|
+
return path4.join(
|
|
1169
1226
|
bareRepoPath,
|
|
1170
1227
|
METADATA_CONSTANTS.WORKTREE_METADATA_PATH,
|
|
1171
1228
|
worktreeName,
|
|
@@ -1178,7 +1235,7 @@ var WorktreeMetadataService = class {
|
|
|
1178
1235
|
}
|
|
1179
1236
|
async saveMetadata(bareRepoPath, worktreeName, metadata) {
|
|
1180
1237
|
const metadataPath = await this.getMetadataPath(bareRepoPath, worktreeName);
|
|
1181
|
-
await fs2.mkdir(
|
|
1238
|
+
await fs2.mkdir(path4.dirname(metadataPath), { recursive: true });
|
|
1182
1239
|
const tmpPath = `${metadataPath}.${process.pid}.${Date.now()}.tmp`;
|
|
1183
1240
|
let renamed = false;
|
|
1184
1241
|
try {
|
|
@@ -1371,7 +1428,7 @@ var WorktreeMetadataService = class {
|
|
|
1371
1428
|
|
|
1372
1429
|
// src/services/worktree-status.service.ts
|
|
1373
1430
|
import * as fs3 from "fs/promises";
|
|
1374
|
-
import * as
|
|
1431
|
+
import * as path5 from "path";
|
|
1375
1432
|
import simpleGit3 from "simple-git";
|
|
1376
1433
|
var OPERATION_FILES = [
|
|
1377
1434
|
{ file: GIT_OPERATIONS.MERGE_HEAD, type: "merge" },
|
|
@@ -1574,7 +1631,7 @@ var WorktreeStatusService = class {
|
|
|
1574
1631
|
async detectOperationFile(gitDir) {
|
|
1575
1632
|
const results = await Promise.all(
|
|
1576
1633
|
OPERATION_FILES.map(
|
|
1577
|
-
({ file }) => fs3.access(
|
|
1634
|
+
({ file }) => fs3.access(path5.join(gitDir, file)).then(
|
|
1578
1635
|
() => true,
|
|
1579
1636
|
() => false
|
|
1580
1637
|
)
|
|
@@ -1695,14 +1752,14 @@ var WorktreeStatusService = class {
|
|
|
1695
1752
|
}
|
|
1696
1753
|
}
|
|
1697
1754
|
async resolveGitDir(worktreePath) {
|
|
1698
|
-
const gitPath =
|
|
1755
|
+
const gitPath = path5.join(worktreePath, PATH_CONSTANTS.GIT_DIR);
|
|
1699
1756
|
try {
|
|
1700
1757
|
const stat4 = await fs3.stat(gitPath);
|
|
1701
1758
|
if (stat4.isFile()) {
|
|
1702
1759
|
const content = await fs3.readFile(gitPath, "utf-8");
|
|
1703
1760
|
const gitdirMatch = content.match(new RegExp(`^${GIT_CONSTANTS.GITDIR_PREFIX}\\s*(.+)$`, "m"));
|
|
1704
1761
|
if (gitdirMatch) {
|
|
1705
|
-
return
|
|
1762
|
+
return path5.resolve(worktreePath, gitdirMatch[1].trim());
|
|
1706
1763
|
}
|
|
1707
1764
|
throw new GitOperationError("resolve-git-dir", `Failed to parse gitdir from .git file at ${gitPath}`);
|
|
1708
1765
|
}
|
|
@@ -1716,7 +1773,7 @@ var WorktreeStatusService = class {
|
|
|
1716
1773
|
}
|
|
1717
1774
|
}
|
|
1718
1775
|
createGitInstance(worktreePath) {
|
|
1719
|
-
const key = `${
|
|
1776
|
+
const key = `${path5.resolve(worktreePath)}::${this.config.skipLfs ? "1" : "0"}`;
|
|
1720
1777
|
let git = this.gitInstances.get(key);
|
|
1721
1778
|
if (!git) {
|
|
1722
1779
|
git = this.config.skipLfs ? simpleGit3(worktreePath).env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit3(worktreePath);
|
|
@@ -1739,7 +1796,7 @@ var GitService = class {
|
|
|
1739
1796
|
this.config = config;
|
|
1740
1797
|
this.logger = logger ?? Logger.createDefault(void 0, config.debug);
|
|
1741
1798
|
this.bareRepoPath = this.config.bareRepoDir || getDefaultBareRepoDir(this.config.repoUrl);
|
|
1742
|
-
this.mainWorktreePath =
|
|
1799
|
+
this.mainWorktreePath = path6.join(this.config.worktreeDir, GIT_CONSTANTS.DEFAULT_BRANCH);
|
|
1743
1800
|
this.metadataService = new WorktreeMetadataService(this.logger);
|
|
1744
1801
|
this.statusService = new WorktreeStatusService({ skipLfs: this.config.skipLfs }, this.logger);
|
|
1745
1802
|
this.sparseCheckoutService = new SparseCheckoutService(this.logger);
|
|
@@ -1767,7 +1824,7 @@ var GitService = class {
|
|
|
1767
1824
|
return this.config.cloneTimeoutMs ?? DEFAULT_CONFIG.CLONE_TIMEOUT_MS;
|
|
1768
1825
|
}
|
|
1769
1826
|
getCachedGit(dirPath, useLfsSkip = false) {
|
|
1770
|
-
const key = `${
|
|
1827
|
+
const key = `${path6.resolve(dirPath)}::${useLfsSkip ? "1" : "0"}`;
|
|
1771
1828
|
let git = this.gitInstances.get(key);
|
|
1772
1829
|
if (!git) {
|
|
1773
1830
|
const block = this.getFetchTimeoutMs();
|
|
@@ -1784,10 +1841,10 @@ var GitService = class {
|
|
|
1784
1841
|
async initialize() {
|
|
1785
1842
|
const { repoUrl } = this.config;
|
|
1786
1843
|
try {
|
|
1787
|
-
await fs4.access(
|
|
1844
|
+
await fs4.access(path6.join(this.bareRepoPath, "HEAD"));
|
|
1788
1845
|
} catch {
|
|
1789
1846
|
this.logger.info(`Cloning from "${repoUrl}" as bare repository into "${this.bareRepoPath}"...`);
|
|
1790
|
-
await fs4.mkdir(
|
|
1847
|
+
await fs4.mkdir(path6.dirname(this.bareRepoPath), { recursive: true });
|
|
1791
1848
|
const cloneBlock = this.getCloneTimeoutMs();
|
|
1792
1849
|
const cloneBase = cloneBlock > 0 ? simpleGit4({ timeout: { block: cloneBlock } }) : simpleGit4();
|
|
1793
1850
|
const cloneGit = this.isLfsSkipEnabled() ? cloneBase.env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : cloneBase;
|
|
@@ -1807,17 +1864,17 @@ var GitService = class {
|
|
|
1807
1864
|
this.logger.info("Fetching remote branches...");
|
|
1808
1865
|
await bareGit.fetch(["--all"]);
|
|
1809
1866
|
this.defaultBranch = await this.detectDefaultBranch(bareGit);
|
|
1810
|
-
this.mainWorktreePath =
|
|
1867
|
+
this.mainWorktreePath = path6.join(this.config.worktreeDir, this.defaultBranch);
|
|
1811
1868
|
let needsMainWorktree = true;
|
|
1812
1869
|
try {
|
|
1813
1870
|
const worktrees = await this.getWorktreesFromBare(bareGit);
|
|
1814
|
-
needsMainWorktree = !worktrees.some((w) =>
|
|
1871
|
+
needsMainWorktree = !worktrees.some((w) => path6.resolve(w.path) === path6.resolve(this.mainWorktreePath));
|
|
1815
1872
|
} catch {
|
|
1816
1873
|
}
|
|
1817
1874
|
if (needsMainWorktree) {
|
|
1818
1875
|
this.logger.info(`Creating ${this.defaultBranch} worktree at "${this.mainWorktreePath}"...`);
|
|
1819
1876
|
await fs4.mkdir(this.config.worktreeDir, { recursive: true });
|
|
1820
|
-
const absoluteWorktreePath =
|
|
1877
|
+
const absoluteWorktreePath = path6.resolve(this.mainWorktreePath);
|
|
1821
1878
|
const branches = await bareGit.branch();
|
|
1822
1879
|
const defaultBranchExists = branches.all.includes(this.defaultBranch);
|
|
1823
1880
|
const useNoCheckoutMain = !!this.config.sparseCheckout;
|
|
@@ -1853,7 +1910,7 @@ var GitService = class {
|
|
|
1853
1910
|
}
|
|
1854
1911
|
const updatedWorktrees = await this.getWorktreesFromBare(bareGit);
|
|
1855
1912
|
const mainWorktreeRegistered = updatedWorktrees.some(
|
|
1856
|
-
(w) =>
|
|
1913
|
+
(w) => path6.resolve(w.path) === path6.resolve(this.mainWorktreePath)
|
|
1857
1914
|
);
|
|
1858
1915
|
if (!mainWorktreeRegistered) {
|
|
1859
1916
|
if (process.env.NODE_ENV !== ENV_CONSTANTS.NODE_ENV_TEST) {
|
|
@@ -1936,7 +1993,7 @@ var GitService = class {
|
|
|
1936
1993
|
const existence = await Promise.all(
|
|
1937
1994
|
lfsFileList.map(async (f) => {
|
|
1938
1995
|
try {
|
|
1939
|
-
await fs4.access(
|
|
1996
|
+
await fs4.access(path6.join(worktreePath, f));
|
|
1940
1997
|
return f;
|
|
1941
1998
|
} catch {
|
|
1942
1999
|
return null;
|
|
@@ -1964,7 +2021,7 @@ var GitService = class {
|
|
|
1964
2021
|
let allDownloaded = true;
|
|
1965
2022
|
const notDownloaded = [];
|
|
1966
2023
|
for (const file of samplesToCheck) {
|
|
1967
|
-
const filePath =
|
|
2024
|
+
const filePath = path6.join(worktreePath, file);
|
|
1968
2025
|
try {
|
|
1969
2026
|
const handle = await fs4.open(filePath, "r");
|
|
1970
2027
|
try {
|
|
@@ -2053,12 +2110,12 @@ var GitService = class {
|
|
|
2053
2110
|
}
|
|
2054
2111
|
async addWorktree(branchName, worktreePath) {
|
|
2055
2112
|
const bareGit = this.getCachedGit(this.bareRepoPath, this.isLfsSkipEnabled());
|
|
2056
|
-
const absoluteWorktreePath =
|
|
2057
|
-
await fs4.mkdir(
|
|
2113
|
+
const absoluteWorktreePath = path6.resolve(worktreePath);
|
|
2114
|
+
await fs4.mkdir(path6.dirname(absoluteWorktreePath), { recursive: true });
|
|
2058
2115
|
try {
|
|
2059
2116
|
await fs4.access(absoluteWorktreePath);
|
|
2060
2117
|
const worktrees = await this.getWorktreesFromBare(bareGit);
|
|
2061
|
-
const isValidWorktree = worktrees.some((w) =>
|
|
2118
|
+
const isValidWorktree = worktrees.some((w) => path6.resolve(w.path) === absoluteWorktreePath);
|
|
2062
2119
|
if (isValidWorktree) {
|
|
2063
2120
|
this.logger.info(` - Worktree for '${branchName}' already exists at '${absoluteWorktreePath}'`);
|
|
2064
2121
|
return;
|
|
@@ -2103,7 +2160,7 @@ var GitService = class {
|
|
|
2103
2160
|
}
|
|
2104
2161
|
if (errorMessage.includes("already registered worktree")) {
|
|
2105
2162
|
const worktrees = await this.getWorktreesFromBare(bareGit);
|
|
2106
|
-
const existingWorktree = worktrees.find((w) =>
|
|
2163
|
+
const existingWorktree = worktrees.find((w) => path6.resolve(w.path) === absoluteWorktreePath);
|
|
2107
2164
|
if (existingWorktree && !existingWorktree.isPrunable) {
|
|
2108
2165
|
this.logger.info(` - Worktree for '${branchName}' was created by concurrent operation`);
|
|
2109
2166
|
return;
|
|
@@ -2149,7 +2206,7 @@ var GitService = class {
|
|
|
2149
2206
|
try {
|
|
2150
2207
|
await fs4.access(absoluteWorktreePath);
|
|
2151
2208
|
const worktrees = await this.getWorktreesFromBare(bareGit);
|
|
2152
|
-
const isValidWorktree = worktrees.some((w) =>
|
|
2209
|
+
const isValidWorktree = worktrees.some((w) => path6.resolve(w.path) === absoluteWorktreePath);
|
|
2153
2210
|
if (isValidWorktree) {
|
|
2154
2211
|
this.logger.info(` - Worktree for '${branchName}' already exists at '${absoluteWorktreePath}'`);
|
|
2155
2212
|
return;
|
|
@@ -2179,7 +2236,7 @@ var GitService = class {
|
|
|
2179
2236
|
const fallbackErrorMessage = getErrorMessage(fallbackError);
|
|
2180
2237
|
if (fallbackErrorMessage.includes("already registered worktree")) {
|
|
2181
2238
|
const worktrees = await this.getWorktreesFromBare(bareGit);
|
|
2182
|
-
const existingWorktree = worktrees.find((w) =>
|
|
2239
|
+
const existingWorktree = worktrees.find((w) => path6.resolve(w.path) === absoluteWorktreePath);
|
|
2183
2240
|
if (existingWorktree && !existingWorktree.isPrunable) {
|
|
2184
2241
|
this.logger.info(` - Worktree for '${branchName}' was created by concurrent operation during fallback`);
|
|
2185
2242
|
return;
|
|
@@ -2402,6 +2459,23 @@ var GitService = class {
|
|
|
2402
2459
|
return false;
|
|
2403
2460
|
}
|
|
2404
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
|
+
}
|
|
2405
2479
|
async compareTreeContent(worktreePath, branch) {
|
|
2406
2480
|
const worktreeGit = this.getCachedGit(worktreePath);
|
|
2407
2481
|
try {
|
|
@@ -2486,7 +2560,7 @@ var GitService = class {
|
|
|
2486
2560
|
// src/services/path-resolution.service.ts
|
|
2487
2561
|
import { createHash } from "crypto";
|
|
2488
2562
|
import * as fs5 from "fs";
|
|
2489
|
-
import * as
|
|
2563
|
+
import * as path7 from "path";
|
|
2490
2564
|
var BRANCH_STEM_MAX = 80;
|
|
2491
2565
|
var BRANCH_HASH_LEN = 8;
|
|
2492
2566
|
var PathResolutionService = class {
|
|
@@ -2496,22 +2570,22 @@ var PathResolutionService = class {
|
|
|
2496
2570
|
return `${stem}-${hash}`;
|
|
2497
2571
|
}
|
|
2498
2572
|
getBranchWorktreePath(worktreeDir, branchName) {
|
|
2499
|
-
return
|
|
2573
|
+
return path7.join(worktreeDir, this.sanitizeBranchName(branchName));
|
|
2500
2574
|
}
|
|
2501
2575
|
resolveRealPath(inputPath) {
|
|
2502
|
-
const absolute =
|
|
2576
|
+
const absolute = path7.resolve(inputPath);
|
|
2503
2577
|
const missing = [];
|
|
2504
2578
|
let current = absolute;
|
|
2505
2579
|
while (!fs5.existsSync(current)) {
|
|
2506
|
-
const parent =
|
|
2580
|
+
const parent = path7.dirname(current);
|
|
2507
2581
|
if (parent === current) {
|
|
2508
2582
|
return absolute;
|
|
2509
2583
|
}
|
|
2510
|
-
missing.unshift(
|
|
2584
|
+
missing.unshift(path7.basename(current));
|
|
2511
2585
|
current = parent;
|
|
2512
2586
|
}
|
|
2513
2587
|
try {
|
|
2514
|
-
return
|
|
2588
|
+
return path7.join(fs5.realpathSync(current), ...missing);
|
|
2515
2589
|
} catch {
|
|
2516
2590
|
return absolute;
|
|
2517
2591
|
}
|
|
@@ -2521,7 +2595,7 @@ var PathResolutionService = class {
|
|
|
2521
2595
|
const a = fold(resolved);
|
|
2522
2596
|
const b = fold(resolvedBase);
|
|
2523
2597
|
if (a === b) return true;
|
|
2524
|
-
return a.length > b.length && a.charAt(b.length) ===
|
|
2598
|
+
return a.length > b.length && a.charAt(b.length) === path7.sep && a.startsWith(b);
|
|
2525
2599
|
}
|
|
2526
2600
|
normalizeWorktreePath(worktreePath, worktreeBaseDir) {
|
|
2527
2601
|
const resolved = this.resolveRealPath(worktreePath);
|
|
@@ -2529,7 +2603,7 @@ var PathResolutionService = class {
|
|
|
2529
2603
|
if (!this.isResolvedPathInsideBase(resolved, resolvedBase)) {
|
|
2530
2604
|
throw new Error(`Worktree path '${worktreePath}' is outside base directory '${worktreeBaseDir}'`);
|
|
2531
2605
|
}
|
|
2532
|
-
return
|
|
2606
|
+
return path7.relative(resolvedBase, resolved);
|
|
2533
2607
|
}
|
|
2534
2608
|
isPathInsideBaseDir(targetPath, baseDir) {
|
|
2535
2609
|
const resolved = this.resolveRealPath(targetPath);
|
|
@@ -2635,7 +2709,7 @@ var WorktreeSyncService = class {
|
|
|
2635
2709
|
};
|
|
2636
2710
|
}
|
|
2637
2711
|
const barePath = this.gitService.getBareRepoPath();
|
|
2638
|
-
const lockTarget =
|
|
2712
|
+
const lockTarget = path8.join(barePath, "HEAD");
|
|
2639
2713
|
try {
|
|
2640
2714
|
await fs6.access(lockTarget);
|
|
2641
2715
|
} catch {
|
|
@@ -2833,12 +2907,12 @@ var WorktreeSyncService = class {
|
|
|
2833
2907
|
}
|
|
2834
2908
|
const reservedPaths = /* @__PURE__ */ new Map();
|
|
2835
2909
|
for (const w of worktrees) {
|
|
2836
|
-
reservedPaths.set(
|
|
2910
|
+
reservedPaths.set(path8.resolve(w.path), w.branch);
|
|
2837
2911
|
}
|
|
2838
2912
|
const plan = [];
|
|
2839
2913
|
for (const branchName of newBranches) {
|
|
2840
2914
|
const worktreePath = this.pathResolution.getBranchWorktreePath(this.config.worktreeDir, branchName);
|
|
2841
|
-
const resolved =
|
|
2915
|
+
const resolved = path8.resolve(worktreePath);
|
|
2842
2916
|
const conflict = reservedPaths.get(resolved);
|
|
2843
2917
|
if (conflict && conflict !== branchName) {
|
|
2844
2918
|
this.logger.error(
|
|
@@ -3043,12 +3117,12 @@ var WorktreeSyncService = class {
|
|
|
3043
3117
|
}
|
|
3044
3118
|
async updateExistingWorktrees(worktrees, remoteBranches) {
|
|
3045
3119
|
this.logger.info("Step 4: Checking for worktrees that need updates...");
|
|
3046
|
-
const divergedDir =
|
|
3120
|
+
const divergedDir = path8.join(this.config.worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
|
|
3047
3121
|
try {
|
|
3048
3122
|
const diverged = await fs6.readdir(divergedDir);
|
|
3049
3123
|
if (diverged.length > 0) {
|
|
3050
3124
|
this.logger.info(
|
|
3051
|
-
`\u{1F4E6} Note: ${diverged.length} diverged worktree(s) in ${
|
|
3125
|
+
`\u{1F4E6} Note: ${diverged.length} diverged worktree(s) in ${path8.relative(process.cwd(), divergedDir)}`
|
|
3052
3126
|
);
|
|
3053
3127
|
}
|
|
3054
3128
|
} catch {
|
|
@@ -3078,7 +3152,23 @@ var WorktreeSyncService = class {
|
|
|
3078
3152
|
return { action: "diverged", worktree };
|
|
3079
3153
|
}
|
|
3080
3154
|
const isBehind = await this.gitService.isWorktreeBehind(worktree.path);
|
|
3081
|
-
|
|
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 };
|
|
3082
3172
|
})
|
|
3083
3173
|
)
|
|
3084
3174
|
);
|
|
@@ -3156,13 +3246,13 @@ var WorktreeSyncService = class {
|
|
|
3156
3246
|
}
|
|
3157
3247
|
async cleanupOrphanedDirectories(worktrees) {
|
|
3158
3248
|
try {
|
|
3159
|
-
const worktreeRelativePaths = worktrees.map((w) =>
|
|
3249
|
+
const worktreeRelativePaths = worktrees.map((w) => path8.relative(this.config.worktreeDir, w.path));
|
|
3160
3250
|
const allDirs = await fs6.readdir(this.config.worktreeDir);
|
|
3161
3251
|
const regularDirs = allDirs.filter((dir) => !dir.startsWith("."));
|
|
3162
3252
|
const orphanedDirs = [];
|
|
3163
3253
|
for (const dir of regularDirs) {
|
|
3164
3254
|
const isPartOfWorktree = worktreeRelativePaths.some((worktreePath) => {
|
|
3165
|
-
return worktreePath === dir || worktreePath.startsWith(dir +
|
|
3255
|
+
return worktreePath === dir || worktreePath.startsWith(dir + path8.sep);
|
|
3166
3256
|
});
|
|
3167
3257
|
if (!isPartOfWorktree) {
|
|
3168
3258
|
orphanedDirs.push(dir);
|
|
@@ -3171,7 +3261,7 @@ var WorktreeSyncService = class {
|
|
|
3171
3261
|
if (orphanedDirs.length > 0) {
|
|
3172
3262
|
this.logger.info(`Found ${orphanedDirs.length} orphaned directories: ${orphanedDirs.join(", ")}`);
|
|
3173
3263
|
for (const dir of orphanedDirs) {
|
|
3174
|
-
const dirPath =
|
|
3264
|
+
const dirPath = path8.join(this.config.worktreeDir, dir);
|
|
3175
3265
|
try {
|
|
3176
3266
|
const stat4 = await fs6.stat(dirPath);
|
|
3177
3267
|
if (stat4.isDirectory()) {
|
|
@@ -3205,7 +3295,7 @@ var WorktreeSyncService = class {
|
|
|
3205
3295
|
} else {
|
|
3206
3296
|
this.logger.info(`\u{1F512} Branch '${worktree.branch}' has diverged with local changes. Moving to diverged...`);
|
|
3207
3297
|
const divergedPath = await this.divergeWorktree(worktree.path, worktree.branch);
|
|
3208
|
-
const relativePath =
|
|
3298
|
+
const relativePath = path8.relative(process.cwd(), divergedPath);
|
|
3209
3299
|
this.logger.info(` Moved to: ${relativePath}`);
|
|
3210
3300
|
this.logger.info(` Your local changes are preserved. To review:`);
|
|
3211
3301
|
this.logger.info(` cd ${relativePath}`);
|
|
@@ -3229,12 +3319,12 @@ var WorktreeSyncService = class {
|
|
|
3229
3319
|
}
|
|
3230
3320
|
}
|
|
3231
3321
|
async divergeWorktree(worktreePath, branchName) {
|
|
3232
|
-
const divergedBaseDir =
|
|
3322
|
+
const divergedBaseDir = path8.join(this.config.worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
|
|
3233
3323
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
3234
3324
|
const uniqueSuffix = Date.now().toString(36) + Math.random().toString(36).substring(2, 7);
|
|
3235
3325
|
const safeBranchName = this.pathResolution.sanitizeBranchName(branchName);
|
|
3236
3326
|
const divergedName = `${timestamp}-${safeBranchName}-${uniqueSuffix}`;
|
|
3237
|
-
const divergedPath =
|
|
3327
|
+
const divergedPath = path8.join(divergedBaseDir, divergedName);
|
|
3238
3328
|
await fs6.mkdir(divergedBaseDir, { recursive: true });
|
|
3239
3329
|
try {
|
|
3240
3330
|
await fs6.rename(worktreePath, divergedPath);
|
|
@@ -3261,7 +3351,7 @@ var WorktreeSyncService = class {
|
|
|
3261
3351
|
Original worktree location: ${worktreePath}`
|
|
3262
3352
|
};
|
|
3263
3353
|
await fs6.writeFile(
|
|
3264
|
-
|
|
3354
|
+
path8.join(divergedPath, METADATA_CONSTANTS.DIVERGED_INFO_FILE),
|
|
3265
3355
|
JSON.stringify(metadata, null, 2)
|
|
3266
3356
|
);
|
|
3267
3357
|
return divergedPath;
|
|
@@ -3316,9 +3406,9 @@ var RepositoryContext = class {
|
|
|
3316
3406
|
discoveryCache = /* @__PURE__ */ new Map();
|
|
3317
3407
|
async loadConfig(configPath, options = {}) {
|
|
3318
3408
|
const setDefaultCurrent = options.setDefaultCurrent ?? true;
|
|
3319
|
-
const absolutePath =
|
|
3409
|
+
const absolutePath = path9.resolve(configPath);
|
|
3320
3410
|
const configFile = await this.configLoader.loadConfigFile(absolutePath);
|
|
3321
|
-
const configDir =
|
|
3411
|
+
const configDir = path9.dirname(absolutePath);
|
|
3322
3412
|
const globalDefaults = configFile.defaults;
|
|
3323
3413
|
const resolvedAll = [];
|
|
3324
3414
|
for (const repo of configFile.repositories) {
|
|
@@ -3355,7 +3445,7 @@ var RepositoryContext = class {
|
|
|
3355
3445
|
return configFile.repositories;
|
|
3356
3446
|
}
|
|
3357
3447
|
async detectFromPath(dirPath) {
|
|
3358
|
-
const absolutePath =
|
|
3448
|
+
const absolutePath = path9.resolve(dirPath);
|
|
3359
3449
|
const cached = this.discoveryCache.get(absolutePath);
|
|
3360
3450
|
if (cached && await this.isCacheFresh(cached)) {
|
|
3361
3451
|
return cached.result;
|
|
@@ -3374,8 +3464,8 @@ var RepositoryContext = class {
|
|
|
3374
3464
|
const { result, adminDir } = await this.detectFromPathUncached(absolutePath);
|
|
3375
3465
|
if (result.isWorktree && result.bareRepoPath && adminDir) {
|
|
3376
3466
|
const [worktreeHeadMtimeMs, worktreesDirMtimeMs] = await Promise.all([
|
|
3377
|
-
safeMtimeMs(
|
|
3378
|
-
safeMtimeMs(
|
|
3467
|
+
safeMtimeMs(path9.join(adminDir, "HEAD")),
|
|
3468
|
+
safeMtimeMs(path9.join(result.bareRepoPath, "worktrees"))
|
|
3379
3469
|
]);
|
|
3380
3470
|
this.discoveryCache.set(absolutePath, {
|
|
3381
3471
|
result,
|
|
@@ -3407,8 +3497,8 @@ var RepositoryContext = class {
|
|
|
3407
3497
|
return this.discoveryCache.size;
|
|
3408
3498
|
}
|
|
3409
3499
|
async discoverSiblingRepositories(currentBareRepoPath) {
|
|
3410
|
-
const repoDir =
|
|
3411
|
-
const workspaceRoot =
|
|
3500
|
+
const repoDir = path9.dirname(currentBareRepoPath);
|
|
3501
|
+
const workspaceRoot = path9.dirname(repoDir);
|
|
3412
3502
|
if (workspaceRoot === repoDir) return [];
|
|
3413
3503
|
let entries;
|
|
3414
3504
|
try {
|
|
@@ -3425,15 +3515,15 @@ var RepositoryContext = class {
|
|
|
3425
3515
|
const results = [];
|
|
3426
3516
|
await Promise.all(
|
|
3427
3517
|
entries.map(async (entry) => {
|
|
3428
|
-
const candidate =
|
|
3429
|
-
const bareCandidate =
|
|
3518
|
+
const candidate = path9.join(workspaceRoot, entry);
|
|
3519
|
+
const bareCandidate = path9.join(candidate, GIT_CONSTANTS.BARE_DIR_NAME);
|
|
3430
3520
|
try {
|
|
3431
3521
|
const stat4 = await fs7.stat(bareCandidate);
|
|
3432
3522
|
if (!stat4.isDirectory()) return;
|
|
3433
3523
|
} catch {
|
|
3434
3524
|
return;
|
|
3435
3525
|
}
|
|
3436
|
-
const resolvedBare =
|
|
3526
|
+
const resolvedBare = path9.resolve(bareCandidate);
|
|
3437
3527
|
const matchedName = configBares.get(normalizePathForCompare(resolvedBare));
|
|
3438
3528
|
results.push({
|
|
3439
3529
|
name: matchedName ?? entry,
|
|
@@ -3455,8 +3545,8 @@ var RepositoryContext = class {
|
|
|
3455
3545
|
if (Date.now() - cached.cachedAt >= DISCOVERY_CACHE_TTL_MS) return false;
|
|
3456
3546
|
if (!cached.worktreeAdminDir || !cached.result.bareRepoPath) return true;
|
|
3457
3547
|
const [currentHeadMtime, currentWorktreesDirMtime] = await Promise.all([
|
|
3458
|
-
safeMtimeMs(
|
|
3459
|
-
safeMtimeMs(
|
|
3548
|
+
safeMtimeMs(path9.join(cached.worktreeAdminDir, "HEAD")),
|
|
3549
|
+
safeMtimeMs(path9.join(cached.result.bareRepoPath, "worktrees"))
|
|
3460
3550
|
]);
|
|
3461
3551
|
return currentHeadMtime === cached.worktreeHeadMtimeMs && currentWorktreesDirMtime === cached.worktreesDirMtimeMs;
|
|
3462
3552
|
}
|
|
@@ -3497,13 +3587,13 @@ var RepositoryContext = class {
|
|
|
3497
3587
|
return unsupported("Invalid .git file format (missing gitdir line)");
|
|
3498
3588
|
}
|
|
3499
3589
|
const gitdir = gitdirMatch[1].trim();
|
|
3500
|
-
const resolvedGitdir =
|
|
3590
|
+
const resolvedGitdir = path9.isAbsolute(gitdir) ? gitdir : path9.resolve(worktreeRoot, gitdir);
|
|
3501
3591
|
const worktreesMatch = resolvedGitdir.match(/^(.+?)[/\\]worktrees[/\\][^/\\]+$/);
|
|
3502
3592
|
if (!worktreesMatch) {
|
|
3503
3593
|
return unsupported("gitdir does not follow worktree structure (missing /worktrees/<name>)");
|
|
3504
3594
|
}
|
|
3505
|
-
const bareRepoPath =
|
|
3506
|
-
const adminDir =
|
|
3595
|
+
const bareRepoPath = path9.resolve(worktreesMatch[1]);
|
|
3596
|
+
const adminDir = path9.resolve(resolvedGitdir);
|
|
3507
3597
|
let repoUrl = null;
|
|
3508
3598
|
let worktrees = [];
|
|
3509
3599
|
let currentBranch = null;
|
|
@@ -3544,7 +3634,7 @@ var RepositoryContext = class {
|
|
|
3544
3634
|
adminDir
|
|
3545
3635
|
};
|
|
3546
3636
|
}
|
|
3547
|
-
const worktreeDir =
|
|
3637
|
+
const worktreeDir = path9.dirname(worktreeRoot);
|
|
3548
3638
|
const noUrlReason = "no remote origin URL detected";
|
|
3549
3639
|
const capabilities = {
|
|
3550
3640
|
listWorktrees: { available: true },
|
|
@@ -3580,7 +3670,7 @@ var RepositoryContext = class {
|
|
|
3580
3670
|
cronSchedule: DEFAULT_CONFIG.CRON_SCHEDULE,
|
|
3581
3671
|
runOnce: true
|
|
3582
3672
|
};
|
|
3583
|
-
const detectedKey = `${AUTO_DETECT_PREFIX}${
|
|
3673
|
+
const detectedKey = `${AUTO_DETECT_PREFIX}${path9.basename(bareRepoPath)}@${bareRepoPath}`;
|
|
3584
3674
|
if (!this.repos.has(detectedKey)) {
|
|
3585
3675
|
this.repos.set(detectedKey, {
|
|
3586
3676
|
name: detectedKey,
|
|
@@ -3672,7 +3762,7 @@ function parseWorktreeList(output, currentPath) {
|
|
|
3672
3762
|
const foldedCurrent = normalizePathForCompare(currentPath);
|
|
3673
3763
|
const results = [];
|
|
3674
3764
|
for (const wt of parseWorktreeListPorcelain(output)) {
|
|
3675
|
-
const resolved =
|
|
3765
|
+
const resolved = path9.resolve(wt.path);
|
|
3676
3766
|
const branch = wt.branch ?? (wt.detached ? `(detached ${(wt.head ?? "").slice(0, 7)})` : null);
|
|
3677
3767
|
if (!branch) continue;
|
|
3678
3768
|
results.push({
|
|
@@ -3692,10 +3782,10 @@ async function safeMtimeMs(filePath) {
|
|
|
3692
3782
|
}
|
|
3693
3783
|
}
|
|
3694
3784
|
async function findWorktreeRoot(startPath) {
|
|
3695
|
-
let current =
|
|
3696
|
-
const root =
|
|
3785
|
+
let current = path9.resolve(startPath);
|
|
3786
|
+
const root = path9.parse(current).root;
|
|
3697
3787
|
while (true) {
|
|
3698
|
-
const gitPath =
|
|
3788
|
+
const gitPath = path9.join(current, ".git");
|
|
3699
3789
|
try {
|
|
3700
3790
|
const content = await fs7.readFile(gitPath, "utf-8");
|
|
3701
3791
|
return { kind: "worktree-file", worktreeRoot: current, gitFileContent: content };
|
|
@@ -3709,7 +3799,7 @@ async function findWorktreeRoot(startPath) {
|
|
|
3709
3799
|
}
|
|
3710
3800
|
}
|
|
3711
3801
|
if (current === root) return null;
|
|
3712
|
-
const parent =
|
|
3802
|
+
const parent = path9.dirname(current);
|
|
3713
3803
|
if (parent === current) return null;
|
|
3714
3804
|
current = parent;
|
|
3715
3805
|
}
|
|
@@ -3720,7 +3810,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
3720
3810
|
import { z } from "zod";
|
|
3721
3811
|
|
|
3722
3812
|
// src/mcp/handlers.ts
|
|
3723
|
-
import * as
|
|
3813
|
+
import * as path10 from "path";
|
|
3724
3814
|
import pLimit2 from "p-limit";
|
|
3725
3815
|
|
|
3726
3816
|
// src/utils/disk-space.ts
|
|
@@ -3917,7 +4007,7 @@ async function getReadyService(ctx, repoName, options = {}) {
|
|
|
3917
4007
|
}
|
|
3918
4008
|
async function ensureRepoWorktreePath(ctx, params, git) {
|
|
3919
4009
|
await ensurePathBelongsToRepo(ctx, params.path, params.repoName, git);
|
|
3920
|
-
return
|
|
4010
|
+
return path10.resolve(params.path);
|
|
3921
4011
|
}
|
|
3922
4012
|
async function ensurePathBelongsToRepo(ctx, targetPath, repoName, git) {
|
|
3923
4013
|
const discovered = ctx.getDiscoveredContext(repoName);
|
|
@@ -3978,7 +4068,7 @@ async function handleListWorktrees(ctx, params, _extra) {
|
|
|
3978
4068
|
const results = await Promise.all(
|
|
3979
4069
|
worktrees.map(
|
|
3980
4070
|
(wt) => limit(async () => {
|
|
3981
|
-
const resolvedPath =
|
|
4071
|
+
const resolvedPath = path10.resolve(wt.path);
|
|
3982
4072
|
const isCurrent = currentPath !== null && pathsEqual(wt.path, currentPath);
|
|
3983
4073
|
const [status, divergence, metadata, sizeBytes] = await Promise.all([
|
|
3984
4074
|
git.getFullWorktreeStatus(wt.path, false).catch(() => null),
|
|
@@ -4058,7 +4148,7 @@ async function handleCreateWorktree(ctx, params, _extra) {
|
|
|
4058
4148
|
return formatToolResponse({
|
|
4059
4149
|
success: true,
|
|
4060
4150
|
branchName,
|
|
4061
|
-
worktreePath:
|
|
4151
|
+
worktreePath: path10.resolve(worktreePath),
|
|
4062
4152
|
created,
|
|
4063
4153
|
pushed
|
|
4064
4154
|
});
|
|
@@ -4146,7 +4236,7 @@ async function handleLoadConfig(ctx, params, _extra) {
|
|
|
4146
4236
|
}
|
|
4147
4237
|
await ctx.loadConfig(configPath);
|
|
4148
4238
|
return formatToolResponse({
|
|
4149
|
-
configPath:
|
|
4239
|
+
configPath: path10.resolve(configPath),
|
|
4150
4240
|
currentRepository: ctx.getCurrentRepo(),
|
|
4151
4241
|
repositories: ctx.getRepositoryList()
|
|
4152
4242
|
});
|