@bevel-software/platform-core-backend 0.29.0 → 0.30.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/__tests__/test-repositories.d.ts +3 -0
- package/dist/__tests__/test-repositories.d.ts.map +1 -0
- package/dist/__tests__/test-repositories.js +18 -0
- package/dist/__tests__/test-repositories.js.map +1 -0
- package/dist/core/create-core-server.js +1 -1
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +3 -0
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +32 -4
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/core/lifecycle.d.ts +8 -0
- package/dist/core/lifecycle.d.ts.map +1 -1
- package/dist/core/lifecycle.js +4 -0
- package/dist/core/lifecycle.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +20 -0
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +32 -1
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +138 -16
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/access/access.routes.d.ts.map +1 -1
- package/dist/modules/access/access.routes.js +20 -6
- package/dist/modules/access/access.routes.js.map +1 -1
- package/dist/modules/agent-instructions/shared-file-rules.d.ts +1 -1
- package/dist/modules/agent-instructions/shared-file-rules.js +1 -1
- package/dist/modules/diff/diff.service.d.ts.map +1 -1
- package/dist/modules/diff/diff.service.js +7 -1
- package/dist/modules/diff/diff.service.js.map +1 -1
- package/dist/modules/kb-fs/git-dir.d.ts +21 -0
- package/dist/modules/kb-fs/git-dir.d.ts.map +1 -0
- package/dist/modules/kb-fs/git-dir.js +58 -0
- package/dist/modules/kb-fs/git-dir.js.map +1 -0
- package/dist/modules/kb-fs/index.d.ts +2 -0
- package/dist/modules/kb-fs/index.d.ts.map +1 -1
- package/dist/modules/kb-fs/index.js +2 -1
- package/dist/modules/kb-fs/index.js.map +1 -1
- package/dist/modules/kb-fs/ref-files.d.ts +63 -0
- package/dist/modules/kb-fs/ref-files.d.ts.map +1 -0
- package/dist/modules/kb-fs/ref-files.js +249 -0
- package/dist/modules/kb-fs/ref-files.js.map +1 -0
- package/dist/modules/plugins/teams.routes.d.ts +4 -0
- package/dist/modules/plugins/teams.routes.d.ts.map +1 -1
- package/dist/modules/plugins/teams.routes.js +9 -1
- package/dist/modules/plugins/teams.routes.js.map +1 -1
- package/dist/modules/tool-auth/internal-token.service.d.ts +1 -1
- package/dist/modules/tool-auth/tool-auth.middleware.d.ts +1 -2
- package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
- package/dist/modules/tool-auth/tool-auth.middleware.js +1 -2
- package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
- package/dist/modules/tool-helpers/tool-handler.d.ts +2 -2
- package/dist/modules/tool-helpers/tool.contract.d.ts +2 -2
- package/dist/modules/workflow/git/git.service.d.ts +29 -0
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +177 -24
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/workflow-hooks.d.ts +1 -1
- package/dist/modules/workspace/agent-access.gate.d.ts +2 -2
- package/dist/modules/workspace/agent-access.gate.js +2 -2
- package/dist/modules/workspace/bounded-matcher.d.ts +79 -0
- package/dist/modules/workspace/bounded-matcher.d.ts.map +1 -0
- package/dist/modules/workspace/bounded-matcher.js +261 -0
- package/dist/modules/workspace/bounded-matcher.js.map +1 -0
- package/dist/modules/workspace/fetch-instant.d.ts +9 -0
- package/dist/modules/workspace/fetch-instant.d.ts.map +1 -0
- package/dist/modules/workspace/fetch-instant.js +13 -0
- package/dist/modules/workspace/fetch-instant.js.map +1 -0
- package/dist/modules/workspace/index.d.ts +7 -0
- package/dist/modules/workspace/index.d.ts.map +1 -1
- package/dist/modules/workspace/index.js +6 -0
- package/dist/modules/workspace/index.js.map +1 -1
- package/dist/modules/workspace/kb-repository.contract.d.ts +124 -0
- package/dist/modules/workspace/kb-repository.contract.d.ts.map +1 -0
- package/dist/modules/workspace/kb-repository.contract.js +19 -0
- package/dist/modules/workspace/kb-repository.contract.js.map +1 -0
- package/dist/modules/workspace/kb-repository.d.ts +236 -0
- package/dist/modules/workspace/kb-repository.d.ts.map +1 -0
- package/dist/modules/workspace/kb-repository.js +811 -0
- package/dist/modules/workspace/kb-repository.js.map +1 -0
- package/dist/modules/workspace/pattern-matcher.contract.d.ts +50 -0
- package/dist/modules/workspace/pattern-matcher.contract.d.ts.map +1 -0
- package/dist/modules/workspace/pattern-matcher.contract.js +27 -0
- package/dist/modules/workspace/pattern-matcher.contract.js.map +1 -0
- package/dist/modules/workspace/routine-write-policy.d.ts +0 -7
- package/dist/modules/workspace/routine-write-policy.d.ts.map +1 -1
- package/dist/modules/workspace/routine-write-policy.js +0 -5
- package/dist/modules/workspace/routine-write-policy.js.map +1 -1
- package/dist/modules/workspace/set-aside-clone.d.ts +40 -0
- package/dist/modules/workspace/set-aside-clone.d.ts.map +1 -1
- package/dist/modules/workspace/set-aside-clone.js +139 -0
- package/dist/modules/workspace/set-aside-clone.js.map +1 -1
- package/dist/modules/workspace/startup/kb-startup-runner.d.ts +99 -0
- package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
- package/dist/modules/workspace/startup/kb-startup-runner.js +407 -29
- package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
- package/dist/modules/workspace/workspace.service.d.ts +68 -32
- package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.service.js +240 -161
- package/dist/modules/workspace/workspace.service.js.map +1 -1
- package/dist/modules/workspace/workspace.tools.d.ts +13 -4
- package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.tools.js +162 -336
- package/dist/modules/workspace/workspace.tools.js.map +1 -1
- package/dist/test-setup.js +4 -0
- package/dist/test-setup.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/test-repositories.ts +22 -0
- package/src/__tests__/workspace-service.ts +69 -0
- package/src/core/__tests__/lifecycle.test.ts +16 -0
- package/src/core/create-core-server.ts +1 -1
- package/src/core/create-core-services.ts +36 -3
- package/src/core/lifecycle.ts +10 -0
- package/src/modules/access/__tests__/access-control.atref-cache.test.ts +144 -4
- package/src/modules/access/__tests__/access-control.group-read.test.ts +73 -0
- package/src/modules/access/__tests__/access.routes.group-grant.test.ts +66 -0
- package/src/modules/access/__tests__/access.routes.unknown-email.test.ts +2 -0
- package/src/modules/access/access-control.interface.ts +22 -0
- package/src/modules/access/access-control.service.ts +142 -14
- package/src/modules/access/access.routes.ts +22 -6
- package/src/modules/agent-instructions/shared-file-rules.ts +1 -1
- package/src/modules/diff/__tests__/diff.service.seed-atomicity.test.ts +2 -1
- package/src/modules/diff/__tests__/diff.service.test.ts +2 -1
- package/src/modules/diff/__tests__/diff.service.worktree.test.ts +76 -0
- package/src/modules/diff/diff.service.ts +6 -4
- package/src/modules/kb-fs/__tests__/git-dir.test.ts +77 -0
- package/src/modules/kb-fs/__tests__/ref-files.test.ts +338 -0
- package/src/modules/kb-fs/git-dir.ts +53 -0
- package/src/modules/kb-fs/index.ts +2 -0
- package/src/modules/kb-fs/ref-files.ts +279 -0
- package/src/modules/plugins/__tests__/teams.routes.test.ts +55 -0
- package/src/modules/plugins/teams.routes.ts +14 -1
- package/src/modules/settings/__tests__/setup.routes.layout-phase.test.ts +2 -2
- package/src/modules/settings/__tests__/setup.routes.managed-phase.test.ts +2 -2
- package/src/modules/tool-auth/internal-token.service.ts +1 -1
- package/src/modules/tool-auth/tool-auth.middleware.ts +1 -2
- package/src/modules/tool-helpers/__tests__/branch-declaration.test.ts +2 -2
- package/src/modules/tool-helpers/tool-handler.ts +2 -2
- package/src/modules/tool-helpers/tool.contract.ts +2 -2
- package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +6 -1
- package/src/modules/workflow/git/__tests__/git.service.changedPathsMemo.test.ts +175 -0
- package/src/modules/workflow/git/__tests__/git.service.worktrees.test.ts +284 -0
- package/src/modules/workflow/git/git.service.ts +184 -22
- package/src/modules/workflow/workflow-hooks.ts +1 -1
- package/src/modules/workspace/__tests__/agent-downloads.test.ts +2 -2
- package/src/modules/workspace/__tests__/bounded-matcher.test.ts +213 -0
- package/src/modules/workspace/__tests__/git-internals.security.test.ts +22 -7
- package/src/modules/workspace/__tests__/kb-repository.test.ts +481 -0
- package/src/modules/workspace/__tests__/rest-tool-arguments.test.ts +14 -0
- package/src/modules/workspace/__tests__/routine-write-policy.test.ts +0 -11
- package/src/modules/workspace/__tests__/workspace-paths-inside-the-repo.test.ts +2 -2
- package/src/modules/workspace/__tests__/workspace.routes.folder-zip-access.test.ts +2 -2
- package/src/modules/workspace/__tests__/workspace.routes.folders.test.ts +4 -3
- package/src/modules/workspace/__tests__/workspace.service.any-workspace-credentials.test.ts +2 -2
- package/src/modules/workspace/__tests__/workspace.service.ensure-remotes-fetched.test.ts +2 -2
- package/src/modules/workspace/__tests__/workspace.service.forget-clone-races.test.ts +12 -5
- package/src/modules/workspace/__tests__/workspace.service.list-cloned.test.ts +6 -6
- package/src/modules/workspace/__tests__/workspace.service.move-never-overwrites.test.ts +2 -1
- package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +2 -1
- package/src/modules/workspace/__tests__/workspace.service.replaced-repository.test.ts +2 -1
- package/src/modules/workspace/__tests__/workspace.service.test.ts +24 -23
- package/src/modules/workspace/__tests__/workspace.service.unknown-branch.test.ts +2 -1
- package/src/modules/workspace/__tests__/workspace.service.worktrees.test.ts +620 -0
- package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +15 -1
- package/src/modules/workspace/__tests__/workspace.tools.branch-errors.test.ts +71 -5
- package/src/modules/workspace/__tests__/workspace.tools.test.ts +153 -298
- package/src/modules/workspace/agent-access.gate.ts +2 -2
- package/src/modules/workspace/bounded-matcher.ts +328 -0
- package/src/modules/workspace/fetch-instant.ts +13 -0
- package/src/modules/workspace/index.ts +12 -0
- package/src/modules/workspace/kb-repository.contract.ts +124 -0
- package/src/modules/workspace/kb-repository.ts +844 -0
- package/src/modules/workspace/pattern-matcher.contract.ts +69 -0
- package/src/modules/workspace/routine-write-policy.ts +0 -15
- package/src/modules/workspace/set-aside-clone.ts +140 -0
- package/src/modules/workspace/startup/__tests__/kb-startup-runner.degraded.test.ts +3 -2
- package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +12 -10
- package/src/modules/workspace/startup/__tests__/kb-startup-runner.worktrees.test.ts +613 -0
- package/src/modules/workspace/startup/kb-startup-runner.ts +422 -29
- package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +4 -3
- package/src/modules/workspace/workspace.service.ts +247 -169
- package/src/modules/workspace/workspace.tools.ts +179 -365
- package/src/test-setup.ts +5 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
|
+
import { statSync } from 'node:fs';
|
|
2
3
|
import { logger } from '../../shared/logging.js';
|
|
3
4
|
const log = logger('workspace');
|
|
4
5
|
import path from 'node:path';
|
|
@@ -20,8 +21,9 @@ import { BranchNotFoundError, RemoteBranchGoneError, WorkflowDomainError, isMiss
|
|
|
20
21
|
import { assertValidBranchName } from '../kb-fs/branch-name.js';
|
|
21
22
|
import { normalizeRepositoryAddress, sameRepository } from '../kb-fs/remote-url.js';
|
|
22
23
|
import { redactSecret, urlQuerySecrets } from '../../shared/redact-secret.js';
|
|
23
|
-
import { setAsideClone, setAsideRootFor, setAsideStamp } from './set-aside-clone.js';
|
|
24
|
-
import {
|
|
24
|
+
import { setAsideClone, setAsideRepository, setAsideRootFor, setAsideStamp } from './set-aside-clone.js';
|
|
25
|
+
import { workingCopyShape } from './kb-repository.js';
|
|
26
|
+
import { cloneCredentialConfigArgs, credentialHelperValue, cloneTrackingConfigArgs, SAFE_IMPLICIT_FETCH_ARGS, } from '../kb-fs/clone-config.js';
|
|
25
27
|
// Zip-bomb guardrails. The upload cap is 50 MB compressed; even a generous
|
|
26
28
|
// 100× expansion ratio still fits inside these limits, so a well-behaved
|
|
27
29
|
// archive will never trip them. They exist so a hostile or malformed zip
|
|
@@ -76,21 +78,6 @@ export class EntryExistsError extends WorkflowDomainError {
|
|
|
76
78
|
this.name = 'EntryExistsError';
|
|
77
79
|
}
|
|
78
80
|
}
|
|
79
|
-
/**
|
|
80
|
-
* A floor under the git deadline for a clone: a first clone of a large
|
|
81
|
-
* knowledge base over a slow link legitimately takes minutes, and the port's
|
|
82
|
-
* default is sized for a running deployment's operations. A configured
|
|
83
|
-
* ceiling above this applies as configured.
|
|
84
|
-
*/
|
|
85
|
-
const CLONE_TIMEOUT_MS = 600_000;
|
|
86
|
-
/**
|
|
87
|
-
* Identity stamped on the per-branch clone's git config. Every workflow
|
|
88
|
-
* commit overrides this with `--author=…` so the human shows up in
|
|
89
|
-
* `git log`; this is just the fallback committer identity so plain
|
|
90
|
-
* git operations have something to attribute to.
|
|
91
|
-
*/
|
|
92
|
-
const BOT_NAME = 'Bevel Workflow';
|
|
93
|
-
const BOT_EMAIL = 'bevel-workflow@bevel.software';
|
|
94
81
|
/** Redact the token the runner authenticates with from an error message. */
|
|
95
82
|
function redactError(err, token) {
|
|
96
83
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -154,6 +141,7 @@ export class WorkspaceService {
|
|
|
154
141
|
gitRunner;
|
|
155
142
|
setAsideRoot;
|
|
156
143
|
aroundSetAside;
|
|
144
|
+
repository;
|
|
157
145
|
/** Maps branch → absolute directory path. Lazily populated. */
|
|
158
146
|
branchDirs = new Map();
|
|
159
147
|
/**
|
|
@@ -245,15 +233,62 @@ export class WorkspaceService {
|
|
|
245
233
|
* where it was and the branch is refused with that reason. Absent, the
|
|
246
234
|
* move runs as it is.
|
|
247
235
|
*/
|
|
248
|
-
aroundSetAside = (_workspaceId, move) => move()
|
|
236
|
+
aroundSetAside = (_workspaceId, move) => move(),
|
|
237
|
+
/**
|
|
238
|
+
* The one repository every branch workspace is a worktree of — THE SAME
|
|
239
|
+
* INSTANCE THE KB STARTUP PHASE AND THE GIT LAYER USE, so that one fetch
|
|
240
|
+
* serves them all and its reservation is one. Built by the composition
|
|
241
|
+
* root, never here.
|
|
242
|
+
*/
|
|
243
|
+
repository) {
|
|
249
244
|
this.workspacesRoot = workspacesRoot;
|
|
250
245
|
this.kb = kb;
|
|
251
246
|
this.disk = disk;
|
|
252
247
|
this.gitRunner = gitRunner;
|
|
253
248
|
this.setAsideRoot = setAsideRoot;
|
|
254
249
|
this.aroundSetAside = aroundSetAside;
|
|
250
|
+
this.repository = repository;
|
|
255
251
|
this.kbRepoUrl = typeof kbRepoUrl === 'function' ? kbRepoUrl : () => kbRepoUrl;
|
|
256
252
|
}
|
|
253
|
+
/**
|
|
254
|
+
* Whether the checkout folder `repoDir` is a worktree (its `.git` a file)
|
|
255
|
+
* rather than a full clone. A stat, no git.
|
|
256
|
+
*/
|
|
257
|
+
async isWorktree(repoDir) {
|
|
258
|
+
// Synchronous on purpose: it is one stat of a local file, and it sits in
|
|
259
|
+
// front of the fetch joins (`ensureRemotesFetched`), where an await
|
|
260
|
+
// between two concurrent callers' in-flight checks is what lets both
|
|
261
|
+
// start a fetch.
|
|
262
|
+
try {
|
|
263
|
+
return statSync(path.join(repoDir, '.git')).isFile();
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Whether `repoDir` holds a working copy that can be served: a worktree of
|
|
271
|
+
* the repository or a full clone. A worktree whose link to the repository is
|
|
272
|
+
* broken is NOT one — it is an unfinished checkout, recreated on the next
|
|
273
|
+
* open. Errors other than absence propagate.
|
|
274
|
+
*/
|
|
275
|
+
async hasWorkingCopy(repoDir) {
|
|
276
|
+
const shape = await workingCopyShape(repoDir);
|
|
277
|
+
return shape === 'worktree' || shape === 'clone';
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* The repository behind a workspace when it is a worktree of it; null for a
|
|
281
|
+
* full clone (which fetches on its own) or a workspace with no checkout.
|
|
282
|
+
* For the git layer, which keys its fetches on this.
|
|
283
|
+
*/
|
|
284
|
+
async sharedRepositoryOf(workspaceId) {
|
|
285
|
+
const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
|
|
286
|
+
return this.repositoryOfCheckout(path.join(workspaceDir, this.kbDirName));
|
|
287
|
+
}
|
|
288
|
+
/** {@link sharedRepositoryOf}, for a caller that already has the checkout folder. */
|
|
289
|
+
async repositoryOfCheckout(repoDir) {
|
|
290
|
+
return (await this.isWorktree(repoDir)) ? this.repository : null;
|
|
291
|
+
}
|
|
257
292
|
/**
|
|
258
293
|
* The set-asides in flight, one per branch. Two callers opening the same
|
|
259
294
|
* branch both find the clone of another repository; the second waits for
|
|
@@ -389,64 +424,6 @@ export class WorkspaceService {
|
|
|
389
424
|
return err instanceof GitRunError && err.exitCode === 2;
|
|
390
425
|
}
|
|
391
426
|
}
|
|
392
|
-
/**
|
|
393
|
-
* Build git clone args. Credentials go in as `clone --config`, which both
|
|
394
|
-
* authenticates this clone and leaves the helper in the new repo's config for
|
|
395
|
-
* every later push — and keeps the token itself out of the repo URL, out of
|
|
396
|
-
* argv, and out of `.git/config` (the helper reads it from the environment).
|
|
397
|
-
*/
|
|
398
|
-
gitCloneArgs(targetDir, branch, referenceRepo) {
|
|
399
|
-
// core.longpaths=true: KB has paths >260 chars; without this the clone silently drops those files on Windows.
|
|
400
|
-
const args = ['clone', '-c', 'core.longpaths=true', '-b', branch];
|
|
401
|
-
// Borrow the object database of an existing sibling clone. Every commit
|
|
402
|
-
// in the KB is already on disk locally, so object transfer drops to
|
|
403
|
-
// ~zero and a cold branch switch pays only the ref-negotiation
|
|
404
|
-
// round-trip instead of a full download. `--dissociate` copies the
|
|
405
|
-
// borrowed objects into the new repo and drops the alternates link, so
|
|
406
|
-
// the new clone stays independent even if the sibling is later swept
|
|
407
|
-
// or gc'd. Still a real clone of the real remote — all `refs/remotes/
|
|
408
|
-
// origin/*` are populated as usual.
|
|
409
|
-
if (referenceRepo) {
|
|
410
|
-
args.push('--reference', referenceRepo, '--dissociate');
|
|
411
|
-
}
|
|
412
|
-
// Persisted into the new repo's config (`clone --config`), not just applied
|
|
413
|
-
// to this invocation — every later push from this clone depends on finding
|
|
414
|
-
// the helper there. See `cloneCredentialArgs`.
|
|
415
|
-
args.push(...cloneCredentialArgs(this.gitRunner.credentials));
|
|
416
|
-
args.push(this.kbRepoUrl(), targetDir);
|
|
417
|
-
return args;
|
|
418
|
-
}
|
|
419
|
-
/**
|
|
420
|
-
* Find an existing workspace clone on disk to use as a `--reference` for
|
|
421
|
-
* a new clone. Any clone of the KB works — they all share the same object
|
|
422
|
-
* history. Returns the absolute path to the inner repo dir, or null when
|
|
423
|
-
* no sibling exists (the first branch after a cold start). Scans disk
|
|
424
|
-
* rather than the in-memory `branchDirs` map so clones that survived a
|
|
425
|
-
* process restart are still found.
|
|
426
|
-
*/
|
|
427
|
-
async findSiblingRepo(excludeBranch) {
|
|
428
|
-
const excludeId = workspaceIdForBranch(excludeBranch);
|
|
429
|
-
let entries;
|
|
430
|
-
try {
|
|
431
|
-
entries = await fs.readdir(this.workspacesRoot, { withFileTypes: true });
|
|
432
|
-
}
|
|
433
|
-
catch {
|
|
434
|
-
return null;
|
|
435
|
-
}
|
|
436
|
-
for (const entry of entries) {
|
|
437
|
-
if (!entry.isDirectory() || entry.name === excludeId)
|
|
438
|
-
continue;
|
|
439
|
-
const repoDir = path.join(this.workspacesRoot, entry.name, this.kbDirName);
|
|
440
|
-
try {
|
|
441
|
-
await fs.access(path.join(repoDir, '.git'));
|
|
442
|
-
return repoDir;
|
|
443
|
-
}
|
|
444
|
-
catch {
|
|
445
|
-
// No `.git` — half-built or unrelated dir; keep looking.
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
return null;
|
|
449
|
-
}
|
|
450
427
|
/**
|
|
451
428
|
* Return the id of any workspace already cloned on disk, or null if none
|
|
452
429
|
* exists. Every clone tracks the same origin, so for a *repo-global* operation
|
|
@@ -482,13 +459,10 @@ export class WorkspaceService {
|
|
|
482
459
|
if (!entry.isDirectory())
|
|
483
460
|
continue;
|
|
484
461
|
const repoDir = path.join(this.workspacesRoot, entry.name, this.kbDirName);
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
catch {
|
|
489
|
-
// No `.git` — half-built or unrelated dir; keep looking.
|
|
462
|
+
// No working copy — half-built, a worktree whose repository is gone, or
|
|
463
|
+
// an unrelated dir; keep looking.
|
|
464
|
+
if (!(await this.hasWorkingCopy(repoDir).catch(() => false)))
|
|
490
465
|
continue;
|
|
491
|
-
}
|
|
492
466
|
await this.refreshCredentialHelperIfStale(branchForWorkspaceId(entry.name), repoDir);
|
|
493
467
|
return entry.name;
|
|
494
468
|
}
|
|
@@ -544,7 +518,10 @@ export class WorkspaceService {
|
|
|
544
518
|
continue;
|
|
545
519
|
}
|
|
546
520
|
try {
|
|
547
|
-
|
|
521
|
+
// A worktree whose repository is gone is not a working copy to pull:
|
|
522
|
+
// it is recreated when the branch is next opened.
|
|
523
|
+
if (!(await this.hasWorkingCopy(path.join(this.workspacesRoot, entry.name, this.kbDirName))))
|
|
524
|
+
continue;
|
|
548
525
|
}
|
|
549
526
|
catch (err) {
|
|
550
527
|
if (isAbsence(err))
|
|
@@ -566,32 +543,6 @@ export class WorkspaceService {
|
|
|
566
543
|
}
|
|
567
544
|
return cloned;
|
|
568
545
|
}
|
|
569
|
-
/**
|
|
570
|
-
* Run the clone. When a `referenceRepo` is supplied and the referenced
|
|
571
|
-
* clone fails (e.g. the sibling's object store is corrupt or mid-write),
|
|
572
|
-
* fall back once to a plain network clone so a bad sibling can never
|
|
573
|
-
* block a new branch from bootstrapping.
|
|
574
|
-
*/
|
|
575
|
-
async runClone(targetDir, branch, referenceRepo) {
|
|
576
|
-
// Long enough for a first clone, whatever the configured ceiling says.
|
|
577
|
-
const timeoutMs = Math.max(this.gitRunner.defaultTimeoutMs, CLONE_TIMEOUT_MS);
|
|
578
|
-
if (referenceRepo) {
|
|
579
|
-
try {
|
|
580
|
-
await this.gitRunner.run(path.dirname(targetDir), this.gitCloneArgs(targetDir, branch, referenceRepo), {
|
|
581
|
-
timeoutMs,
|
|
582
|
-
});
|
|
583
|
-
return;
|
|
584
|
-
}
|
|
585
|
-
catch (err) {
|
|
586
|
-
log.warn(`referenced clone for "${branch}" failed, retrying without reference:`, {
|
|
587
|
-
detail: redactError(err, this.gitRunner.credentials.token()),
|
|
588
|
-
});
|
|
589
|
-
// Clear any partial output so the retry clones into a clean dir.
|
|
590
|
-
await fs.rm(targetDir, { recursive: true, force: true }).catch(() => { });
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
await this.gitRunner.run(path.dirname(targetDir), this.gitCloneArgs(targetDir, branch), { timeoutMs });
|
|
594
|
-
}
|
|
595
546
|
/**
|
|
596
547
|
* Get-or-create the per-branch workspace. Idempotent — repeat calls for
|
|
597
548
|
* the same branch reuse the existing clone. Concurrent first-callers
|
|
@@ -627,9 +578,11 @@ export class WorkspaceService {
|
|
|
627
578
|
await this.refreshCredentialHelperIfStale(branch, repoDir);
|
|
628
579
|
return this.buildWorkspaceInfo(branch, workspaceDir);
|
|
629
580
|
}
|
|
630
|
-
// Check disk — the clone
|
|
631
|
-
// process and survived a restart.
|
|
632
|
-
|
|
581
|
+
// Check disk — the worktree (or a clone from an earlier release) may have
|
|
582
|
+
// been made by a previous process and survived a restart. A worktree whose
|
|
583
|
+
// repository is gone is not on disk in any useful sense: bootstrap below
|
|
584
|
+
// recreates it.
|
|
585
|
+
const onDisk = await this.hasWorkingCopy(repoDir).catch(() => false);
|
|
633
586
|
// A clone fetches and pushes through the address stored in its OWN
|
|
634
587
|
// `remote.origin.url`, which nothing rewrites when the configured
|
|
635
588
|
// repository changes. Adopting one of the repository that was replaced is
|
|
@@ -662,7 +615,12 @@ export class WorkspaceService {
|
|
|
662
615
|
// refuses this caller with the same reason.
|
|
663
616
|
await setAsideInFlight;
|
|
664
617
|
}
|
|
665
|
-
else if (onDisk
|
|
618
|
+
else if (onDisk
|
|
619
|
+
? await this.isCloneOfAnotherRepository(repoDir)
|
|
620
|
+
: // Nothing of this branch on disk yet, but the worktree it is about to
|
|
621
|
+
// get would be of the one repository — whose address may be the
|
|
622
|
+
// replaced one, until the startup phase has been through.
|
|
623
|
+
await this.isRepositoryOfAnother()) {
|
|
666
624
|
// Asked again: another caller may have started the move while the
|
|
667
625
|
// address was being read.
|
|
668
626
|
const startedMeanwhile = this.inFlightSetAsides.get(branch);
|
|
@@ -679,13 +637,54 @@ export class WorkspaceService {
|
|
|
679
637
|
// the middle of making. Nothing is left to move; the bootstrap below
|
|
680
638
|
// waits for that clone.
|
|
681
639
|
}
|
|
640
|
+
else if (!onDisk || (await this.isWorktree(repoDir))) {
|
|
641
|
+
// A worktree of a repository of another address: the address is the
|
|
642
|
+
// REPOSITORY's, and its commits are the repository's, so the repository
|
|
643
|
+
// and every worktree of it are set aside together, into one folder.
|
|
644
|
+
const kept = path.join(setAsideRootFor(this.workspacesRoot, this.setAsideRoot), setAsideStamp());
|
|
645
|
+
const ids = await this.worktreeIdsOnDisk();
|
|
646
|
+
if (onDisk && !ids.includes(id))
|
|
647
|
+
ids.push(id);
|
|
648
|
+
let movedIds = null;
|
|
649
|
+
const move = async () => {
|
|
650
|
+
if (this.discardsOf(id) !== discardsAtAdoption)
|
|
651
|
+
return;
|
|
652
|
+
// Asked again under the reservation: two branches opened at once
|
|
653
|
+
// both read the old address, and the first one's set-aside and fresh
|
|
654
|
+
// repository may be done by the time this one holds it. Moving then
|
|
655
|
+
// would set aside the replacement and the first branch's worktree.
|
|
656
|
+
let superseded = false;
|
|
657
|
+
const ids = await setAsideRepository(this.repository, this.workspacesRoot, this.kbDirName, kept, async () => {
|
|
658
|
+
superseded = !(await this.isRepositoryOfAnother());
|
|
659
|
+
return !superseded;
|
|
660
|
+
});
|
|
661
|
+
if (superseded)
|
|
662
|
+
return;
|
|
663
|
+
movedIds = ids;
|
|
664
|
+
for (const movedId of movedIds)
|
|
665
|
+
this.forgetClone(movedId);
|
|
666
|
+
};
|
|
667
|
+
// What surrounds a set-aside is asked for every workspace that goes,
|
|
668
|
+
// nested, so the move runs once with all of them held.
|
|
669
|
+
const wrapped = ids.reduce((inner, wid) => () => this.aroundSetAside(wid, inner), move);
|
|
670
|
+
const moving = this.settleSetAside(wrapped(), () => movedIds !== null, branch).finally(() => {
|
|
671
|
+
this.inFlightSetAsides.delete(branch);
|
|
672
|
+
});
|
|
673
|
+
this.inFlightSetAsides.set(branch, moving);
|
|
674
|
+
await moving;
|
|
675
|
+
if (movedIds) {
|
|
676
|
+
log.warn(`the knowledge-base repository and its ${movedIds.length} worktree(s) were of another ` +
|
|
677
|
+
`repository. Set aside together at ${kept}; nothing was deleted, and "${branch}" is being opened ` +
|
|
678
|
+
'fresh from the configured one. Work that was never pushed is in that folder.');
|
|
679
|
+
}
|
|
680
|
+
}
|
|
682
681
|
else {
|
|
683
682
|
const kept = path.join(setAsideRootFor(this.workspacesRoot, this.setAsideRoot), setAsideStamp(), id);
|
|
684
683
|
// What surrounds the move may wait, and the startup phase may set
|
|
685
684
|
// this very copy aside meanwhile. Then there is nothing left to move:
|
|
686
685
|
// the phase did it, and said so.
|
|
687
686
|
let moved = false;
|
|
688
|
-
const
|
|
687
|
+
const move = async () => {
|
|
689
688
|
if (this.discardsOf(id) !== discardsAtAdoption)
|
|
690
689
|
return;
|
|
691
690
|
await setAsideClone(repoDir, kept);
|
|
@@ -695,7 +694,8 @@ export class WorkspaceService {
|
|
|
695
694
|
// that looked at the old copy before this line knows its answer is
|
|
696
695
|
// stale, however late it comes back.
|
|
697
696
|
this.forgetClone(id);
|
|
698
|
-
}
|
|
697
|
+
};
|
|
698
|
+
const moving = this.settleSetAside(this.aroundSetAside(id, move), () => moved, branch).finally(() => {
|
|
699
699
|
this.inFlightSetAsides.delete(branch);
|
|
700
700
|
});
|
|
701
701
|
this.inFlightSetAsides.set(branch, moving);
|
|
@@ -864,7 +864,37 @@ export class WorkspaceService {
|
|
|
864
864
|
* process that does not know where the repository is has no business
|
|
865
865
|
* declaring a clone stale.
|
|
866
866
|
*/
|
|
867
|
-
|
|
867
|
+
/**
|
|
868
|
+
* Wait for a set-aside. Its failure refuses the branch only while nothing
|
|
869
|
+
* has moved: once the move happened, what failed is what runs AFTER it
|
|
870
|
+
* around the move (`aroundSetAside` looking for the copy's work again,
|
|
871
|
+
* which already retries itself), and refusing then would report a move that
|
|
872
|
+
* did happen as one that did not. That failure is logged, and the branch is
|
|
873
|
+
* opened fresh.
|
|
874
|
+
*/
|
|
875
|
+
async settleSetAside(moving, didMove, branch) {
|
|
876
|
+
try {
|
|
877
|
+
await moving;
|
|
878
|
+
}
|
|
879
|
+
catch (err) {
|
|
880
|
+
if (!didMove())
|
|
881
|
+
throw err;
|
|
882
|
+
log.error(`the working copy of "${branch}" was set aside, but what surrounds the move failed after it; the branch ` +
|
|
883
|
+
'is opened fresh:', { detail: redactError(err, this.gitRunner.credentials.token()) });
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Whether the one repository is on disk, finished, and of an address that
|
|
888
|
+
* is not the configured one. For a branch opened with nothing on disk: its
|
|
889
|
+
* worktree would be added to that repository and fetch from the old
|
|
890
|
+
* address.
|
|
891
|
+
*/
|
|
892
|
+
async isRepositoryOfAnother() {
|
|
893
|
+
if (!(await this.repository.exists()))
|
|
894
|
+
return false;
|
|
895
|
+
return this.isCloneOfAnotherRepository(this.repository.dir, 'knowledge-base repository');
|
|
896
|
+
}
|
|
897
|
+
async isCloneOfAnotherRepository(repoDir, label) {
|
|
868
898
|
const configured = this.kbRepoUrl().trim();
|
|
869
899
|
if (configured === '')
|
|
870
900
|
return false;
|
|
@@ -882,7 +912,7 @@ export class WorkspaceService {
|
|
|
882
912
|
// knowledge-base address is one that may carry its credential there (a
|
|
883
913
|
// signed URL — see `urlQuerySecrets`). So the address is scrubbed on the
|
|
884
914
|
// way into the log, exactly as the startup phase scrubs its own.
|
|
885
|
-
log.warn(`the "${path.basename(path.dirname(repoDir))}" working copy is a clone of another repository ` +
|
|
915
|
+
log.warn(`the ${label ?? `"${path.basename(path.dirname(repoDir))}" working copy`} is a clone of another repository ` +
|
|
886
916
|
`(${redactSecret(normalizeRepositoryAddress(origin), urlQuerySecrets(origin))}).`);
|
|
887
917
|
return true;
|
|
888
918
|
}
|
|
@@ -961,6 +991,21 @@ export class WorkspaceService {
|
|
|
961
991
|
* tracking keys before it pulls.
|
|
962
992
|
*/
|
|
963
993
|
async normalizeCloneConfig(repoDir, branch) {
|
|
994
|
+
if (await this.isWorktree(repoDir)) {
|
|
995
|
+
// A worktree reads the repository's one config: the branch's keys are
|
|
996
|
+
// written there, one writer at a time, and the credential helper is the
|
|
997
|
+
// repository's, stamped once for every worktree.
|
|
998
|
+
try {
|
|
999
|
+
await this.repository.writeConfig(repoDir, cloneTrackingConfigArgs(branch));
|
|
1000
|
+
}
|
|
1001
|
+
catch (err) {
|
|
1002
|
+
log.warn(`could not normalize the config of the "${branch}" worktree:`, {
|
|
1003
|
+
detail: redactError(err, this.gitRunner.credentials.token()),
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
await this.repository.refreshCredentials();
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
964
1009
|
try {
|
|
965
1010
|
for (const args of cloneTrackingConfigArgs(branch)) {
|
|
966
1011
|
await this.gitRunner.run(repoDir, args);
|
|
@@ -1054,8 +1099,32 @@ export class WorkspaceService {
|
|
|
1054
1099
|
async refreshCredentialHelperIfStale(branch, repoDir) {
|
|
1055
1100
|
if (this.stampedCredentialFingerprint.get(branch) === this.credentialFingerprint())
|
|
1056
1101
|
return;
|
|
1102
|
+
if (await this.isWorktree(repoDir)) {
|
|
1103
|
+
// The repository's helper serves every worktree; it keeps its own
|
|
1104
|
+
// fingerprint, and a no-op costs no git.
|
|
1105
|
+
await this.repository.refreshCredentials();
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1057
1108
|
await this.stampCredentialHelper(repoDir, branch);
|
|
1058
1109
|
}
|
|
1110
|
+
/** The workspace ids whose checkout folder is a worktree (sound or broken) — a disk scan. */
|
|
1111
|
+
async worktreeIdsOnDisk() {
|
|
1112
|
+
let entries;
|
|
1113
|
+
try {
|
|
1114
|
+
entries = await fs.readdir(this.workspacesRoot, { withFileTypes: true });
|
|
1115
|
+
}
|
|
1116
|
+
catch {
|
|
1117
|
+
return [];
|
|
1118
|
+
}
|
|
1119
|
+
const ids = [];
|
|
1120
|
+
for (const entry of entries) {
|
|
1121
|
+
if (!entry.isDirectory())
|
|
1122
|
+
continue;
|
|
1123
|
+
if (await this.isWorktree(path.join(this.workspacesRoot, entry.name, this.kbDirName)))
|
|
1124
|
+
ids.push(entry.name);
|
|
1125
|
+
}
|
|
1126
|
+
return ids;
|
|
1127
|
+
}
|
|
1059
1128
|
async cloneProcessMapForBranch(workspaceDir, branch) {
|
|
1060
1129
|
const targetDir = path.join(workspaceDir, this.kbDirName);
|
|
1061
1130
|
// Probe whether `targetDir` exists. We split the existence check from
|
|
@@ -1081,16 +1150,14 @@ export class WorkspaceService {
|
|
|
1081
1150
|
}
|
|
1082
1151
|
if (target !== null) {
|
|
1083
1152
|
// The directory exists, but a previous bootstrap may have crashed
|
|
1084
|
-
//
|
|
1085
|
-
//
|
|
1086
|
-
//
|
|
1087
|
-
//
|
|
1088
|
-
|
|
1153
|
+
// midway — leaving a directory shell without `.git` — or it is a
|
|
1154
|
+
// worktree whose link to the repository is broken (the repository was
|
|
1155
|
+
// lost, or the worktree pruned from under it). Either is an unfinished
|
|
1156
|
+
// checkout: wipe it and add the worktree again. A working worktree, or a
|
|
1157
|
+
// full clone from an earlier release, is adopted as it is.
|
|
1158
|
+
let shape;
|
|
1089
1159
|
try {
|
|
1090
|
-
|
|
1091
|
-
if (dotGit.isDirectory() || dotGit.isFile()) {
|
|
1092
|
-
alreadyCloned = true;
|
|
1093
|
-
}
|
|
1160
|
+
shape = await workingCopyShape(targetDir);
|
|
1094
1161
|
}
|
|
1095
1162
|
catch (err) {
|
|
1096
1163
|
// Only treat "does-not-exist" as "half-built dir, wipe and
|
|
@@ -1101,8 +1168,9 @@ export class WorkspaceService {
|
|
|
1101
1168
|
// mean `.git` itself vanished under us — an absence either way.)
|
|
1102
1169
|
if (!isAbsence(err))
|
|
1103
1170
|
throw err;
|
|
1171
|
+
shape = 'none';
|
|
1104
1172
|
}
|
|
1105
|
-
if (
|
|
1173
|
+
if (shape === 'worktree' || shape === 'clone') {
|
|
1106
1174
|
// We don't auto-pull because that could clobber another user's
|
|
1107
1175
|
// in-progress lock-held edits. The CONFIG is re-stamped though — it
|
|
1108
1176
|
// never touches the working tree, and a clone with drifted tracking
|
|
@@ -1110,30 +1178,24 @@ export class WorkspaceService {
|
|
|
1110
1178
|
await this.normalizeCloneConfig(targetDir, branch);
|
|
1111
1179
|
return;
|
|
1112
1180
|
}
|
|
1113
|
-
|
|
1181
|
+
if (shape === 'broken') {
|
|
1182
|
+
log.warn(`the "${branch}" worktree had lost its link to the repository; it is added again.`);
|
|
1183
|
+
}
|
|
1184
|
+
// Half-built dir — wipe and add the worktree again.
|
|
1114
1185
|
await fs.rm(targetDir, { recursive: true, force: true });
|
|
1115
1186
|
}
|
|
1116
|
-
// Borrow an existing sibling clone's objects to skip the bulk download.
|
|
1117
|
-
// Resolved before the clone so a failure mid-clone still rolls back cleanly.
|
|
1118
|
-
const reference = await this.findSiblingRepo(branch);
|
|
1119
1187
|
try {
|
|
1120
|
-
|
|
1121
|
-
//
|
|
1122
|
-
//
|
|
1123
|
-
|
|
1124
|
-
//
|
|
1125
|
-
//
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
//
|
|
1130
|
-
// this
|
|
1131
|
-
// explicitly means the shape is asserted rather than assumed, and the
|
|
1132
|
-
// same call is what repairs an existing clone that drifted.
|
|
1133
|
-
await this.normalizeCloneConfig(targetDir, branch);
|
|
1134
|
-
log.info(`Cloned ${this.kbDirName} for branch "${branch}"` + (reference ? ' (referenced a sibling clone)' : ''));
|
|
1135
|
-
// A fresh clone has already downloaded every ref — tell the git layer
|
|
1136
|
-
// so the first `listBranches` skips the redundant implicit `git fetch`.
|
|
1188
|
+
// A worktree of the one repository: no history is copied. The
|
|
1189
|
+
// repository is created and fetched first if this is the first branch
|
|
1190
|
+
// opened on the deployment, and the identity, the long-paths setting,
|
|
1191
|
+
// the credential helper and the branch's tracking config are the
|
|
1192
|
+
// repository's (see `KbRepository`). The add asks the remote for the
|
|
1193
|
+
// branch, so the worktree starts at its tip as a fresh clone would.
|
|
1194
|
+
await this.repository.addWorktree(branch, targetDir);
|
|
1195
|
+
log.info(`Added the ${this.kbDirName} worktree for branch "${branch}"`);
|
|
1196
|
+
// A working copy was just made — tell the git layer, so a fetch record
|
|
1197
|
+
// it kept for this workspace id (a clone it replaced) is not read as
|
|
1198
|
+
// this one's, and the first `listBranches` skips a redundant fetch.
|
|
1137
1199
|
// Isolated from the clone-rollback `catch` below: a misbehaving listener
|
|
1138
1200
|
// must not cause a successful clone to be wiped.
|
|
1139
1201
|
try {
|
|
@@ -1149,6 +1211,7 @@ export class WorkspaceService {
|
|
|
1149
1211
|
const redacted = redactError(err, this.gitRunner.credentials.token());
|
|
1150
1212
|
// Roll back partial state so the next bootstrap retries cleanly.
|
|
1151
1213
|
await fs.rm(targetDir, { recursive: true, force: true }).catch(() => { });
|
|
1214
|
+
await this.repository.prune().catch(() => { });
|
|
1152
1215
|
// A branch origin does not have is a fact about the branch, not a
|
|
1153
1216
|
// failure of ours — but WHICH fact depends on whether the platform ever
|
|
1154
1217
|
// knew the name. A branch it cloned or listed and origin no longer has
|
|
@@ -1196,8 +1259,14 @@ export class WorkspaceService {
|
|
|
1196
1259
|
*/
|
|
1197
1260
|
async deleteWorkspace(workspaceId) {
|
|
1198
1261
|
const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
|
|
1199
|
-
await fs.rm(workspaceDir, { recursive: true, force: true });
|
|
1200
1262
|
const branch = branchForWorkspaceId(workspaceId);
|
|
1263
|
+
const repoDir = path.join(workspaceDir, this.kbDirName);
|
|
1264
|
+
// A worktree is removed through git and pruned from the repository, with
|
|
1265
|
+
// its branch's local ref; the repository's objects and every other
|
|
1266
|
+
// worktree are untouched.
|
|
1267
|
+
if (await this.isWorktree(repoDir))
|
|
1268
|
+
await this.repository.removeWorktree(repoDir, branch);
|
|
1269
|
+
await fs.rm(workspaceDir, { recursive: true, force: true });
|
|
1201
1270
|
this.branchDirs.delete(branch);
|
|
1202
1271
|
}
|
|
1203
1272
|
async listFiles(workspaceId, readFilter) {
|
|
@@ -1540,6 +1609,16 @@ export class WorkspaceService {
|
|
|
1540
1609
|
async ensureRemotesFetched(workspaceId, opts = {}) {
|
|
1541
1610
|
const workspaceDir = await this.resolveWorkspaceDir(workspaceId);
|
|
1542
1611
|
const repoDir = path.join(workspaceDir, this.kbDirName);
|
|
1612
|
+
if (await this.isWorktree(repoDir)) {
|
|
1613
|
+
// ONE fetch for every workspace: the repository's, throttled and joined
|
|
1614
|
+
// there.
|
|
1615
|
+
const ok = await this.repository.fetch({ maxAgeMs: FETCH_CACHE_TTL_MS, force: opts.force });
|
|
1616
|
+
if (opts.strict && !ok)
|
|
1617
|
+
throw new Error('git fetch origin failed — remote refs could not be refreshed');
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1620
|
+
// A full clone from an earlier release fetches on its own until the boot
|
|
1621
|
+
// phase converts it.
|
|
1543
1622
|
const now = Date.now();
|
|
1544
1623
|
// The TTL is stamped only by a SUCCESSFUL fetch, so a fresh window is
|
|
1545
1624
|
// itself the proof a strict caller wants.
|
|
@@ -2197,13 +2276,7 @@ export class WorkspaceService {
|
|
|
2197
2276
|
catch {
|
|
2198
2277
|
return false;
|
|
2199
2278
|
}
|
|
2200
|
-
|
|
2201
|
-
await fs.access(path.join(workspaceDir, this.kbDirName, '.git'));
|
|
2202
|
-
return true;
|
|
2203
|
-
}
|
|
2204
|
-
catch {
|
|
2205
|
-
return false;
|
|
2206
|
-
}
|
|
2279
|
+
return this.hasWorkingCopy(path.join(workspaceDir, this.kbDirName)).catch(() => false);
|
|
2207
2280
|
}
|
|
2208
2281
|
/**
|
|
2209
2282
|
* Enumerate every branch this process has bootstrapped a workspace for.
|
|
@@ -2332,6 +2405,14 @@ export class WorkspaceService {
|
|
|
2332
2405
|
log.warn(`sweep failed for ${entry.name}:`, { err });
|
|
2333
2406
|
}
|
|
2334
2407
|
}
|
|
2408
|
+
// The repository is not under the root, so the sweep never touched it.
|
|
2409
|
+
// What it forgets is the worktrees whose folders went, and their
|
|
2410
|
+
// branches' local refs, as removing a clone took its branch with it.
|
|
2411
|
+
if (removed.length > 0) {
|
|
2412
|
+
await this.repository
|
|
2413
|
+
.forgetWorktrees(removed.map((id) => branchForWorkspaceId(id)))
|
|
2414
|
+
.catch((err) => log.warn('could not prune the swept worktrees from the repository:', { err }));
|
|
2415
|
+
}
|
|
2335
2416
|
return { removed };
|
|
2336
2417
|
}
|
|
2337
2418
|
/**
|
|
@@ -2392,14 +2473,12 @@ export class WorkspaceService {
|
|
|
2392
2473
|
return this.branchDirs.get(branch);
|
|
2393
2474
|
}
|
|
2394
2475
|
const workspaceDir = this.workspaceDirWithinRoot(workspaceId);
|
|
2395
|
-
|
|
2396
|
-
|
|
2476
|
+
// A worktree whose link to the repository is broken falls through too:
|
|
2477
|
+
// the bootstrap recreates it.
|
|
2478
|
+
if (await this.hasWorkingCopy(path.join(workspaceDir, this.kbDirName)).catch(() => false)) {
|
|
2397
2479
|
this.registerBranchDir(branch, workspaceDir);
|
|
2398
2480
|
return workspaceDir;
|
|
2399
2481
|
}
|
|
2400
|
-
catch {
|
|
2401
|
-
// Fall through to lazy bootstrap.
|
|
2402
|
-
}
|
|
2403
2482
|
try {
|
|
2404
2483
|
await this.getOrCreateForBranch(branch);
|
|
2405
2484
|
const dir = this.branchDirs.get(branch);
|