canopycms 0.0.67 → 0.0.68-int.101
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/README.md +24 -17
- package/dist/ai/handler.js +8 -0
- package/dist/api/admin-branch-health.js +17 -20
- package/dist/api/admin.d.ts +30 -9
- package/dist/api/admin.js +32 -2
- package/dist/api/assets.js +84 -76
- package/dist/api/branch-create-window.d.ts +12 -0
- package/dist/api/branch-create-window.js +13 -0
- package/dist/api/branch-review.d.ts +3 -3
- package/dist/api/branch-review.js +14 -5
- package/dist/api/branch-status.d.ts +5 -5
- package/dist/api/branch-status.js +26 -6
- package/dist/api/branch-withdraw.d.ts +2 -2
- package/dist/api/branch-withdraw.js +7 -2
- package/dist/api/branch.d.ts +15 -2
- package/dist/api/branch.js +118 -92
- package/dist/api/client.d.ts +18 -6
- package/dist/api/client.js +79 -12
- package/dist/api/content.d.ts +6 -5
- package/dist/api/content.js +44 -41
- package/dist/api/entries.js +16 -15
- package/dist/api/github-sync.d.ts +19 -1
- package/dist/api/github-sync.js +57 -10
- package/dist/api/index.d.ts +1 -1
- package/dist/api/reference-options.js +1 -1
- package/dist/api/resolve-references.js +14 -40
- package/dist/api/settings-helpers.d.ts +1 -1
- package/dist/api/settings-helpers.js +1 -4
- package/dist/api/user.d.ts +3 -0
- package/dist/api/user.js +3 -0
- package/dist/assets/asset-url.d.ts +22 -16
- package/dist/assets/asset-url.js +42 -25
- package/dist/assets/factory.d.ts +5 -0
- package/dist/assets/factory.js +5 -0
- package/dist/assets/finalize.js +3 -0
- package/dist/assets/index.d.ts +1 -1
- package/dist/assets/materialize.d.ts +153 -0
- package/dist/assets/materialize.js +402 -0
- package/dist/assets/store-local.d.ts +19 -5
- package/dist/assets/store-local.js +69 -10
- package/dist/assets/store-s3.d.ts +38 -7
- package/dist/assets/store-s3.js +128 -22
- package/dist/assets/transform-directives.d.ts +52 -13
- package/dist/assets/transform-directives.js +87 -27
- package/dist/assets/transform.d.ts +3 -3
- package/dist/assets/transform.js +13 -8
- package/dist/assets/types.d.ts +31 -4
- package/dist/auth/file-based-auth-cache.js +7 -8
- package/dist/authorization/content.d.ts +5 -4
- package/dist/authorization/content.js +5 -5
- package/dist/authorization/path.d.ts +11 -6
- package/dist/authorization/path.js +11 -6
- package/dist/authorization/types.d.ts +2 -2
- package/dist/branch-health.d.ts +7 -1
- package/dist/branch-health.js +8 -3
- package/dist/branch-metadata.d.ts +5 -5
- package/dist/branch-metadata.js +44 -36
- package/dist/branch-provisioning.d.ts +167 -0
- package/dist/branch-provisioning.js +501 -0
- package/dist/branch-schema-cache.d.ts +5 -3
- package/dist/branch-schema-cache.js +29 -11
- package/dist/branch-sparse.d.ts +23 -0
- package/dist/branch-sparse.js +118 -0
- package/dist/branch-workspace.d.ts +42 -3
- package/dist/branch-workspace.js +240 -94
- package/dist/build/asset-refs.d.ts +93 -0
- package/dist/build/asset-refs.js +251 -0
- package/dist/build-identity.d.ts +3 -0
- package/dist/build-identity.js +13 -0
- package/dist/cli/asset-refs.d.ts +52 -0
- package/dist/cli/asset-refs.js +194 -0
- package/dist/cli/cli.d.ts +15 -1
- package/dist/cli/cli.js +5928 -2195
- package/dist/cli/configured-asset-store.d.ts +12 -0
- package/dist/cli/configured-asset-store.js +46 -0
- package/dist/cli/generate-ai-content.js +2185 -587
- package/dist/cli/github-app-manifest.d.ts +4 -3
- package/dist/cli/github-app-manifest.js +4 -3
- package/dist/cli/init.js +30 -27
- package/dist/cli/migrate.js +12 -8
- package/dist/cli/sync.js +83 -37
- package/dist/cli/template-files/Dockerfile.cms.template +8 -0
- package/dist/cli/template-files/cdk-app.ts.template +4 -0
- package/dist/cli/template-files/cms-stack.ts.template +23 -12
- package/dist/cli/template-files/deploy-cms.yml.template +2 -0
- package/dist/client.d.ts +1 -1
- package/dist/client.js +1 -1
- package/dist/config/helpers.js +1 -2
- package/dist/config/schemas/config.d.ts +19 -32
- package/dist/config/schemas/config.js +9 -11
- package/dist/config/schemas/field.js +1 -0
- package/dist/config/schemas/media.d.ts +4 -13
- package/dist/config/schemas/media.js +5 -8
- package/dist/config/schemas/url.d.ts +4 -20
- package/dist/config/schemas/url.js +6 -22
- package/dist/config/types.d.ts +30 -20
- package/dist/config/validation.d.ts +6 -0
- package/dist/config/validation.js +37 -0
- package/dist/content-listing.d.ts +12 -12
- package/dist/content-listing.js +10 -6
- package/dist/content-reader.d.ts +11 -2
- package/dist/content-reader.js +22 -14
- package/dist/content-store.d.ts +37 -9
- package/dist/content-store.js +70 -43
- package/dist/content-tree.d.ts +1 -1
- package/dist/content-tree.js +2 -2
- package/dist/context.d.ts +27 -9
- package/dist/context.js +154 -70
- package/dist/editor/BranchManager.d.ts +7 -1
- package/dist/editor/BranchManager.js +29 -14
- package/dist/editor/CanopyEditor.d.ts +1 -1
- package/dist/editor/CanopyEditor.js +2 -6
- package/dist/editor/Editor.d.ts +4 -3
- package/dist/editor/Editor.js +32 -22
- package/dist/editor/FormRenderer.js +34 -18
- package/dist/editor/PreviewFrame.d.ts +24 -0
- package/dist/editor/PreviewFrame.js +140 -0
- package/dist/editor/admin/SystemHealthPanel.js +47 -7
- package/dist/editor/components/BranchesDrawer.d.ts +9 -0
- package/dist/editor/components/BranchesDrawer.js +4 -0
- package/dist/editor/components/NoEditPermissionNotice.d.ts +10 -0
- package/dist/editor/components/NoEditPermissionNotice.js +17 -0
- package/dist/editor/context/AssetContext.d.ts +18 -35
- package/dist/editor/context/AssetContext.js +20 -29
- package/dist/editor/context/index.d.ts +1 -1
- package/dist/editor/context/index.js +1 -1
- package/dist/editor/editor-config.d.ts +1 -2
- package/dist/editor/editor-config.js +0 -23
- package/dist/editor/editor-utils.d.ts +24 -21
- package/dist/editor/editor-utils.js +75 -62
- package/dist/editor/fields/BlockField.d.ts +1 -0
- package/dist/editor/fields/BlockField.js +6 -4
- package/dist/editor/fields/CodeField.d.ts +1 -0
- package/dist/editor/fields/CodeField.js +2 -2
- package/dist/editor/fields/DateTimeField.d.ts +1 -0
- package/dist/editor/fields/DateTimeField.js +2 -2
- package/dist/editor/fields/FieldDescription.d.ts +16 -0
- package/dist/editor/fields/FieldDescription.js +11 -0
- package/dist/editor/fields/ImageField.d.ts +1 -0
- package/dist/editor/fields/ImageField.js +3 -2
- package/dist/editor/fields/InlineGroupField.js +4 -1
- package/dist/editor/fields/MarkdownField.d.ts +1 -0
- package/dist/editor/fields/MarkdownField.js +115 -20
- package/dist/editor/fields/NumberField.d.ts +1 -0
- package/dist/editor/fields/NumberField.js +2 -2
- package/dist/editor/fields/NumberListField.d.ts +1 -0
- package/dist/editor/fields/NumberListField.js +2 -2
- package/dist/editor/fields/ObjectField.d.ts +1 -0
- package/dist/editor/fields/ObjectField.js +5 -2
- package/dist/editor/fields/ReferenceField.d.ts +1 -0
- package/dist/editor/fields/ReferenceField.js +5 -4
- package/dist/editor/fields/SelectField.d.ts +1 -0
- package/dist/editor/fields/SelectField.js +2 -2
- package/dist/editor/fields/StringListField.d.ts +1 -0
- package/dist/editor/fields/StringListField.js +2 -2
- package/dist/editor/fields/TextField.d.ts +1 -0
- package/dist/editor/fields/TextField.js +2 -2
- package/dist/editor/fields/ToggleField.d.ts +1 -0
- package/dist/editor/fields/ToggleField.js +7 -4
- package/dist/editor/fields/entry-link/InsertEntryLink.d.ts +1 -1
- package/dist/editor/fields/entry-link/InsertEntryLink.js +3 -2
- package/dist/editor/fields/mdx-jsx-support.d.ts +1 -0
- package/dist/editor/fields/mdx-jsx-support.js +137 -0
- package/dist/editor/hooks/create-branch-request.d.ts +20 -0
- package/dist/editor/hooks/create-branch-request.js +52 -0
- package/dist/editor/hooks/useBranchActions.d.ts +13 -2
- package/dist/editor/hooks/useBranchActions.js +48 -17
- package/dist/editor/hooks/useBranchManager.d.ts +21 -0
- package/dist/editor/hooks/useBranchManager.js +204 -37
- package/dist/editor/hooks/useBranchesData.d.ts +6 -0
- package/dist/editor/hooks/useBranchesData.js +9 -3
- package/dist/editor/hooks/useCommentSystem.js +25 -6
- package/dist/editor/hooks/useDraftManager.d.ts +16 -1
- package/dist/editor/hooks/useDraftManager.js +193 -36
- package/dist/editor/hooks/useEntriesData.js +3 -3
- package/dist/editor/hooks/useEntryManager.d.ts +2 -1
- package/dist/editor/hooks/useEntryManager.js +34 -19
- package/dist/editor/media/AssetCard.d.ts +1 -1
- package/dist/editor/media/crop-math.d.ts +3 -4
- package/dist/editor/media/crop-math.js +11 -22
- package/dist/editor/media/editor-image-src.d.ts +7 -0
- package/dist/editor/media/editor-image-src.js +12 -0
- package/dist/editor/preview-asset-base.d.ts +8 -0
- package/dist/editor/preview-asset-base.js +13 -0
- package/dist/editor/preview-bridge.d.ts +9 -21
- package/dist/editor/preview-bridge.js +19 -139
- package/dist/editor/preview-path.d.ts +10 -0
- package/dist/editor/preview-path.js +20 -0
- package/dist/editor/theme.js +2 -2
- package/dist/entry-schema-registry.d.ts +4 -4
- package/dist/entry-schema-registry.js +10 -6
- package/dist/entry-schema.d.ts +27 -4
- package/dist/entry-schema.js +20 -3
- package/dist/git-manager.d.ts +173 -6
- package/dist/git-manager.js +594 -76
- package/dist/github-service.d.ts +15 -0
- package/dist/github-service.js +19 -1
- package/dist/http/handler.js +37 -13
- package/dist/http/index.d.ts +2 -0
- package/dist/http/index.js +2 -0
- package/dist/http/router.d.ts +2 -0
- package/dist/http/router.js +2 -1
- package/dist/http/types.d.ts +2 -0
- package/dist/http/worker-not-ready.d.ts +10 -0
- package/dist/http/worker-not-ready.js +24 -0
- package/dist/operating-mode/client-unsafe-strategy.js +0 -6
- package/dist/operating-mode/types.d.ts +1 -4
- package/dist/paths/branch-name.d.ts +5 -0
- package/dist/paths/branch-name.js +9 -0
- package/dist/paths/branch.d.ts +6 -2
- package/dist/paths/branch.js +16 -10
- package/dist/paths/index.d.ts +3 -3
- package/dist/paths/index.js +3 -3
- package/dist/paths/normalize.d.ts +7 -0
- package/dist/paths/normalize.js +9 -0
- package/dist/paths/validation.js +0 -2
- package/dist/preview.d.ts +8 -0
- package/dist/preview.js +7 -0
- package/dist/reference-resolver.d.ts +5 -21
- package/dist/reference-resolver.js +9 -44
- package/dist/resolve-canopy-user.js +2 -1
- package/dist/schema/schema-store.d.ts +54 -31
- package/dist/schema/schema-store.js +77 -31
- package/dist/server.d.ts +64 -9
- package/dist/server.js +48 -8
- package/dist/services.d.ts +18 -6
- package/dist/services.js +85 -70
- package/dist/settings-workspace.d.ts +23 -3
- package/dist/settings-workspace.js +197 -45
- package/dist/static/seo.d.ts +2 -14
- package/dist/static/seo.js +2 -25
- package/dist/submission-attribution.d.ts +73 -0
- package/dist/submission-attribution.js +221 -0
- package/dist/sync-core.d.ts +12 -1
- package/dist/sync-core.js +31 -13
- package/dist/task-queue/worker-status.d.ts +8 -0
- package/dist/task-queue/worker-status.js +17 -0
- package/dist/types.d.ts +31 -2
- package/dist/utils/content-serialize.d.ts +5 -2
- package/dist/utils/content-serialize.js +98 -24
- package/dist/utils/content-write-lock.d.ts +18 -7
- package/dist/utils/content-write-lock.js +18 -9
- package/dist/utils/debug.d.ts +8 -0
- package/dist/utils/debug.js +10 -2
- package/dist/utils/git.d.ts +32 -0
- package/dist/utils/git.js +42 -0
- package/dist/utils/occ-json-write.js +10 -2
- package/dist/utils/provision-log.d.ts +25 -0
- package/dist/utils/provision-log.js +49 -0
- package/dist/utils/provisioning-lock.d.ts +35 -5
- package/dist/utils/provisioning-lock.js +73 -12
- package/dist/utils/request-timing.d.ts +25 -0
- package/dist/utils/request-timing.js +101 -0
- package/dist/utils/sanitize-href.d.ts +8 -22
- package/dist/utils/sanitize-href.js +11 -28
- package/dist/utils/url-prefix.d.ts +30 -0
- package/dist/utils/url-prefix.js +61 -0
- package/dist/utils/yaml-source-splice.d.ts +58 -0
- package/dist/utils/yaml-source-splice.js +515 -0
- package/dist/validation/entry-validator.js +9 -3
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/dist/worker/canopy-state.d.ts +45 -0
- package/dist/worker/canopy-state.js +75 -0
- package/dist/worker/cms-worker.d.ts +8 -3
- package/dist/worker/cms-worker.js +38 -2
- package/dist/worker/git-sync.d.ts +30 -12
- package/dist/worker/git-sync.js +253 -58
- package/dist/worker/history-rewrite.d.ts +1 -1
- package/dist/worker/history-rewrite.js +1 -1
- package/dist/worker/provisioned-workspace.d.ts +35 -0
- package/dist/worker/provisioned-workspace.js +50 -0
- package/dist/worker/rebase.d.ts +1 -1
- package/dist/worker/rebase.js +95 -23
- package/dist/worker/remote-git-maintenance.d.ts +3 -0
- package/dist/worker/remote-git-maintenance.js +11 -0
- package/dist/worker/sparse-cone.d.ts +20 -0
- package/dist/worker/sparse-cone.js +132 -0
- package/dist/worker/task-runner.js +99 -14
- package/dist/worker/worker-context.d.ts +6 -1
- package/package.json +8 -2
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type ProvisionedWorkspaceHold = {
|
|
2
|
+
kind: 'held';
|
|
3
|
+
release: () => Promise<void>;
|
|
4
|
+
/**
|
|
5
|
+
* Whether the lock was lost mid-hold (its marker vanished or was taken over), so another
|
|
6
|
+
* process may now be cloning into this directory. Callers check it before each destructive
|
|
7
|
+
* git step and stop rather than carry on unguarded.
|
|
8
|
+
*/
|
|
9
|
+
isCompromised: () => boolean;
|
|
10
|
+
}
|
|
11
|
+
/** Another process holds the provisioning lock: a clone, or an admin purge or repair, is in flight. */
|
|
12
|
+
| {
|
|
13
|
+
kind: 'locked';
|
|
14
|
+
}
|
|
15
|
+
/** No clone with metadata at this path yet. */
|
|
16
|
+
| {
|
|
17
|
+
kind: 'not-provisioned';
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Take a branch workspace's provisioning lock for the sync loop, the same cross-host lock the
|
|
21
|
+
* Lambda holds while it publishes the workspace (branch-provisioning.ts) and admin actions hold
|
|
22
|
+
* while they repair or purge it. The caller holds it across its dirty check and every git step, and
|
|
23
|
+
* releases it in a `finally`.
|
|
24
|
+
*
|
|
25
|
+
* Zero retries: the worker skips and retries next cycle rather than wait, so it never blocks on
|
|
26
|
+
* the Lambda and cannot deadlock with it. The rebase nests the content-write lock inside this one;
|
|
27
|
+
* the worker takes that one try-only as well, so the nesting cannot deadlock either.
|
|
28
|
+
*
|
|
29
|
+
* Provisioned means a `.git` directory AND `branch.json`. A workspace is published complete with
|
|
30
|
+
* both ([PROV-1], branch-provisioning.ts), so anything else at a branch name is a leftover for
|
|
31
|
+
* `repairBranchDirResidue` (git-sync.ts).
|
|
32
|
+
*/
|
|
33
|
+
export declare function holdProvisionedWorkspace(contentBranchesPath: string, dirName: string): Promise<ProvisionedWorkspaceHold>;
|
|
34
|
+
/** Release a hold, logging rather than throwing: the sync loop's callers never throw. */
|
|
35
|
+
export declare function releaseProvisionedWorkspace(release: () => Promise<void>, dirName: string): Promise<void>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { BRANCH_META_DIR, BRANCH_META_FILE } from '../branch-metadata-file.js';
|
|
4
|
+
import { getErrorMessage, isNodeError } from '../utils/error.js';
|
|
5
|
+
import { branchProvisioningLockName, tryAcquireProvisioningLock } from '../utils/provisioning-lock.js';
|
|
6
|
+
import { workerLogWarn } from './log.js';
|
|
7
|
+
/**
|
|
8
|
+
* Take a branch workspace's provisioning lock for the sync loop, the same cross-host lock the
|
|
9
|
+
* Lambda holds while it publishes the workspace (branch-provisioning.ts) and admin actions hold
|
|
10
|
+
* while they repair or purge it. The caller holds it across its dirty check and every git step, and
|
|
11
|
+
* releases it in a `finally`.
|
|
12
|
+
*
|
|
13
|
+
* Zero retries: the worker skips and retries next cycle rather than wait, so it never blocks on
|
|
14
|
+
* the Lambda and cannot deadlock with it. The rebase nests the content-write lock inside this one;
|
|
15
|
+
* the worker takes that one try-only as well, so the nesting cannot deadlock either.
|
|
16
|
+
*
|
|
17
|
+
* Provisioned means a `.git` directory AND `branch.json`. A workspace is published complete with
|
|
18
|
+
* both ([PROV-1], branch-provisioning.ts), so anything else at a branch name is a leftover for
|
|
19
|
+
* `repairBranchDirResidue` (git-sync.ts).
|
|
20
|
+
*/
|
|
21
|
+
export async function holdProvisionedWorkspace(contentBranchesPath, dirName) {
|
|
22
|
+
let release;
|
|
23
|
+
let compromised = false;
|
|
24
|
+
try {
|
|
25
|
+
release = await tryAcquireProvisioningLock(contentBranchesPath, branchProvisioningLockName(dirName), (err) => {
|
|
26
|
+
compromised = true;
|
|
27
|
+
workerLogWarn(` Provisioning lock for ${dirName} was compromised mid-hold: ${getErrorMessage(err)}`);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
if (isNodeError(err) && err.code === 'ELOCKED')
|
|
32
|
+
return { kind: 'locked' };
|
|
33
|
+
throw err;
|
|
34
|
+
}
|
|
35
|
+
const root = path.join(contentBranchesPath, dirName);
|
|
36
|
+
const [gitIsDir, hasMetadata] = await Promise.all([
|
|
37
|
+
fs.stat(path.join(root, '.git')).then((stat) => stat.isDirectory(), () => false),
|
|
38
|
+
fs.stat(path.join(root, BRANCH_META_DIR, BRANCH_META_FILE)).then(() => true, () => false),
|
|
39
|
+
]);
|
|
40
|
+
if (gitIsDir && hasMetadata)
|
|
41
|
+
return { kind: 'held', release, isCompromised: () => compromised };
|
|
42
|
+
await releaseProvisionedWorkspace(release, dirName);
|
|
43
|
+
return { kind: 'not-provisioned' };
|
|
44
|
+
}
|
|
45
|
+
/** Release a hold, logging rather than throwing: the sync loop's callers never throw. */
|
|
46
|
+
export async function releaseProvisionedWorkspace(release, dirName) {
|
|
47
|
+
await release().catch((err) => {
|
|
48
|
+
workerLogWarn(` Failed to release the provisioning lock for ${dirName}: ${getErrorMessage(err)}`);
|
|
49
|
+
});
|
|
50
|
+
}
|
package/dist/worker/rebase.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ import type { WorkerContext } from './worker-context.js';
|
|
|
20
20
|
* but still need their PR's resolution polled, and this loop is the only thing
|
|
21
21
|
* that walks every branch workspace.
|
|
22
22
|
*/
|
|
23
|
-
export type RebaseContext = Pick<WorkerContext, 'githubOwner' | 'githubRepo' | 'baseBranch' | 'sanitizedBaseBranch' | 'contentBranchesPath' | 'contentRoot' | 'remoteGitPath' | 'taskDir' | 'taskTimeoutMs' | 'octokit' | 'afterConflictDetectedForTesting' | 'afterRebaseCompletedForTesting'>;
|
|
23
|
+
export type RebaseContext = Pick<WorkerContext, 'githubOwner' | 'githubRepo' | 'baseBranch' | 'sanitizedBaseBranch' | 'contentBranchesPath' | 'contentRoot' | 'remoteGitPath' | 'taskDir' | 'taskTimeoutMs' | 'octokit' | 'afterConflictDetectedForTesting' | 'afterRebaseCompletedForTesting' | 'ensureSettingsBranch'>;
|
|
24
24
|
/**
|
|
25
25
|
* Per-cycle outcome of `rebaseActiveBranches()`, folded by `syncGit()` into the
|
|
26
26
|
* worker's self-reported status (`WorkerStatusReport.lastGitSync`, see
|
package/dist/worker/rebase.js
CHANGED
|
@@ -4,11 +4,14 @@ import { simpleGit } from 'simple-git';
|
|
|
4
4
|
import { BranchMetadataFileManager, buildMergedBranchUpdate, getBranchMetadataFileManager, } from '../branch-metadata.js';
|
|
5
5
|
import { extractIdFromFilename } from '../content-id-index.js';
|
|
6
6
|
import { invalidateBranchContentCaches } from '../content-index-generation.js';
|
|
7
|
+
import { isSettingsBranchName } from '../paths/branch-name.js';
|
|
7
8
|
import { normalizeFilesystemPath } from '../paths/normalize.js';
|
|
8
9
|
import { ROOT_COLLECTION_ID } from '../paths/types.js';
|
|
9
10
|
import { tryAcquireContentWriteLock } from '../utils/content-write-lock.js';
|
|
11
|
+
import { holdProvisionedWorkspace, releaseProvisionedWorkspace, } from './provisioned-workspace.js';
|
|
10
12
|
import { getErrorMessage, isNodeError, redactCredentials } from '../utils/error.js';
|
|
11
|
-
import { isRebaseInProgress } from '../utils/git.js';
|
|
13
|
+
import { isCanopyInternalPath, isRebaseInProgress } from '../utils/git.js';
|
|
14
|
+
import { TRACKED_CANOPY_STATE_FIX, restoreRetiredSchemaCache, splitByUpstreamTracking, trackedCanopyStateChanges, } from './canopy-state.js';
|
|
12
15
|
import { enqueueGitHubPush, forcePublishToLocalRemote, markHistoryRewritten, readPublishedSha, reconcilePendingRewrite, } from './history-rewrite.js';
|
|
13
16
|
import { workerLog, workerLogWarn } from './log.js';
|
|
14
17
|
/**
|
|
@@ -123,7 +126,7 @@ const MAX_REBASE_ROUNDS = 50;
|
|
|
123
126
|
* - the **unexpected-error** exit aborts HERE, before breaking, so the caller's
|
|
124
127
|
* `!completed` abort is a caught no-op for that path;
|
|
125
128
|
* - the **lock-compromised** exit leaves the rebase in progress and is aborted
|
|
126
|
-
* by `rebaseOneBranch`'s own `
|
|
129
|
+
* by `rebaseOneBranch`'s own `lockCompromised() && !completed` branch,
|
|
127
130
|
* which returns `skippedLocked` before the `!completed` block below it is
|
|
128
131
|
* reached -- a distinct site that cannot be folded into that one;
|
|
129
132
|
* - the **conflict-resolution-failure** and **MAX_REBASE_ROUNDS** exits are the
|
|
@@ -159,8 +162,8 @@ async function runRebaseRounds(ctx, branchGit, branchDir, fetchedBaseTip, isLock
|
|
|
159
162
|
break;
|
|
160
163
|
try {
|
|
161
164
|
if (nextAction === 'start') {
|
|
162
|
-
// The pinned base tip fetched above
|
|
163
|
-
//
|
|
165
|
+
// The pinned base tip fetched above; a fetch by path updates no
|
|
166
|
+
// remote-tracking ref.
|
|
164
167
|
await branchGit.rebase([fetchedBaseTip]);
|
|
165
168
|
}
|
|
166
169
|
else if (nextAction === 'continue') {
|
|
@@ -196,20 +199,23 @@ async function runRebaseRounds(ctx, branchGit, branchDir, fetchedBaseTip, isLock
|
|
|
196
199
|
//
|
|
197
200
|
// A per-file resolution that STILL fails routes into the `!completed`
|
|
198
201
|
// path below, which aborts and records, rather than escaping.
|
|
202
|
+
//
|
|
203
|
+
// `--sparse` on add/rm: a conflicted path outside a sparse clone's cone
|
|
204
|
+
// is materialised for the conflict, but git refuses to stage it without.
|
|
199
205
|
const conflictKind = new Map(st.files.map((f) => [f.path, `${f.index}${f.working_dir}`]));
|
|
200
206
|
let resolutionFailure;
|
|
201
207
|
for (const file of st.conflicted) {
|
|
202
208
|
const kind = conflictKind.get(file);
|
|
203
209
|
try {
|
|
204
210
|
if (kind === 'UD') {
|
|
205
|
-
await branchGit.raw(['rm', '-f', '--', file]);
|
|
211
|
+
await branchGit.raw(['rm', '-f', '--sparse', '--', file]);
|
|
206
212
|
}
|
|
207
213
|
else if (kind === 'DU') {
|
|
208
|
-
await branchGit.add
|
|
214
|
+
await branchGit.raw(['add', '--sparse', '--', file]);
|
|
209
215
|
}
|
|
210
216
|
else {
|
|
211
217
|
await branchGit.raw(['checkout', '--theirs', file]);
|
|
212
|
-
await branchGit.add
|
|
218
|
+
await branchGit.raw(['add', '--sparse', '--', file]);
|
|
213
219
|
}
|
|
214
220
|
}
|
|
215
221
|
catch (resolveErr) {
|
|
@@ -406,7 +412,14 @@ export async function runRebaseCycle(ctx) {
|
|
|
406
412
|
workerLog(` Skipping ${branchDir}: base branch (refreshed separately)`);
|
|
407
413
|
continue;
|
|
408
414
|
}
|
|
409
|
-
|
|
415
|
+
// A settings branch is an orphan: rebasing it onto the base would give it content
|
|
416
|
+
// history. The API refuses to provision one, so a directory under that name is a
|
|
417
|
+
// leftover and is left alone.
|
|
418
|
+
if (isSettingsBranchName(branchDir, ctx.ensureSettingsBranch())) {
|
|
419
|
+
workerLogWarn(` Skipping ${branchDir}: settings branch, never a content workspace`);
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
const outcome = await holdAndRebaseOneBranch(ctx, branchDir, branchPath);
|
|
410
423
|
if (outcome.kind === 'rebased')
|
|
411
424
|
rebased.push(branchDir);
|
|
412
425
|
else if (outcome.kind === 'skippedDirty')
|
|
@@ -423,6 +436,39 @@ export async function runRebaseCycle(ctx) {
|
|
|
423
436
|
}
|
|
424
437
|
return { rebased, skippedDirty, skippedLocked, failed };
|
|
425
438
|
}
|
|
439
|
+
/**
|
|
440
|
+
* {@link rebaseOneBranch} under the branch's provisioning lock, so no git step races a clone of
|
|
441
|
+
* this directory. A branch whose lock is held elsewhere is `skippedLocked` and retried next cycle;
|
|
442
|
+
* one with no `branch.json` is skipped. Lock order: provisioning, then the
|
|
443
|
+
* content-write lock inside it; see `provisioned-workspace.ts` for why that order cannot deadlock.
|
|
444
|
+
* Never throws, like rebaseOneBranch.
|
|
445
|
+
*/
|
|
446
|
+
async function holdAndRebaseOneBranch(ctx, branchDir, branchPath) {
|
|
447
|
+
let hold;
|
|
448
|
+
try {
|
|
449
|
+
hold = await holdProvisionedWorkspace(ctx.contentBranchesPath, branchDir);
|
|
450
|
+
}
|
|
451
|
+
catch (err) {
|
|
452
|
+
// [REDACT] failed[] is served to the browser via worker-status.json.
|
|
453
|
+
const error = redactCredentials(`could not take the provisioning lock: ${getErrorMessage(err)}`);
|
|
454
|
+
workerLogWarn(` Skipping ${branchDir}: ${error}`);
|
|
455
|
+
return { kind: 'failed', error };
|
|
456
|
+
}
|
|
457
|
+
if (hold.kind === 'locked') {
|
|
458
|
+
workerLog(` Skipping ${branchDir}: provisioning lock held elsewhere (retrying next cycle)`);
|
|
459
|
+
return { kind: 'skippedLocked' };
|
|
460
|
+
}
|
|
461
|
+
// Silent: a directory without branch.json is a staging-era leftover, which
|
|
462
|
+
// repairBranchDirResidue (git-sync.ts) quarantines and logs at the cycle's start.
|
|
463
|
+
if (hold.kind === 'not-provisioned')
|
|
464
|
+
return { kind: 'none' };
|
|
465
|
+
try {
|
|
466
|
+
return await rebaseOneBranch(ctx, branchDir, branchPath, hold.isCompromised);
|
|
467
|
+
}
|
|
468
|
+
finally {
|
|
469
|
+
await releaseProvisionedWorkspace(hold.release, branchDir);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
426
472
|
/**
|
|
427
473
|
* Rebase ONE branch workspace onto the base branch, keeping the branch's
|
|
428
474
|
* version of every conflicting file.
|
|
@@ -435,7 +481,7 @@ export async function runRebaseCycle(ctx) {
|
|
|
435
481
|
* returned as `{ kind: 'failed' }`, so one unreadable branch cannot halt the
|
|
436
482
|
* rest of the cycle ([SYNC-M2]).
|
|
437
483
|
*/
|
|
438
|
-
async function rebaseOneBranch(ctx, branchDir, branchPath) {
|
|
484
|
+
async function rebaseOneBranch(ctx, branchDir, branchPath, isProvisioningLockCompromised) {
|
|
439
485
|
// Read only by the catch below. See the `rebased` rider on
|
|
440
486
|
// BranchRebaseOutcome.
|
|
441
487
|
let didRebase = false;
|
|
@@ -489,6 +535,9 @@ async function rebaseOneBranch(ctx, branchDir, branchPath) {
|
|
|
489
535
|
// record it and bail before the next one rather than replaying over
|
|
490
536
|
// an editor's concurrent save.
|
|
491
537
|
let contentLockCompromised = false;
|
|
538
|
+
// Either lock lost means another writer may be live against this tree;
|
|
539
|
+
// both get the same bail-before-the-next-destructive-step treatment.
|
|
540
|
+
const lockCompromised = () => contentLockCompromised || isProvisioningLockCompromised();
|
|
492
541
|
try {
|
|
493
542
|
releaseContentLock = await tryAcquireContentWriteLock(branchPath, (lockErr) => {
|
|
494
543
|
contentLockCompromised = true;
|
|
@@ -574,12 +623,37 @@ async function rebaseOneBranch(ctx, branchDir, branchPath) {
|
|
|
574
623
|
}
|
|
575
624
|
// Skip dirty branches — the editor has changes that cannot be rebased.
|
|
576
625
|
// Inside the lock, so no write can land between this check and the rebase
|
|
577
|
-
// below.
|
|
578
|
-
|
|
579
|
-
if (
|
|
626
|
+
// below. canopycms's own untracked state is not dirt.
|
|
627
|
+
let dirtyCheck = await branchGit.status();
|
|
628
|
+
if (await restoreRetiredSchemaCache(branchGit, dirtyCheck)) {
|
|
629
|
+
workerLog(` ${branchDir}: restored the retired in-tree schema cache`);
|
|
630
|
+
dirtyCheck = await branchGit.status();
|
|
631
|
+
}
|
|
632
|
+
const editorDirt = dirtyCheck.files.filter((f) => !isCanopyInternalPath(f.path));
|
|
633
|
+
if (editorDirt.length > 0) {
|
|
580
634
|
workerLog(` Skipping ${branchDir}: has uncommitted changes`);
|
|
581
635
|
return { kind: 'skippedDirty' };
|
|
582
636
|
}
|
|
637
|
+
// `git rebase` refuses to start over ANY modified tracked file, so
|
|
638
|
+
// tracked canopycms state blocks. The worker never untracks it here:
|
|
639
|
+
// replaying a branch commit that touched it writes historical bytes over
|
|
640
|
+
// the live file, and the abort that follows deletes it. So the index and
|
|
641
|
+
// the bytes stay untouched, and the wedge is recorded for an operator.
|
|
642
|
+
const trackedState = trackedCanopyStateChanges(dirtyCheck);
|
|
643
|
+
if (trackedState.length > 0) {
|
|
644
|
+
await branchGit.fetch(ctx.remoteGitPath, ctx.baseBranch);
|
|
645
|
+
const baseTip = (await branchGit.revparse(['FETCH_HEAD'])).trim();
|
|
646
|
+
const { stillTracked } = await splitByUpstreamTracking(branchGit, trackedState, baseTip);
|
|
647
|
+
const reason = stillTracked.length > 0
|
|
648
|
+
? `git cannot rebase over modified canopycms state the repo tracks ` +
|
|
649
|
+
`(${trackedState.join(', ')}). To fix, ${TRACKED_CANOPY_STATE_FIX}.`
|
|
650
|
+
: `git cannot rebase over modified canopycms state this clone still tracks ` +
|
|
651
|
+
`(${trackedState.join(', ')}), though the base branch no longer does; ` +
|
|
652
|
+
`the clone needs manual repair`;
|
|
653
|
+
workerLogWarn(` Skipping ${branchDir}: ${reason}`);
|
|
654
|
+
await recordRebaseFailure(ctx, branchPath, branchDir, reason);
|
|
655
|
+
return { kind: 'failed', error: reason };
|
|
656
|
+
}
|
|
583
657
|
// The clone's own ref name: branchDir is the sanitized DIRECTORY name and
|
|
584
658
|
// need not match it. A literal 'HEAD' means a detached clone (a crashed
|
|
585
659
|
// rebase can leave one), where nothing below can safely name a ref, so
|
|
@@ -601,14 +675,12 @@ async function rebaseOneBranch(ctx, branchDir, branchPath) {
|
|
|
601
675
|
marker: metaFile.branch.historyRewrittenFrom,
|
|
602
676
|
});
|
|
603
677
|
}
|
|
604
|
-
await branchGit.fetch(
|
|
678
|
+
await branchGit.fetch(ctx.remoteGitPath, ctx.baseBranch);
|
|
605
679
|
// rev-list, not status.behind, which needs an upstream tracking branch
|
|
606
680
|
// that checkoutBranch's fallback paths do not always configure. Against
|
|
607
|
-
// the just-fetched tip
|
|
608
|
-
//
|
|
609
|
-
//
|
|
610
|
-
// — FETCH_HEAD is one shared mutable file per repo, repointed by any
|
|
611
|
-
// concurrent fetch.
|
|
681
|
+
// the just-fetched tip: a fetch by path updates no remote-tracking ref.
|
|
682
|
+
// Pinned to a SHA immediately — FETCH_HEAD is one shared mutable file per
|
|
683
|
+
// repo, repointed by any concurrent fetch.
|
|
612
684
|
const fetchedBaseTip = (await branchGit.revparse(['FETCH_HEAD'])).trim();
|
|
613
685
|
const behindCount = parseInt((await branchGit.raw(['rev-list', '--count', `HEAD..${fetchedBaseTip}`])).trim(), 10);
|
|
614
686
|
const meta = getBranchMetadataFileManager(branchPath, ctx.contentBranchesPath);
|
|
@@ -651,7 +723,7 @@ async function rebaseOneBranch(ctx, branchDir, branchPath) {
|
|
|
651
723
|
// the rebase makes unreadable.
|
|
652
724
|
const preRebaseHead = (await branchGit.revparse(['HEAD'])).trim();
|
|
653
725
|
const publishedSha = canPublish ? await readPublishedSha(ctx, branchRef) : null;
|
|
654
|
-
const { completed, conflictedFiles, failureReason } = await runRebaseRounds(ctx, branchGit, branchDir, fetchedBaseTip,
|
|
726
|
+
const { completed, conflictedFiles, failureReason } = await runRebaseRounds(ctx, branchGit, branchDir, fetchedBaseTip, lockCompromised);
|
|
655
727
|
// Outside the round loop's try/catch, so a throwing test hook can
|
|
656
728
|
// never be misread as a rebase error.
|
|
657
729
|
if (completed)
|
|
@@ -676,14 +748,14 @@ async function rebaseOneBranch(ctx, branchDir, branchPath) {
|
|
|
676
748
|
// history while a concurrent save is uncommitted working-tree state, and
|
|
677
749
|
// that writer is already told to retry via ContentWriteLockBusyError. So
|
|
678
750
|
// when the rebase completed, log the lost exclusivity loudly and finish.
|
|
679
|
-
if (
|
|
680
|
-
workerLogWarn(` Skipping ${branchDir}:
|
|
751
|
+
if (lockCompromised() && !completed) {
|
|
752
|
+
workerLogWarn(` Skipping ${branchDir}: a lock it held was compromised mid-rebase (retrying next cycle)`);
|
|
681
753
|
// No-op when no rebase is in progress; failure is expected there.
|
|
682
754
|
await branchGit.rebase(['--abort']).catch(() => { });
|
|
683
755
|
return { kind: 'skippedLocked' };
|
|
684
756
|
}
|
|
685
|
-
if (
|
|
686
|
-
workerLogWarn(`
|
|
757
|
+
if (lockCompromised()) {
|
|
758
|
+
workerLogWarn(` A lock held for ${branchDir} was compromised, but its rebase had already completed -- finishing the sync (history is rewritten; skipping now would strand the history-rewrite marker and wedge the branch)`);
|
|
687
759
|
}
|
|
688
760
|
if (!completed) {
|
|
689
761
|
// failureReason is only set on the "unexpected error" break above;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { repackBareRemoteIfNeeded } from '../git-manager.js';
|
|
2
|
+
import { workerLog } from './log.js';
|
|
3
|
+
/** The worker's per-cycle repack of `remote.git` (git-manager.ts owns the rule), logged. */
|
|
4
|
+
export async function maintainRemoteGit(gitDir) {
|
|
5
|
+
const result = await repackBareRemoteIfNeeded(gitDir);
|
|
6
|
+
if (result.repacked) {
|
|
7
|
+
workerLog(`remote.git maintenance: loose ${result.before.loose}→${result.after.loose} ` +
|
|
8
|
+
`packs ${result.before.packs}→${result.after.packs} in ${result.ms} ms`);
|
|
9
|
+
}
|
|
10
|
+
return result;
|
|
11
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface SparseConeReport {
|
|
2
|
+
reapplied: string[];
|
|
3
|
+
failed: {
|
|
4
|
+
branch: string;
|
|
5
|
+
error: string;
|
|
6
|
+
}[];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Move each provisioned sparse clone to the cone the deployment's config now asks for, as
|
|
10
|
+
* recorded beside the branch directories by the Lambda (branch-sparse.ts): a changed content
|
|
11
|
+
* root would otherwise leave every existing clone without its content. A full clone stays
|
|
12
|
+
* full. Best-effort per clone, like [SYNC-M2].
|
|
13
|
+
*
|
|
14
|
+
* The cone is compared without a lock, and changed only under the clone's provisioning lock
|
|
15
|
+
* and content-write lock, both zero-retry, outside a rebase: `sparse-checkout set` rewrites the
|
|
16
|
+
* working tree. A clone that is busy, or holds an unpublished deletion, is retried next cycle.
|
|
17
|
+
*/
|
|
18
|
+
export declare function reapplySparseCones(ctx: {
|
|
19
|
+
contentBranchesPath: string;
|
|
20
|
+
}): Promise<SparseConeReport>;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { simpleGit } from 'simple-git';
|
|
4
|
+
import { readRecordedSparseCone, sameCone } from '../branch-sparse.js';
|
|
5
|
+
import { invalidateBranchContentCaches } from '../content-index-generation.js';
|
|
6
|
+
import { tryAcquireContentWriteLock } from '../utils/content-write-lock.js';
|
|
7
|
+
import { getErrorMessage, isNodeError } from '../utils/error.js';
|
|
8
|
+
import { isRebaseInProgress } from '../utils/git.js';
|
|
9
|
+
import { workerLog, workerLogWarn } from './log.js';
|
|
10
|
+
import { holdProvisionedWorkspace, releaseProvisionedWorkspace } from './provisioned-workspace.js';
|
|
11
|
+
/** A clone's cone directories, or null when it is not sparse (or cannot be read). */
|
|
12
|
+
async function currentCone(git) {
|
|
13
|
+
try {
|
|
14
|
+
const listed = (await git.raw(['sparse-checkout', 'list'])).trim();
|
|
15
|
+
return listed ? listed.split('\n') : null;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Move each provisioned sparse clone to the cone the deployment's config now asks for, as
|
|
23
|
+
* recorded beside the branch directories by the Lambda (branch-sparse.ts): a changed content
|
|
24
|
+
* root would otherwise leave every existing clone without its content. A full clone stays
|
|
25
|
+
* full. Best-effort per clone, like [SYNC-M2].
|
|
26
|
+
*
|
|
27
|
+
* The cone is compared without a lock, and changed only under the clone's provisioning lock
|
|
28
|
+
* and content-write lock, both zero-retry, outside a rebase: `sparse-checkout set` rewrites the
|
|
29
|
+
* working tree. A clone that is busy, or holds an unpublished deletion, is retried next cycle.
|
|
30
|
+
*/
|
|
31
|
+
export async function reapplySparseCones(ctx) {
|
|
32
|
+
const report = { reapplied: [], failed: [] };
|
|
33
|
+
const recorded = await readRecordedSparseCone(ctx.contentBranchesPath);
|
|
34
|
+
if (!recorded)
|
|
35
|
+
return report;
|
|
36
|
+
let entries;
|
|
37
|
+
try {
|
|
38
|
+
entries = await fs.readdir(ctx.contentBranchesPath, { withFileTypes: true });
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
if (isNodeError(err) && err.code === 'ENOENT')
|
|
42
|
+
return report;
|
|
43
|
+
throw err;
|
|
44
|
+
}
|
|
45
|
+
const dirNames = entries
|
|
46
|
+
.filter((entry) => entry.isDirectory() && !entry.name.startsWith('.'))
|
|
47
|
+
.map((entry) => entry.name)
|
|
48
|
+
.sort();
|
|
49
|
+
for (const dirName of dirNames) {
|
|
50
|
+
const branchPath = path.join(ctx.contentBranchesPath, dirName);
|
|
51
|
+
// Without its own .git, git would answer for a repository above the branches root.
|
|
52
|
+
const isClone = await fs.stat(path.join(branchPath, '.git')).then((stat) => stat.isDirectory(), () => false);
|
|
53
|
+
if (!isClone)
|
|
54
|
+
continue;
|
|
55
|
+
const git = simpleGit({ baseDir: branchPath });
|
|
56
|
+
const cone = await currentCone(git);
|
|
57
|
+
if (cone === null || sameCone(cone, recorded.cone))
|
|
58
|
+
continue;
|
|
59
|
+
try {
|
|
60
|
+
const outcome = await reapplyOne(ctx.contentBranchesPath, dirName, git, recorded.cone);
|
|
61
|
+
if (outcome === 'applied') {
|
|
62
|
+
workerLog(` ${dirName}: sparse-checkout cone ${[...cone].sort().join(',')} -> ` +
|
|
63
|
+
(recorded.cone ? [...recorded.cone].sort().join(',') : 'full checkout'));
|
|
64
|
+
report.reapplied.push(dirName);
|
|
65
|
+
}
|
|
66
|
+
else if (typeof outcome === 'number') {
|
|
67
|
+
workerLog(` ${dirName}: sparse-checkout cone waits for ${outcome} unpublished ` +
|
|
68
|
+
`deletion${outcome === 1 ? '' : 's'} to be published`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
const error = getErrorMessage(err);
|
|
73
|
+
workerLogWarn(` ${dirName}: could not change the sparse-checkout cone: ${error}`);
|
|
74
|
+
report.failed.push({ branch: dirName, error });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return report;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Apply `target` under both locks: `'applied'`, `'skipped'` when the clone is busy or no longer
|
|
81
|
+
* differs, or the count of its unpublished deletions, which hold the cone where it is.
|
|
82
|
+
*/
|
|
83
|
+
async function reapplyOne(contentBranchesPath, dirName, git, target) {
|
|
84
|
+
const branchPath = path.join(contentBranchesPath, dirName);
|
|
85
|
+
const hold = await holdProvisionedWorkspace(contentBranchesPath, dirName);
|
|
86
|
+
if (hold.kind !== 'held')
|
|
87
|
+
return 'skipped';
|
|
88
|
+
try {
|
|
89
|
+
let releaseContent;
|
|
90
|
+
let contentLost = false;
|
|
91
|
+
try {
|
|
92
|
+
releaseContent = await tryAcquireContentWriteLock(branchPath, () => {
|
|
93
|
+
contentLost = true;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
if (isNodeError(err) && err.code === 'ELOCKED')
|
|
98
|
+
return 'skipped';
|
|
99
|
+
throw err;
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
if (await isRebaseInProgress(branchPath))
|
|
103
|
+
return 'skipped';
|
|
104
|
+
const cone = await currentCone(git);
|
|
105
|
+
if (cone === null || sameCone(cone, target))
|
|
106
|
+
return 'skipped';
|
|
107
|
+
// A tracked file missing from the working tree is the one edit a cone change loses: leaving
|
|
108
|
+
// the cone marks it skip-worktree, so the deletion drops out of `status` and `add -A`, and a
|
|
109
|
+
// wider cone checks the file out again. Modified, untracked and staged changes all survive.
|
|
110
|
+
const deleted = (await git.raw(['ls-files', '--deleted', '-z'])).split('\0').filter(Boolean);
|
|
111
|
+
if (deleted.length > 0)
|
|
112
|
+
return deleted.length;
|
|
113
|
+
if (hold.isCompromised() || contentLost)
|
|
114
|
+
return 'skipped';
|
|
115
|
+
try {
|
|
116
|
+
await git.raw(target
|
|
117
|
+
? ['sparse-checkout', 'set', '--cone', '--', ...target]
|
|
118
|
+
: ['sparse-checkout', 'disable']);
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
await invalidateBranchContentCaches(branchPath);
|
|
122
|
+
}
|
|
123
|
+
return 'applied';
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
await releaseContent().catch(() => { });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
await releaseProvisionedWorkspace(hold.release, dirName);
|
|
131
|
+
}
|
|
132
|
+
}
|