@loicngr/kobo 1.11.39 → 1.11.41
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/AGENTS.md +101 -32
- package/CHANGELOG.md +8 -0
- package/README.md +6 -4
- package/dist/mcp-server/kobo-tasks-handlers.js +16 -9
- package/dist/mcp-server/kobo-tasks-server.js +13 -2
- package/dist/mcp-server/workspace-dialogue-client.js +28 -0
- package/dist/server/db/mcp-message-schema.js +16 -0
- package/dist/server/db/migrations.js +4 -0
- package/dist/server/db/schema.js +4 -0
- package/dist/server/db/search-schema.js +53 -0
- package/dist/server/index.js +17 -86
- package/dist/server/middleware/network-auth-middleware.js +5 -1
- package/dist/server/routes/changelog.js +4 -49
- package/dist/server/routes/git.js +2 -2
- package/dist/server/routes/images.js +65 -5
- package/dist/server/routes/mcp.js +47 -0
- package/dist/server/routes/pull-requests.js +1 -1
- package/dist/server/routes/search.js +13 -4
- package/dist/server/routes/settings.js +5 -0
- package/dist/server/routes/workspaces.js +250 -248
- package/dist/server/services/agent/engines/claude-code/engine.js +31 -33
- package/dist/server/services/agent/engines/codex/engine.js +37 -8
- package/dist/server/services/agent/engines/codex/event-mapper.js +3 -1
- package/dist/server/services/agent/engines/codex/options-builder.js +4 -3
- package/dist/server/services/agent/engines/codex/server-requests.js +8 -1
- package/dist/server/services/agent/orchestrator.js +20 -3
- package/dist/server/services/attachment-service.js +122 -0
- package/dist/server/services/change-source-branch-service.js +10 -42
- package/dist/server/services/chat-receipt-service.js +33 -0
- package/dist/server/services/cron-service.js +27 -1
- package/dist/server/services/image-service.js +16 -7
- package/dist/server/services/mcp-client-context.js +12 -0
- package/dist/server/services/mcp-connection-info-service.js +63 -0
- package/dist/server/services/mcp-message-delivery-service.js +59 -0
- package/dist/server/services/mcp-message-request-service.js +64 -0
- package/dist/server/services/pr-checkout-service.js +10 -10
- package/dist/server/services/pr-watcher-service.js +14 -9
- package/dist/server/services/search/indexer.js +232 -0
- package/dist/server/services/search/worker.js +76 -0
- package/dist/server/services/search-service.js +104 -110
- package/dist/server/services/update-check-service.js +119 -0
- package/dist/server/services/websocket-service.js +121 -35
- package/dist/server/services/workspace-dialogue-service.js +128 -0
- package/dist/server/services/workspace-message-service.js +58 -0
- package/dist/server/services/workspace-permission-policy-service.js +33 -3
- package/dist/server/services/worktree-restore-service.js +2 -2
- package/dist/server/services/worktree-service.js +197 -124
- package/dist/server/utils/bounded-process.js +137 -0
- package/dist/server/utils/git-ops.js +76 -128
- package/dist/server/utils/git-repo-lock.js +27 -13
- package/dist/shared/attachments.js +58 -0
- package/dist/shared/workspace-dialogue-tools.js +89 -0
- package/dist/shared/workspace-message-types.js +19 -0
- package/package.json +2 -1
- package/src/client/dist/pwa/assets/{ActivityFeed-Chsw1Mh7.css → ActivityFeed-BYR5x7aa.css} +1 -1
- package/src/client/dist/pwa/assets/ActivityFeed-DTIe6O9o.js +8 -0
- package/src/client/dist/pwa/assets/{ChangelogPage-D8563008.js → ChangelogPage-BWeu0YeI.js} +1 -1
- package/src/client/dist/pwa/assets/{ComparisonPanel-Bd0X08sg.js → ComparisonPanel-o_EHlTwV.js} +2 -2
- package/src/client/dist/pwa/assets/CreatePage-CGi88k6z.css +1 -0
- package/src/client/dist/pwa/assets/CreatePage-DbY5gBv8.js +2 -0
- package/src/client/dist/pwa/assets/DashboardPage-CIo_gesD.js +1 -0
- package/src/client/dist/pwa/assets/{DiffViewer-DnR79ycb.css → DiffViewer-DEPOa3D7.css} +1 -1
- package/src/client/dist/pwa/assets/DiffViewer-DEcf48KI.js +8 -0
- package/src/client/dist/pwa/assets/{DocumentsPanel-BJsJ0fNs.js → DocumentsPanel-D9vcB91W.js} +1 -1
- package/src/client/dist/pwa/assets/{DrawerToggleButton-BVLO5K5d.js → DrawerToggleButton-Ca0_CxSl.js} +1 -1
- package/src/client/dist/pwa/assets/GitPanel-DCmB2R5i.js +3 -0
- package/src/client/dist/pwa/assets/{HealthPage-RFZS7Lao.js → HealthPage-_q3vy_xn.js} +1 -1
- package/src/client/dist/pwa/assets/MainLayout-Duwnq3cc.js +2 -0
- package/src/client/dist/pwa/assets/{QBtnDropdown-X8oNU5Mb.js → QBtnDropdown-BeiZIHVA.js} +1 -1
- package/src/client/dist/pwa/assets/{QDrawer-Cij17skX.js → QDrawer-Fj3M9QGD.js} +1 -1
- package/src/client/dist/pwa/assets/QSelect-DtSUuK8t.js +1 -0
- package/src/client/dist/pwa/assets/{QTooltip-CwcCGSWq.js → QTooltip-DwkX8eQ6.js} +1 -1
- package/src/client/dist/pwa/assets/SchedulePanel-BAA4Aajn.css +1 -0
- package/src/client/dist/pwa/assets/SchedulePanel-BlZDd7TK.js +1 -0
- package/src/client/dist/pwa/assets/SearchPage-BGc1oQSM.css +1 -0
- package/src/client/dist/pwa/assets/SearchPage-BGi1yRxU.js +1 -0
- package/src/client/dist/pwa/assets/SettingsPage-DnO-ImWE.js +9 -0
- package/src/client/dist/pwa/assets/SettingsPage-sVlsJQ0b.css +1 -0
- package/src/client/dist/pwa/assets/{SplitWorkspacePage-D7lGFRKs.js → SplitWorkspacePage-vz6jN1DM.js} +1 -1
- package/src/client/dist/pwa/assets/{TerminalPanel-DI8DHb9e.js → TerminalPanel-IdPIGp2_.js} +9 -9
- package/src/client/dist/pwa/assets/{TouchPan-B3nrkCY8.js → TouchPan-DTJqxVp9.js} +1 -1
- package/src/client/dist/pwa/assets/{TourReplayButton-DU1UShMY.js → TourReplayButton-Bj2GGyhU.js} +1 -1
- package/src/client/dist/pwa/assets/{WorkspaceAttentionLabels-C-yN5IX8.js → WorkspaceAttentionLabels-BBaGZlwv.js} +1 -1
- package/src/client/dist/pwa/assets/WorkspacePage-DCg9zSxz.css +1 -0
- package/src/client/dist/pwa/assets/WorkspacePage-Dy0spsfi.js +4 -0
- package/src/client/dist/pwa/assets/{browser-f9MHQjfi.js → browser-BFj5wwbS.js} +1 -1
- package/src/client/dist/pwa/assets/{build-path-tree-BWBzP8os.js → build-path-tree-C-HSiySk.js} +1 -1
- package/src/client/dist/pwa/assets/de-BdLe7apJ.js +17 -0
- package/src/client/dist/pwa/assets/{documents-C2cLnEBQ.js → documents-C2Xnjh9Z.js} +1 -1
- package/src/client/dist/pwa/assets/{editor.api-sw146MWN.js → editor.api-BbKfnNmv.js} +1 -1
- package/src/client/dist/pwa/assets/{efforts-C5ZZgCH4.js → efforts-BM7fdaaL.js} +1 -1
- package/src/client/dist/pwa/assets/engineFeatures-b74YRODT.js +1 -0
- package/src/client/dist/pwa/assets/es-C1LM-3aq.js +17 -0
- package/src/client/dist/pwa/assets/expand-template-DiDWiIkx.js +1 -0
- package/src/client/dist/pwa/assets/{format-BwWP6Aay.js → format-CnbUxAmw.js} +1 -1
- package/src/client/dist/pwa/assets/{formatters-DyWtO-nR.js → formatters-B2uUIbAY.js} +1 -1
- package/src/client/dist/pwa/assets/fr-KCK6r5ga.js +17 -0
- package/src/client/dist/pwa/assets/{freemarker2-DZ5Hm8UH.js → freemarker2-Cu1vGY0x.js} +1 -1
- package/src/client/dist/pwa/assets/{handlebars-BRUGpTA7.js → handlebars-CETuqLyz.js} +1 -1
- package/src/client/dist/pwa/assets/{html-DNOayNNs.js → html-BTNpr39P.js} +1 -1
- package/src/client/dist/pwa/assets/i18n-BkUirOzl.js +1 -0
- package/src/client/dist/pwa/assets/index-mvh5w1cM.js +20 -0
- package/src/client/dist/pwa/assets/it-BM6t0IGU.js +17 -0
- package/src/client/dist/pwa/assets/{javascript-C0tDQU3z.js → javascript-C7Ncb6V7.js} +1 -1
- package/src/client/dist/pwa/assets/{liquid-CN46O6OF.js → liquid-D0hUdg4x.js} +1 -1
- package/src/client/dist/pwa/assets/{mdx-DM-lrr8Q.js → mdx-BK5H1Xst.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco-contributions-CkN7y-8c.js → monaco-contributions-BBW6JNgz.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco.contribution-2BcAOLjc.js → monaco.contribution-1LZrAlY4.js} +2 -2
- package/src/client/dist/pwa/assets/network-auth-CVGS__6a.js +1 -0
- package/src/client/dist/pwa/assets/{notify-theme-a91Tcr6o.js → notify-theme-DIKLxr8F.js} +1 -1
- package/src/client/dist/pwa/assets/{pending-diff-open-DOtELZfB.js → pending-diff-open-B02zuH1j.js} +1 -1
- package/src/client/dist/pwa/assets/{permissionModes-C-9Crj-c.js → permissionModes-C3otN1Vk.js} +1 -1
- package/src/client/dist/pwa/assets/{project-color-BRe4oyNx.js → project-color-BUcW_eZA.js} +1 -1
- package/src/client/dist/pwa/assets/{python-BRpJG6sZ.js → python-DEeUVvnp.js} +1 -1
- package/src/client/dist/pwa/assets/{razor-CehCmvHl.js → razor-CG6cL8ya.js} +1 -1
- package/src/client/dist/pwa/assets/{render-chat-markdown-DlbQLZ02.js → render-chat-markdown-Ct_T5TPN.js} +1 -1
- package/src/client/dist/pwa/assets/{typescript-Dt_8mVuj.js → typescript-CSQAg5UC.js} +1 -1
- package/src/client/dist/pwa/assets/use-search-index-status-DtLzygcy.js +1 -0
- package/src/client/dist/pwa/assets/use-tours-_6HwJ_Rp.js +2 -0
- package/src/client/dist/pwa/assets/{workspace-templates-GCObcjO6.js → workspace-templates-B8WWsWEI.js} +1 -1
- package/src/client/dist/pwa/assets/{xml-_gOwdzB4.js → xml-BSB4-DXR.js} +1 -1
- package/src/client/dist/pwa/assets/{yaml-BxeBqpGf.js → yaml-CSGKph85.js} +1 -1
- package/src/client/dist/pwa/index.html +1 -1
- package/src/client/dist/pwa/sw.js +1 -1
- package/src/mcp-server/README.md +169 -9
- package/src/mcp-server/kobo-tasks-handlers.ts +14 -11
- package/src/mcp-server/kobo-tasks-server.ts +19 -2
- package/src/mcp-server/workspace-dialogue-client.ts +34 -0
- package/src/client/dist/pwa/assets/ActivityFeed-BzzvcOmq.js +0 -8
- package/src/client/dist/pwa/assets/CreatePage-A3pVradx.css +0 -1
- package/src/client/dist/pwa/assets/CreatePage-Bon8qIka.js +0 -2
- package/src/client/dist/pwa/assets/DashboardPage-DYxdxEul.js +0 -1
- package/src/client/dist/pwa/assets/DiffViewer-BpsLNxnj.js +0 -8
- package/src/client/dist/pwa/assets/GitPanel-Cal_wzC0.js +0 -3
- package/src/client/dist/pwa/assets/MainLayout-vvyiRcql.js +0 -2
- package/src/client/dist/pwa/assets/QSelect-xd-AlGo_.js +0 -1
- package/src/client/dist/pwa/assets/SchedulePanel-BunlzsOY.css +0 -1
- package/src/client/dist/pwa/assets/SchedulePanel-CXRUsz6G.js +0 -1
- package/src/client/dist/pwa/assets/SearchPage-CVgRa025.css +0 -1
- package/src/client/dist/pwa/assets/SearchPage-DopbgLxh.js +0 -1
- package/src/client/dist/pwa/assets/SettingsPage-Do9d0Pov.css +0 -1
- package/src/client/dist/pwa/assets/SettingsPage-Jx6TnOzR.js +0 -9
- package/src/client/dist/pwa/assets/WorkspacePage-BAC6u9OP.css +0 -1
- package/src/client/dist/pwa/assets/WorkspacePage-DKRpdpPg.js +0 -4
- package/src/client/dist/pwa/assets/de-QkDLrcGq.js +0 -17
- package/src/client/dist/pwa/assets/engineFeatures-CAVMDawq.js +0 -1
- package/src/client/dist/pwa/assets/es-BgQFKlo8.js +0 -17
- package/src/client/dist/pwa/assets/expand-template-B6xAlAaM.js +0 -1
- package/src/client/dist/pwa/assets/fr-DAdLWjNM.js +0 -17
- package/src/client/dist/pwa/assets/i18n-CBez_vip.js +0 -1
- package/src/client/dist/pwa/assets/index-VL3fI9a3.js +0 -20
- package/src/client/dist/pwa/assets/it-Bumaewp1.js +0 -17
- package/src/client/dist/pwa/assets/network-auth-KYS4snQH.js +0 -1
- package/src/client/dist/pwa/assets/use-tours-CSVM_54K.js +0 -2
|
@@ -1,19 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import fs from 'node:fs';
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
5
|
+
import { runBoundedProcess } from '../utils/bounded-process.js';
|
|
4
6
|
import { isGitBranchExistsError } from '../utils/git-ops.js';
|
|
5
7
|
import { withGitRepoLock } from '../utils/git-repo-lock.js';
|
|
6
8
|
import { resolveWorkspaceWorktreePath, resolveWorktreesRoot } from '../utils/worktree-paths.js';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
+
const READ_TIMEOUT_MS = 10_000;
|
|
10
|
+
const MUTATION_TIMEOUT_MS = 5 * 60_000;
|
|
11
|
+
async function git(repoPath, args, timeoutMs = READ_TIMEOUT_MS) {
|
|
12
|
+
return (await runBoundedProcess('git', args, {
|
|
9
13
|
cwd: repoPath,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
// "Permission denied", and permission-failure detection silently misses it.
|
|
15
|
-
env: { ...process.env, LC_ALL: 'C', LANG: 'C' },
|
|
16
|
-
}).trim();
|
|
14
|
+
timeoutMs,
|
|
15
|
+
stdoutLimit: 64 * 1024 * 1024,
|
|
16
|
+
env: { ...process.env, LC_ALL: 'C', LANG: 'C', GIT_TERMINAL_PROMPT: '0' },
|
|
17
|
+
})).replace(/\n$/, '');
|
|
17
18
|
}
|
|
18
19
|
/** True when an exec/git error message indicates a filesystem permission failure.
|
|
19
20
|
* git runs under LC_ALL=C (English), but we also match common localized phrasings
|
|
@@ -26,20 +27,57 @@ export function buildDockerChownArgs(worktreePath, uid, gid, image) {
|
|
|
26
27
|
return ['run', '--rm', '-v', `${worktreePath}:/w`, image, 'chown', '-R', `${uid}:${gid}`, '/w'];
|
|
27
28
|
}
|
|
28
29
|
const DEFAULT_CLEANUP_IMAGE = 'alpine';
|
|
29
|
-
function isDockerAvailable() {
|
|
30
|
+
async function isDockerAvailable() {
|
|
30
31
|
try {
|
|
31
|
-
|
|
32
|
+
await runBoundedProcess('docker', ['version'], { timeoutMs: READ_TIMEOUT_MS });
|
|
32
33
|
return true;
|
|
33
34
|
}
|
|
34
35
|
catch {
|
|
35
36
|
return false;
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
|
-
function reclaimWorktreeOwnershipViaDocker(worktreePath, uid, gid, image) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
async function reclaimWorktreeOwnershipViaDocker(worktreePath, uid, gid, image) {
|
|
40
|
+
const containerName = `kobo-cleanup-${randomUUID()}`;
|
|
41
|
+
const args = buildDockerChownArgs(worktreePath, uid, gid, image);
|
|
42
|
+
args.splice(1, 0, '--name', containerName);
|
|
43
|
+
let failure;
|
|
44
|
+
try {
|
|
45
|
+
await runBoundedProcess('docker', args, { timeoutMs: MUTATION_TIMEOUT_MS });
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
failure = err;
|
|
49
|
+
}
|
|
50
|
+
// Docker containers belong to the daemon, not the CLI's process group.
|
|
51
|
+
// A killed/disconnected `docker run` cannot prove that chown has stopped.
|
|
52
|
+
let stopRequested = false;
|
|
53
|
+
let warned = false;
|
|
54
|
+
for (;;) {
|
|
55
|
+
try {
|
|
56
|
+
const state = await runBoundedProcess('docker', ['ps', '-a', '--filter', `name=^/${containerName}$`, '--format', '{{.ID}} {{.State}}'], { timeoutMs: READ_TIMEOUT_MS, stdoutLimit: 8192 });
|
|
57
|
+
const active = state
|
|
58
|
+
.trim()
|
|
59
|
+
.split('\n')
|
|
60
|
+
.filter(Boolean)
|
|
61
|
+
.some((line) => !/ (?:exited|dead|created)$/.test(line));
|
|
62
|
+
if (!active)
|
|
63
|
+
break;
|
|
64
|
+
try {
|
|
65
|
+
await runBoundedProcess('docker', stopRequested ? ['kill', containerName] : ['stop', '--time', '5', containerName], { timeoutMs: READ_TIMEOUT_MS });
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
stopRequested = true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
if (!warned) {
|
|
73
|
+
warned = true;
|
|
74
|
+
console.error(`[worktree] Cannot yet confirm cleanup container '${containerName}' stopped; retaining ownership:`, err);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
await delay(1000);
|
|
78
|
+
}
|
|
79
|
+
if (failure)
|
|
80
|
+
throw failure;
|
|
43
81
|
}
|
|
44
82
|
function getExcludeFilePath(projectPath) {
|
|
45
83
|
return path.join(projectPath, '.git', 'info', 'exclude');
|
|
@@ -50,68 +88,99 @@ function projectRelativeWorktreePath(projectPath, worktreePath) {
|
|
|
50
88
|
return null;
|
|
51
89
|
return relativePath;
|
|
52
90
|
}
|
|
53
|
-
function addToExclude(projectPath, worktreePath, excludeFile = getExcludeFilePath(projectPath)) {
|
|
91
|
+
async function addToExclude(projectPath, worktreePath, excludeFile = getExcludeFilePath(projectPath)) {
|
|
54
92
|
const relativePath = projectRelativeWorktreePath(projectPath, worktreePath);
|
|
55
93
|
if (!relativePath)
|
|
56
94
|
return;
|
|
57
95
|
// Ensure the .git/info directory exists
|
|
58
96
|
const infoDir = path.dirname(excludeFile);
|
|
59
|
-
if (!
|
|
60
|
-
fs.
|
|
97
|
+
if (!(await restorePathExists(infoDir))) {
|
|
98
|
+
await fs.mkdir(infoDir, { recursive: true });
|
|
61
99
|
}
|
|
62
100
|
const entry = `/${relativePath}`;
|
|
63
101
|
let current = '';
|
|
64
|
-
if (
|
|
65
|
-
current = fs.
|
|
102
|
+
if (await restorePathExists(excludeFile)) {
|
|
103
|
+
current = await fs.readFile(excludeFile, 'utf-8');
|
|
66
104
|
}
|
|
67
105
|
if (!current.split('\n').includes(entry)) {
|
|
68
106
|
const newContent = current.endsWith('\n') || current === '' ? `${current}${entry}\n` : `${current}\n${entry}\n`;
|
|
69
|
-
fs.
|
|
107
|
+
await fs.writeFile(excludeFile, newContent, 'utf-8');
|
|
70
108
|
}
|
|
71
109
|
}
|
|
72
|
-
function removeFromExclude(projectPath, worktreePath) {
|
|
110
|
+
async function removeFromExclude(projectPath, worktreePath) {
|
|
73
111
|
const relativePath = projectRelativeWorktreePath(projectPath, worktreePath);
|
|
74
112
|
if (!relativePath)
|
|
75
113
|
return;
|
|
76
114
|
const excludeFile = getExcludeFilePath(projectPath);
|
|
77
|
-
if (!
|
|
115
|
+
if (!(await restorePathExists(excludeFile)))
|
|
78
116
|
return;
|
|
79
117
|
const entry = `/${relativePath}`;
|
|
80
|
-
const lines = fs.
|
|
118
|
+
const lines = (await fs.readFile(excludeFile, 'utf-8')).split('\n');
|
|
81
119
|
const filtered = lines.filter((line) => line !== entry);
|
|
82
120
|
const trimmed = filtered.join('\n').replace(/\n+$/, '');
|
|
83
|
-
fs.
|
|
121
|
+
await fs.writeFile(excludeFile, trimmed ? `${trimmed}\n` : '', 'utf-8');
|
|
84
122
|
}
|
|
85
123
|
/** Create a git worktree for the given branch, based on `baseRef`.
|
|
86
124
|
* `baseRef` is passed explicitly by the caller: `origin/<sourceBranch>` in the
|
|
87
125
|
* nominal path, or the local `<sourceBranch>` when origin is unreachable.
|
|
88
126
|
* Returns the worktree path and which base was used. */
|
|
89
127
|
export function createWorktree(projectPath, branchName, baseRef, worktreesPath, projectSlug, explicitPath) {
|
|
128
|
+
return withGitRepoLock(projectPath, () => createWorktreeUnlocked(projectPath, branchName, baseRef, worktreesPath, projectSlug, explicitPath));
|
|
129
|
+
}
|
|
130
|
+
/** A checkout was created even though a Git hook or subsequent bookkeeping failed. */
|
|
131
|
+
export class WorktreeCreationError extends Error {
|
|
132
|
+
worktreePath;
|
|
133
|
+
branchCreated;
|
|
134
|
+
constructor(worktreePath, branchCreated, cause) {
|
|
135
|
+
super(cause instanceof Error ? cause.message : String(cause), { cause });
|
|
136
|
+
this.worktreePath = worktreePath;
|
|
137
|
+
this.branchCreated = branchCreated;
|
|
138
|
+
this.name = 'WorktreeCreationError';
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/** Create a worktree while the caller already owns the shared repository lock. */
|
|
142
|
+
export async function createWorktreeUnlocked(projectPath, branchName, baseRef, worktreesPath, projectSlug, explicitPath) {
|
|
90
143
|
const worktreesDir = resolveWorktreesRoot(projectPath, worktreesPath);
|
|
91
|
-
if (!
|
|
92
|
-
fs.
|
|
144
|
+
if (!(await restorePathExists(worktreesDir))) {
|
|
145
|
+
await fs.mkdir(worktreesDir, { recursive: true });
|
|
93
146
|
}
|
|
94
147
|
const worktreePath = explicitPath || resolveWorkspaceWorktreePath(projectPath, branchName, worktreesPath, projectSlug);
|
|
95
148
|
const base = baseRef.startsWith('origin/') ? 'origin' : 'local';
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
|
|
149
|
+
// Capture ownership evidence before Git runs: a failing post-checkout hook
|
|
150
|
+
// can leave a fully registered checkout even though `git worktree add` fails.
|
|
151
|
+
const pathExisted = await restorePathExists(worktreePath);
|
|
152
|
+
const registeredBefore = (await listWorktrees(projectPath)).some((entry) => path.resolve(entry.path) === path.resolve(worktreePath));
|
|
153
|
+
const branchRef = `refs/heads/${branchName}`;
|
|
154
|
+
const branchExisted = (await git(projectPath, ['for-each-ref', '--format=%(refname)', branchRef]))
|
|
155
|
+
.split('\n')
|
|
156
|
+
.includes(branchRef);
|
|
99
157
|
let branchCreated = true;
|
|
100
158
|
try {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
159
|
+
try {
|
|
160
|
+
await git(projectPath, ['worktree', 'add', '-b', branchName, worktreePath, baseRef], MUTATION_TIMEOUT_MS);
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
164
|
+
if (!isGitBranchExistsError(message))
|
|
165
|
+
throw err;
|
|
108
166
|
branchCreated = false;
|
|
167
|
+
await git(projectPath, ['worktree', 'add', worktreePath, branchName], MUTATION_TIMEOUT_MS);
|
|
109
168
|
}
|
|
110
|
-
|
|
111
|
-
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
if (!pathExisted &&
|
|
172
|
+
!registeredBefore &&
|
|
173
|
+
(await isMatchingWorkspaceWorktree({ projectPath, worktreePath, workingBranch: branchName }))) {
|
|
174
|
+
throw new WorktreeCreationError(worktreePath, branchCreated && !branchExisted, err);
|
|
112
175
|
}
|
|
176
|
+
throw new Error(`Failed to create worktree for branch '${branchName}': ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
await addToExclude(projectPath, worktreePath);
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
throw new WorktreeCreationError(worktreePath, branchCreated, err);
|
|
113
183
|
}
|
|
114
|
-
addToExclude(projectPath, worktreePath);
|
|
115
184
|
return { worktreePath, base, branchCreated };
|
|
116
185
|
}
|
|
117
186
|
/** `git worktree remove` drops the administrative entry even when it fails to
|
|
@@ -122,14 +191,14 @@ export function createWorktree(projectPath, branchName, baseRef, worktreesPath,
|
|
|
122
191
|
*
|
|
123
192
|
* Callers hold the repository lock: acquiring it again here would deadlock on
|
|
124
193
|
* the same key, since the chain is strictly sequential per common git dir. */
|
|
125
|
-
function assertWorktreeGone(projectPath, worktreePath) {
|
|
194
|
+
async function assertWorktreeGone(projectPath, worktreePath) {
|
|
126
195
|
try {
|
|
127
|
-
git(projectPath, ['worktree', 'prune']);
|
|
196
|
+
await git(projectPath, ['worktree', 'prune']);
|
|
128
197
|
}
|
|
129
198
|
catch (err) {
|
|
130
199
|
console.warn(`[worktree] prune after removing '${worktreePath}' failed:`, err instanceof Error ? err.message : err);
|
|
131
200
|
}
|
|
132
|
-
if (
|
|
201
|
+
if (await restorePathExists(worktreePath)) {
|
|
133
202
|
throw new Error(`Failed to remove worktree '${worktreePath}': the directory is still on disk after removal`);
|
|
134
203
|
}
|
|
135
204
|
}
|
|
@@ -149,28 +218,28 @@ export function removeWorktree(projectPath, worktreePath) {
|
|
|
149
218
|
return withGitRepoLock(projectPath, () => removeWorktreeUnlocked(projectPath, worktreePath));
|
|
150
219
|
}
|
|
151
220
|
/** Remove a worktree. Call ONLY while already holding the repository lock. */
|
|
152
|
-
export function removeWorktreeUnlocked(projectPath, worktreePath) {
|
|
221
|
+
export async function removeWorktreeUnlocked(projectPath, worktreePath) {
|
|
153
222
|
try {
|
|
154
|
-
git(projectPath, ['worktree', 'remove', worktreePath, '--force']);
|
|
223
|
+
await git(projectPath, ['worktree', 'remove', worktreePath, '--force'], MUTATION_TIMEOUT_MS);
|
|
155
224
|
}
|
|
156
225
|
catch (err) {
|
|
157
226
|
const message = err instanceof Error ? err.message : String(err);
|
|
158
227
|
const uid = typeof process.getuid === 'function' ? process.getuid() : null;
|
|
159
228
|
const gid = typeof process.getgid === 'function' ? process.getgid() : null;
|
|
160
|
-
if (isPermissionError(message) && uid != null && gid != null && isDockerAvailable()) {
|
|
229
|
+
if (isPermissionError(message) && uid != null && gid != null && (await isDockerAvailable())) {
|
|
161
230
|
const image = process.env.KOBO_WORKTREE_CLEANUP_IMAGE || DEFAULT_CLEANUP_IMAGE;
|
|
162
231
|
console.warn(`[worktree] '${worktreePath}' has root-owned files (permission denied); reclaiming ownership via Docker (${image})…`);
|
|
163
232
|
try {
|
|
164
|
-
reclaimWorktreeOwnershipViaDocker(worktreePath, uid, gid, image);
|
|
233
|
+
await reclaimWorktreeOwnershipViaDocker(worktreePath, uid, gid, image);
|
|
165
234
|
// The first `git worktree remove` already de-registered this worktree (it
|
|
166
235
|
// drops the admin entry even when the directory rm fails on permission), so
|
|
167
236
|
// retrying it errors with "is not a working tree". Now that we own the files,
|
|
168
237
|
// delete the directory directly and prune any dangling worktree metadata.
|
|
169
|
-
fs.
|
|
170
|
-
git(projectPath, ['worktree', 'prune']);
|
|
238
|
+
await fs.rm(worktreePath, { recursive: true, force: true });
|
|
239
|
+
await git(projectPath, ['worktree', 'prune']);
|
|
171
240
|
console.log(`[worktree] Docker cleanup succeeded; removed '${worktreePath}'`);
|
|
172
|
-
removeFromExclude(projectPath, worktreePath);
|
|
173
|
-
assertWorktreeGone(projectPath, worktreePath);
|
|
241
|
+
await removeFromExclude(projectPath, worktreePath);
|
|
242
|
+
await assertWorktreeGone(projectPath, worktreePath);
|
|
174
243
|
return;
|
|
175
244
|
}
|
|
176
245
|
catch (retryErr) {
|
|
@@ -181,20 +250,20 @@ export function removeWorktreeUnlocked(projectPath, worktreePath) {
|
|
|
181
250
|
}
|
|
182
251
|
throw new Error(`Failed to remove worktree '${worktreePath}': ${message}`);
|
|
183
252
|
}
|
|
184
|
-
removeFromExclude(projectPath, worktreePath);
|
|
185
|
-
assertWorktreeGone(projectPath, worktreePath);
|
|
253
|
+
await removeFromExclude(projectPath, worktreePath);
|
|
254
|
+
await assertWorktreeGone(projectPath, worktreePath);
|
|
186
255
|
}
|
|
187
256
|
/** List all git worktrees for a repository by parsing `git worktree list --porcelain`. */
|
|
188
|
-
export function listWorktrees(projectPath) {
|
|
189
|
-
const output = git(projectPath, ['worktree', 'list', '--porcelain']);
|
|
257
|
+
export async function listWorktrees(projectPath) {
|
|
258
|
+
const output = await git(projectPath, ['worktree', 'list', '--porcelain', '-z']);
|
|
190
259
|
const worktrees = [];
|
|
191
|
-
const blocks = output.split('\
|
|
260
|
+
const blocks = output.split('\0\0').filter(Boolean);
|
|
192
261
|
for (const block of blocks) {
|
|
193
|
-
const lines = block.split('\
|
|
262
|
+
const lines = block.split('\0');
|
|
194
263
|
const worktree = {};
|
|
195
264
|
for (const line of lines) {
|
|
196
265
|
if (line.startsWith('worktree ')) {
|
|
197
|
-
worktree.path = line.slice('worktree '.length)
|
|
266
|
+
worktree.path = line.slice('worktree '.length);
|
|
198
267
|
}
|
|
199
268
|
else if (line.startsWith('HEAD ')) {
|
|
200
269
|
worktree.head = line.slice('HEAD '.length).trim();
|
|
@@ -219,27 +288,27 @@ export function listWorktrees(projectPath) {
|
|
|
219
288
|
return worktrees;
|
|
220
289
|
}
|
|
221
290
|
/** Check whether a worktree for the given branch already exists. */
|
|
222
|
-
export function worktreeExists(projectPath, branchName) {
|
|
291
|
+
export async function worktreeExists(projectPath, branchName) {
|
|
223
292
|
try {
|
|
224
|
-
const worktrees = listWorktrees(projectPath);
|
|
293
|
+
const worktrees = await listWorktrees(projectPath);
|
|
225
294
|
return worktrees.some((wt) => wt.branch === branchName);
|
|
226
295
|
}
|
|
227
296
|
catch {
|
|
228
297
|
return false;
|
|
229
298
|
}
|
|
230
299
|
}
|
|
231
|
-
function canonicalize(p) {
|
|
300
|
+
async function canonicalize(p) {
|
|
232
301
|
try {
|
|
233
|
-
return fs.
|
|
302
|
+
return await fs.realpath(p);
|
|
234
303
|
}
|
|
235
304
|
catch {
|
|
236
305
|
return p;
|
|
237
306
|
}
|
|
238
307
|
}
|
|
239
|
-
function detectSourceBranch(projectPath, worktreePath, branch) {
|
|
308
|
+
async function detectSourceBranch(projectPath, worktreePath, branch) {
|
|
240
309
|
// 1. Branch's tracked upstream (configured locally)
|
|
241
310
|
try {
|
|
242
|
-
const upstream = git(worktreePath, ['config', '--get', `branch.${branch}.merge`]);
|
|
311
|
+
const upstream = await git(worktreePath, ['config', '--get', `branch.${branch}.merge`]);
|
|
243
312
|
if (upstream)
|
|
244
313
|
return upstream.replace(/^refs\/heads\//, '');
|
|
245
314
|
}
|
|
@@ -248,7 +317,7 @@ function detectSourceBranch(projectPath, worktreePath, branch) {
|
|
|
248
317
|
}
|
|
249
318
|
// 2. Repo's default branch (origin/HEAD)
|
|
250
319
|
try {
|
|
251
|
-
const head = git(projectPath, ['symbolic-ref', 'refs/remotes/origin/HEAD']);
|
|
320
|
+
const head = await git(projectPath, ['symbolic-ref', 'refs/remotes/origin/HEAD']);
|
|
252
321
|
if (head)
|
|
253
322
|
return head.replace(/^refs\/remotes\/origin\//, '');
|
|
254
323
|
}
|
|
@@ -264,19 +333,23 @@ function detectSourceBranch(projectPath, worktreePath, branch) {
|
|
|
264
333
|
* branch to anchor a workspace to). Both sides of the path comparison are
|
|
265
334
|
* canonicalized to defeat symlinks / trailing-slash variants.
|
|
266
335
|
*/
|
|
267
|
-
export function listOrphanWorktrees(projectPath, attachedPaths) {
|
|
268
|
-
const canonAttached = new Set(Array.from(attachedPaths).map(canonicalize));
|
|
269
|
-
const canonProject = canonicalize(projectPath);
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
336
|
+
export async function listOrphanWorktrees(projectPath, attachedPaths) {
|
|
337
|
+
const canonAttached = new Set(await Promise.all(Array.from(attachedPaths).map(canonicalize)));
|
|
338
|
+
const canonProject = await canonicalize(projectPath);
|
|
339
|
+
const orphans = [];
|
|
340
|
+
for (const worktree of await listWorktrees(projectPath)) {
|
|
341
|
+
const canonicalPath = await canonicalize(worktree.path);
|
|
342
|
+
if (canonicalPath === canonProject ||
|
|
343
|
+
canonAttached.has(canonicalPath) ||
|
|
344
|
+
!worktree.branch ||
|
|
345
|
+
worktree.branch === '(detached HEAD)')
|
|
346
|
+
continue;
|
|
347
|
+
orphans.push({
|
|
348
|
+
...worktree,
|
|
349
|
+
suggestedSourceBranch: await detectSourceBranch(projectPath, worktree.path, worktree.branch),
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
return orphans;
|
|
280
353
|
}
|
|
281
354
|
export class WorktreeCheckoutError extends Error {
|
|
282
355
|
code;
|
|
@@ -287,18 +360,17 @@ export class WorktreeCheckoutError extends Error {
|
|
|
287
360
|
}
|
|
288
361
|
}
|
|
289
362
|
const restoreGitEnv = () => ({ ...process.env, LC_ALL: 'C', LANG: 'C', GIT_TERMINAL_PROMPT: '0' });
|
|
290
|
-
function restoreGit(cwd, args) {
|
|
291
|
-
return
|
|
363
|
+
async function restoreGit(cwd, args) {
|
|
364
|
+
return (await runBoundedProcess('git', ['-c', 'core.hooksPath=/dev/null', ...args], {
|
|
292
365
|
cwd,
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
timeout: 30_000,
|
|
366
|
+
timeoutMs: args[0] === 'fetch' || (args[0] === 'worktree' && args[1] === 'add') ? MUTATION_TIMEOUT_MS : READ_TIMEOUT_MS,
|
|
367
|
+
stdoutLimit: 64 * 1024 * 1024,
|
|
296
368
|
env: restoreGitEnv(),
|
|
297
|
-
}).replace(/\n$/, '');
|
|
369
|
+
})).replace(/\n$/, '');
|
|
298
370
|
}
|
|
299
371
|
/** NUL delimiters preserve quoted, whitespace and newline-containing worktree paths. */
|
|
300
|
-
function restoreRegistrations(projectPath) {
|
|
301
|
-
return restoreGit(projectPath, ['worktree', 'list', '--porcelain', '-z'])
|
|
372
|
+
async function restoreRegistrations(projectPath) {
|
|
373
|
+
return (await restoreGit(projectPath, ['worktree', 'list', '--porcelain', '-z']))
|
|
302
374
|
.split('\0\0')
|
|
303
375
|
.filter(Boolean)
|
|
304
376
|
.map((block) => {
|
|
@@ -309,31 +381,35 @@ function restoreRegistrations(projectPath) {
|
|
|
309
381
|
};
|
|
310
382
|
});
|
|
311
383
|
}
|
|
312
|
-
function commonGitDirectory(cwd) {
|
|
313
|
-
return fs.
|
|
384
|
+
async function commonGitDirectory(cwd) {
|
|
385
|
+
return await fs.realpath(await restoreGit(cwd, ['rev-parse', '--path-format=absolute', '--git-common-dir']));
|
|
314
386
|
}
|
|
315
387
|
/** Accept only the exact registered root in the same repository on the expected branch. */
|
|
316
|
-
export function isMatchingWorkspaceWorktree(input) {
|
|
388
|
+
export async function isMatchingWorkspaceWorktree(input) {
|
|
317
389
|
try {
|
|
318
|
-
if (!fs.
|
|
390
|
+
if (!(await fs.lstat(input.worktreePath)).isDirectory())
|
|
319
391
|
return false;
|
|
320
|
-
const target = fs.
|
|
321
|
-
if (fs.
|
|
392
|
+
const target = await fs.realpath(input.worktreePath);
|
|
393
|
+
if ((await fs.realpath(await restoreGit(input.worktreePath, ['rev-parse', '--show-toplevel']))) !== target)
|
|
322
394
|
return false;
|
|
323
|
-
if (commonGitDirectory(input.projectPath) !== commonGitDirectory(input.worktreePath))
|
|
395
|
+
if ((await commonGitDirectory(input.projectPath)) !== (await commonGitDirectory(input.worktreePath)))
|
|
324
396
|
return false;
|
|
325
397
|
const ref = `refs/heads/${input.workingBranch}`;
|
|
326
|
-
if (restoreGit(input.worktreePath, ['symbolic-ref', 'HEAD']) !== ref)
|
|
398
|
+
if ((await restoreGit(input.worktreePath, ['symbolic-ref', 'HEAD'])) !== ref)
|
|
327
399
|
return false;
|
|
328
|
-
|
|
400
|
+
for (const entry of await restoreRegistrations(input.projectPath)) {
|
|
401
|
+
if ((await canonicalize(entry.path)) === target && entry.branch === ref)
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
return false;
|
|
329
405
|
}
|
|
330
406
|
catch {
|
|
331
407
|
return false;
|
|
332
408
|
}
|
|
333
409
|
}
|
|
334
|
-
function restorePathExists(target) {
|
|
410
|
+
async function restorePathExists(target) {
|
|
335
411
|
try {
|
|
336
|
-
fs.
|
|
412
|
+
await fs.lstat(target);
|
|
337
413
|
return true;
|
|
338
414
|
}
|
|
339
415
|
catch (err) {
|
|
@@ -342,9 +418,9 @@ function restorePathExists(target) {
|
|
|
342
418
|
throw err;
|
|
343
419
|
}
|
|
344
420
|
}
|
|
345
|
-
function resolveRestoreCommit(projectPath, ref) {
|
|
421
|
+
async function resolveRestoreCommit(projectPath, ref) {
|
|
346
422
|
try {
|
|
347
|
-
return restoreGit(projectPath, ['rev-parse', '--verify', '--end-of-options', `${ref}^{commit}`]);
|
|
423
|
+
return await restoreGit(projectPath, ['rev-parse', '--verify', '--end-of-options', `${ref}^{commit}`]);
|
|
348
424
|
}
|
|
349
425
|
catch {
|
|
350
426
|
return null;
|
|
@@ -355,7 +431,7 @@ export async function restoreWorktreeCheckoutUnlocked(input) {
|
|
|
355
431
|
const { projectPath, workingBranch } = input;
|
|
356
432
|
const worktreePath = path.resolve(input.worktreePath);
|
|
357
433
|
try {
|
|
358
|
-
commonGitDirectory(projectPath);
|
|
434
|
+
await commonGitDirectory(projectPath);
|
|
359
435
|
}
|
|
360
436
|
catch {
|
|
361
437
|
throw new WorktreeCheckoutError('project-unavailable', 'The project Git repository is unavailable. Restore its location first.');
|
|
@@ -363,14 +439,17 @@ export async function restoreWorktreeCheckoutUnlocked(input) {
|
|
|
363
439
|
try {
|
|
364
440
|
if (workingBranch.startsWith('-'))
|
|
365
441
|
throw new Error('Branch names cannot start with a dash');
|
|
366
|
-
restoreGit(projectPath, ['check-ref-format', `refs/heads/${workingBranch}`]);
|
|
367
|
-
if (restorePathExists(worktreePath)) {
|
|
368
|
-
if (!isMatchingWorkspaceWorktree(input)) {
|
|
442
|
+
await restoreGit(projectPath, ['check-ref-format', `refs/heads/${workingBranch}`]);
|
|
443
|
+
if (await restorePathExists(worktreePath)) {
|
|
444
|
+
if (!(await isMatchingWorkspaceWorktree(input))) {
|
|
369
445
|
throw new WorktreeCheckoutError('path-conflict', 'The destination is occupied by a different checkout or directory. Move it before retrying.');
|
|
370
446
|
}
|
|
371
|
-
return {
|
|
447
|
+
return {
|
|
448
|
+
source: 'existing-worktree',
|
|
449
|
+
headCommitSha: await restoreGit(worktreePath, ['rev-parse', '--verify', 'HEAD']),
|
|
450
|
+
};
|
|
372
451
|
}
|
|
373
|
-
const registrations = restoreRegistrations(projectPath);
|
|
452
|
+
const registrations = await restoreRegistrations(projectPath);
|
|
374
453
|
// Do not globally prune: another missing checkout may still need its registration.
|
|
375
454
|
if (registrations.some((entry) => path.resolve(entry.path) === worktreePath)) {
|
|
376
455
|
throw new WorktreeCheckoutError('path-conflict', 'Git still registers the missing destination. Repair that worktree registration before retrying.');
|
|
@@ -379,25 +458,19 @@ export async function restoreWorktreeCheckoutUnlocked(input) {
|
|
|
379
458
|
throw new WorktreeCheckoutError('branch-in-use', 'The working branch is already checked out elsewhere. Free that checkout before retrying.');
|
|
380
459
|
}
|
|
381
460
|
let source = 'local-branch';
|
|
382
|
-
const localCommit = resolveRestoreCommit(projectPath, `refs/heads/${workingBranch}`);
|
|
461
|
+
const localCommit = await resolveRestoreCommit(projectPath, `refs/heads/${workingBranch}`);
|
|
383
462
|
let commit = localCommit;
|
|
384
463
|
if (!commit) {
|
|
385
464
|
source = 'saved-commit';
|
|
386
465
|
commit =
|
|
387
466
|
input.headCommitSha && /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/i.test(input.headCommitSha)
|
|
388
|
-
? resolveRestoreCommit(projectPath, input.headCommitSha)
|
|
467
|
+
? await resolveRestoreCommit(projectPath, input.headCommitSha)
|
|
389
468
|
: null;
|
|
390
469
|
if (!commit) {
|
|
391
470
|
source = 'remote-branch';
|
|
392
471
|
try {
|
|
393
|
-
await
|
|
394
|
-
|
|
395
|
-
cwd: projectPath,
|
|
396
|
-
timeout: 60_000,
|
|
397
|
-
env: restoreGitEnv(),
|
|
398
|
-
}, (error) => (error ? reject(error) : resolve()));
|
|
399
|
-
});
|
|
400
|
-
commit = resolveRestoreCommit(projectPath, 'FETCH_HEAD');
|
|
472
|
+
await restoreGit(projectPath, ['fetch', '--no-tags', 'origin', `refs/heads/${workingBranch}`]);
|
|
473
|
+
commit = await resolveRestoreCommit(projectPath, 'FETCH_HEAD');
|
|
401
474
|
}
|
|
402
475
|
catch {
|
|
403
476
|
// Avoid exposing remote credentials embedded in Git error output.
|
|
@@ -408,18 +481,18 @@ export async function restoreWorktreeCheckoutUnlocked(input) {
|
|
|
408
481
|
}
|
|
409
482
|
}
|
|
410
483
|
// Recheck after the asynchronous fetch, including empty directories and dangling links.
|
|
411
|
-
if (restorePathExists(worktreePath)) {
|
|
484
|
+
if (await restorePathExists(worktreePath)) {
|
|
412
485
|
throw new WorktreeCheckoutError('path-conflict', 'The destination appeared during recovery. Move the conflicting directory before retrying.');
|
|
413
486
|
}
|
|
414
487
|
if (localCommit)
|
|
415
|
-
restoreGit(projectPath, ['worktree', 'add', '--', worktreePath, workingBranch]);
|
|
488
|
+
await restoreGit(projectPath, ['worktree', 'add', '--', worktreePath, workingBranch]);
|
|
416
489
|
else
|
|
417
|
-
restoreGit(projectPath, ['worktree', 'add', '-b', workingBranch, '--', worktreePath, commit]);
|
|
418
|
-
if (!isMatchingWorkspaceWorktree(input)) {
|
|
490
|
+
await restoreGit(projectPath, ['worktree', 'add', '-b', workingBranch, '--', worktreePath, commit]);
|
|
491
|
+
if (!(await isMatchingWorkspaceWorktree(input))) {
|
|
419
492
|
throw new WorktreeCheckoutError('git-failed', 'The restored checkout could not be verified. Inspect the checkout before retrying.');
|
|
420
493
|
}
|
|
421
|
-
addToExclude(projectPath, worktreePath, path.join(commonGitDirectory(projectPath), 'info', 'exclude'));
|
|
422
|
-
return { source, headCommitSha: restoreGit(worktreePath, ['rev-parse', '--verify', 'HEAD']) };
|
|
494
|
+
await addToExclude(projectPath, worktreePath, path.join(await commonGitDirectory(projectPath), 'info', 'exclude'));
|
|
495
|
+
return { source, headCommitSha: await restoreGit(worktreePath, ['rev-parse', '--verify', 'HEAD']) };
|
|
423
496
|
}
|
|
424
497
|
catch (err) {
|
|
425
498
|
if (err instanceof WorktreeCheckoutError)
|