@ebay-dt/bonfire-node 0.2.0-alpha.0 → 0.2.0-alpha.11
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 +18 -5
- package/dist/deployment-provider.d.ts +2 -1
- package/dist/deployment-provider.d.ts.map +1 -1
- package/dist/deployment-provider.js +2 -1
- package/dist/deployment-provider.js.map +1 -1
- package/dist/duplicate.d.ts.map +1 -1
- package/dist/duplicate.js +103 -47
- package/dist/duplicate.js.map +1 -1
- package/dist/git-repository.d.ts +16 -1
- package/dist/git-repository.d.ts.map +1 -1
- package/dist/git-repository.js +127 -1
- package/dist/git-repository.js.map +1 -1
- package/dist/github-cli.d.ts.map +1 -1
- package/dist/github-cli.js +3 -1
- package/dist/github-cli.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/prototype-templates.d.ts +0 -1
- package/dist/internal/prototype-templates.d.ts.map +1 -1
- package/dist/internal/prototype-templates.js +0 -5
- package/dist/internal/prototype-templates.js.map +1 -1
- package/dist/lifecycle/deployment-worktree.d.ts +36 -0
- package/dist/lifecycle/deployment-worktree.d.ts.map +1 -0
- package/dist/lifecycle/deployment-worktree.js +503 -0
- package/dist/lifecycle/deployment-worktree.js.map +1 -0
- package/dist/lifecycle/local-prototype-lifecycle.d.ts +15 -1
- package/dist/lifecycle/local-prototype-lifecycle.d.ts.map +1 -1
- package/dist/lifecycle/local-prototype-lifecycle.js +738 -98
- package/dist/lifecycle/local-prototype-lifecycle.js.map +1 -1
- package/dist/lifecycle/preparation-marker.d.ts +12 -0
- package/dist/lifecycle/preparation-marker.d.ts.map +1 -0
- package/dist/lifecycle/preparation-marker.js +76 -0
- package/dist/lifecycle/preparation-marker.js.map +1 -0
- package/dist/lifecycle/prototype-path-policy.d.ts +18 -0
- package/dist/lifecycle/prototype-path-policy.d.ts.map +1 -0
- package/dist/lifecycle/prototype-path-policy.js +429 -0
- package/dist/lifecycle/prototype-path-policy.js.map +1 -0
- package/dist/lifecycle/repository-summary.d.ts +7 -1
- package/dist/lifecycle/repository-summary.d.ts.map +1 -1
- package/dist/lifecycle/repository-summary.js +20 -4
- package/dist/lifecycle/repository-summary.js.map +1 -1
- package/dist/lifecycle/repository-synchronization.d.ts +13 -8
- package/dist/lifecycle/repository-synchronization.d.ts.map +1 -1
- package/dist/lifecycle/repository-synchronization.js +680 -139
- package/dist/lifecycle/repository-synchronization.js.map +1 -1
- package/dist/lifecycle/runtime-context.d.ts +2 -0
- package/dist/lifecycle/runtime-context.d.ts.map +1 -1
- package/dist/lifecycle/runtime-context.js +5 -1
- package/dist/lifecycle/runtime-context.js.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"repository-summary.js","sourceRoot":"","sources":["../../src/lifecycle/repository-summary.ts"],"names":[],"mappings":"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","sourcesContent":["import { relative } from \"node:path\";\nimport {\n getGitManagementCapabilities,\n parseRepositoryRelativePath,\n type BonfireIdentity,\n type GitMode,\n type PrototypeRef,\n type PrototypeSummary,\n type RepositoryAttentionCode,\n type RepositoryChange,\n type RepositoryChangeScope,\n type RepositoryChangeStatus,\n type RepositorySummary,\n} from \"@ebay-dt/bonfire-core\";\nimport type { PrototypeCatalogLike } from \"../prototype-catalog.js\";\nimport {\n type GitRepository,\n type GitStatusFile,\n} from \"../git-repository.js\";\nimport { hasUnresolvedRepositoryOperation } from \"./repository-synchronization.js\";\n\nexport type RepositorySummaryOptions = {\n repository: GitRepository;\n catalog: PrototypeCatalogLike;\n repositoryRoot: string;\n prototypeRoot: string;\n identity: BonfireIdentity;\n mode: GitMode;\n remote: string;\n primaryBranch: string;\n /** Fetch remote-tracking refs before comparing managed update state. */\n refresh?: boolean;\n};\n\ntype SummaryPathContext = {\n scope: RepositoryChangeScope;\n prototype: PrototypeRef | undefined;\n};\n\nfunction normalizePath(value: string) {\n return value.replaceAll(\"\\\\\", \"/\").replace(/^\\.\\//u, \"\");\n}\n\nfunction pathPrefix(repositoryRoot: string, prototypeRoot: string) {\n const value = normalizePath(relative(repositoryRoot, prototypeRoot));\n return value === \".\" ? \"\" : value.replace(/\\/+$/u, \"\");\n}\n\nfunction summaryKey(ref: PrototypeRef) {\n return `${ref.login}/${ref.slug}`;\n}\n\nfunction contextForPath(\n value: string,\n prefix: string,\n summaries: ReadonlyMap<string, PrototypeSummary>,\n identity: BonfireIdentity,\n): SummaryPathContext {\n const path = normalizePath(value);\n const rest = prefix === \"\" ? path : path.startsWith(`${prefix}/`) ? path.slice(prefix.length + 1) : null;\n if (rest === null) return { scope: \"out-of-scope\", prototype: undefined };\n\n const segments = rest.split(\"/\");\n const login = segments[0];\n const slug = segments[1];\n if (login === undefined || slug === undefined) {\n return { scope: \"unknown\", prototype: undefined };\n }\n\n const ref = { login, slug } satisfies PrototypeRef;\n const summary = summaries.get(summaryKey(ref));\n if (summary === undefined) return { scope: \"unknown\", prototype: undefined };\n\n return {\n scope: login === identity.login ? \"owned-prototype\" : \"other-prototype\",\n prototype: ref,\n };\n}\n\nfunction safePath(value: string) {\n try {\n return parseRepositoryRelativePath(normalizePath(value));\n } catch {\n return null;\n }\n}\n\nfunction addChange(\n changes: Map<string, RepositoryChange>,\n path: string,\n status: RepositoryChangeStatus,\n context: SummaryPathContext,\n) {\n const safe = safePath(path);\n if (safe === null) return;\n const key = `${status}:${safe}`;\n changes.set(key, {\n path: safe,\n status,\n scope: context.scope,\n ...(context.prototype === undefined ? {} : { prototype: context.prototype }),\n });\n}\n\nfunction statusPaths(file: GitStatusFile) {\n return [file.path, ...(file.from === undefined ? [] : [file.from])];\n}\n\nasync function diffPaths(repository: GitRepository, range: string) {\n const output = await repository.raw([\"diff\", \"--name-only\", \"-z\", range]);\n return output.split(\"\\0\").filter((path) => path.length > 0);\n}\n\nasync function refRelation(repository: GitRepository, left: string, right: string) {\n const output = await repository.raw([\n \"rev-list\",\n \"--left-right\",\n \"--count\",\n `${left}...${right}`,\n ]);\n const values = output.trim().split(/\\s+/u).map(Number);\n if (\n values.length !== 2 ||\n values.some((value) => !Number.isSafeInteger(value) || value < 0)\n ) {\n throw new Error(\"Malformed Git ref relation\");\n }\n return { ahead: values[0]!, behind: values[1]! };\n}\n\nfunction addDiffChanges(\n changes: Map<string, RepositoryChange>,\n paths: readonly string[],\n status: RepositoryChangeStatus,\n prefix: string,\n summaries: ReadonlyMap<string, PrototypeSummary>,\n identity: BonfireIdentity,\n) {\n for (const path of paths) {\n addChange(\n changes,\n path,\n status,\n contextForPath(path, prefix, summaries, identity),\n );\n }\n}\n\nexport async function getRepositorySummary(\n options: RepositorySummaryOptions,\n): Promise<RepositorySummary> {\n const summaries = new Map(\n (await options.catalog.scan()).map((summary) => [\n summaryKey(summary.ref),\n summary,\n ]),\n );\n const prefix = pathPrefix(options.repositoryRoot, options.prototypeRoot);\n const changes = new Map<string, RepositoryChange>();\n const attention = new Set<RepositoryAttentionCode>();\n const status = await options.repository.status();\n const currentBranch = await options.repository.currentBranch();\n const unresolved = await hasUnresolvedRepositoryOperation(options.repository);\n const detached = currentBranch === \"HEAD\" || currentBranch.length === 0;\n const wrongBranch = !detached && currentBranch !== options.identity.login;\n\n for (const file of status.files) {\n const fileStatus =\n file.index.includes(\"U\") || file.workingDirectory.includes(\"U\")\n ? \"unresolved\"\n : \"uncommitted\";\n for (const path of statusPaths(file)) {\n addChange(\n changes,\n path,\n fileStatus,\n contextForPath(path, prefix, summaries, options.identity),\n );\n }\n }\n\n if (status.files.length > 0) attention.add(\"dirty-worktree\");\n if (unresolved) attention.add(\"unresolved-operation\");\n if (detached) attention.add(\"detached-head\");\n if (wrongBranch) attention.add(\"wrong-branch\");\n\n const remotes = await options.repository.remotes();\n const hasRemote = remotes.some((remote) => remote.name === options.remote);\n let fetchFailed = false;\n if (options.refresh && options.mode === \"managed\" && hasRemote) {\n try {\n await options.repository.fetch(options.remote);\n } catch {\n fetchFailed = true;\n attention.add(\"fetch-failed\");\n }\n }\n const branches = await options.repository.branches();\n const localIdentity = branches.all.includes(options.identity.login);\n const remoteIdentity = `remotes/${options.remote}/${options.identity.login}`;\n const remotePrimary = `remotes/${options.remote}/${options.primaryBranch}`;\n const hasRemoteIdentity = hasRemote && branches.all.includes(remoteIdentity);\n const hasRemotePrimary = hasRemote && branches.all.includes(remotePrimary);\n let updateCheck: RepositorySummary[\"updateCheck\"] =\n options.mode === \"manual\" ? \"not-checked\" : \"unavailable\";\n\n if (options.mode === \"managed\") {\n if (fetchFailed) {\n updateCheck = \"attention\";\n } else if (!hasRemote) {\n updateCheck = \"unavailable\";\n } else if (!hasRemotePrimary || !localIdentity) {\n updateCheck = \"attention\";\n attention.add(\"missing-primary\");\n } else {\n try {\n const primaryRelation = await refRelation(\n options.repository,\n options.identity.login,\n remotePrimary,\n );\n updateCheck =\n primaryRelation.behind > 0 ? \"updates-available\" : \"up-to-date\";\n if (primaryRelation.behind > 0) {\n addDiffChanges(\n changes,\n await diffPaths(\n options.repository,\n `${options.identity.login}..${remotePrimary}`,\n ),\n \"updates-available\",\n prefix,\n summaries,\n options.identity,\n );\n }\n } catch {\n updateCheck = \"attention\";\n attention.add(\"unknown\");\n }\n }\n\n if (hasRemoteIdentity && localIdentity) {\n try {\n const identityRelation = await refRelation(\n options.repository,\n options.identity.login,\n remoteIdentity,\n );\n if (identityRelation.ahead > 0) {\n addDiffChanges(\n changes,\n await diffPaths(\n options.repository,\n `${remoteIdentity}..${options.identity.login}`,\n ),\n \"committed-unpushed\",\n prefix,\n summaries,\n options.identity,\n );\n }\n } catch {\n attention.add(\"unknown\");\n }\n }\n }\n\n let readiness: RepositorySummary[\"readiness\"] = \"ready\";\n if (detached || wrongBranch || unresolved) {\n readiness = \"blocked\";\n } else if (!status.clean) {\n readiness = \"attention\";\n }\n return {\n mode: options.mode,\n readiness,\n updateCheck,\n changes: [...changes.values()].sort((left, right) =>\n left.path < right.path ? -1 : left.path > right.path ? 1 : 0,\n ),\n attention: [...attention].sort(),\n capabilities: getGitManagementCapabilities(options.mode),\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"repository-summary.js","sourceRoot":"","sources":["../../src/lifecycle/repository-summary.ts"],"names":[],"mappings":"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","sourcesContent":["import { relative } from \"node:path\";\nimport {\n getGitManagementCapabilities,\n parseRepositoryRelativePath,\n type BonfireIdentity,\n type GitMode,\n type PrototypeRef,\n type PrototypeSummary,\n type RepositoryAttentionCode,\n type RepositoryChange,\n type RepositoryChangeScope,\n type RepositoryChangeStatus,\n type BranchPreparationAttention,\n type RepositorySummary,\n} from \"@ebay-dt/bonfire-core\";\nimport type { PrototypeCatalogLike } from \"../prototype-catalog.js\";\nimport { type GitRepository, type GitStatusFile } from \"../git-repository.js\";\nimport { hasUnresolvedRepositoryOperation } from \"./repository-synchronization.js\";\n\nexport type RepositorySummaryOptions = {\n repository: GitRepository;\n catalog: PrototypeCatalogLike;\n repositoryRoot: string;\n prototypeRoot: string;\n identity: BonfireIdentity;\n mode: GitMode;\n remote: string;\n primaryBranch: string;\n /** True when the running server must restart to load updated dependencies. */\n restartRequired?: boolean;\n /** Fetch remote-tracking refs before comparing managed update state. */\n refresh?: boolean;\n /** Latest non-ready preparation attempt retained across clean reads. */\n preparationAttention?: BranchPreparationAttention;\n /** Protected dependency-installation marker, if present. */\n preparationPending?: boolean;\n};\n\ntype SummaryPathContext = {\n scope: RepositoryChangeScope;\n prototype: PrototypeRef | undefined;\n};\n\nfunction normalizePath(value: string) {\n return value.replaceAll(\"\\\\\", \"/\").replace(/^\\.\\//u, \"\");\n}\n\nfunction pathPrefix(repositoryRoot: string, prototypeRoot: string) {\n const value = normalizePath(relative(repositoryRoot, prototypeRoot));\n return value === \".\" ? \"\" : value.replace(/\\/+$/u, \"\");\n}\n\nfunction summaryKey(ref: PrototypeRef) {\n return `${ref.login}/${ref.slug}`;\n}\n\nfunction contextForPath(\n value: string,\n prefix: string,\n summaries: ReadonlyMap<string, PrototypeSummary>,\n identity: BonfireIdentity,\n): SummaryPathContext {\n const path = normalizePath(value);\n const rest =\n prefix === \"\"\n ? path\n : path.startsWith(`${prefix}/`)\n ? path.slice(prefix.length + 1)\n : null;\n if (rest === null) return { scope: \"out-of-scope\", prototype: undefined };\n\n const segments = rest.split(\"/\");\n const login = segments[0];\n const slug = segments[1];\n if (login === undefined || slug === undefined) {\n return { scope: \"unknown\", prototype: undefined };\n }\n\n const ref = { login, slug } satisfies PrototypeRef;\n const summary = summaries.get(summaryKey(ref));\n if (summary === undefined) return { scope: \"unknown\", prototype: undefined };\n\n return {\n scope: login === identity.login ? \"owned-prototype\" : \"other-prototype\",\n prototype: ref,\n };\n}\n\nfunction safePath(value: string) {\n try {\n return parseRepositoryRelativePath(normalizePath(value));\n } catch {\n return null;\n }\n}\n\nfunction addChange(\n changes: Map<string, RepositoryChange>,\n path: string,\n status: RepositoryChangeStatus,\n context: SummaryPathContext,\n) {\n const safe = safePath(path);\n if (safe === null) return;\n const key = `${status}:${safe}`;\n changes.set(key, {\n path: safe,\n status,\n scope: context.scope,\n ...(context.prototype === undefined\n ? {}\n : { prototype: context.prototype }),\n });\n}\n\nfunction statusPaths(file: GitStatusFile) {\n return [file.path, ...(file.from === undefined ? [] : [file.from])];\n}\n\nasync function diffPaths(repository: GitRepository, range: string) {\n const output = await repository.raw([\"diff\", \"--name-only\", \"-z\", range]);\n return output.split(\"\\0\").filter((path) => path.length > 0);\n}\n\nasync function refRelation(\n repository: GitRepository,\n left: string,\n right: string,\n) {\n const output = await repository.raw([\n \"rev-list\",\n \"--left-right\",\n \"--count\",\n `${left}...${right}`,\n ]);\n const values = output.trim().split(/\\s+/u).map(Number);\n if (\n values.length !== 2 ||\n values.some((value) => !Number.isSafeInteger(value) || value < 0)\n ) {\n throw new Error(\"Malformed Git ref relation\");\n }\n return { ahead: values[0]!, behind: values[1]! };\n}\n\nfunction addDiffChanges(\n changes: Map<string, RepositoryChange>,\n paths: readonly string[],\n status: RepositoryChangeStatus,\n prefix: string,\n summaries: ReadonlyMap<string, PrototypeSummary>,\n identity: BonfireIdentity,\n) {\n for (const path of paths) {\n addChange(\n changes,\n path,\n status,\n contextForPath(path, prefix, summaries, identity),\n );\n }\n}\n\nexport async function getRepositorySummary(\n options: RepositorySummaryOptions,\n): Promise<RepositorySummary> {\n const summaries = new Map(\n (await options.catalog.scan()).map((summary) => [\n summaryKey(summary.ref),\n summary,\n ]),\n );\n const prefix = pathPrefix(options.repositoryRoot, options.prototypeRoot);\n const changes = new Map<string, RepositoryChange>();\n const attention = new Set<RepositoryAttentionCode>();\n const status = await options.repository.status();\n const currentBranch = await options.repository.currentBranch();\n const unresolved = await hasUnresolvedRepositoryOperation(options.repository);\n const detached = currentBranch === \"HEAD\" || currentBranch.length === 0;\n const wrongBranch = !detached && currentBranch !== options.identity.login;\n\n for (const file of status.files) {\n const fileStatus =\n file.index.includes(\"U\") || file.workingDirectory.includes(\"U\")\n ? \"unresolved\"\n : \"uncommitted\";\n for (const path of statusPaths(file)) {\n addChange(\n changes,\n path,\n fileStatus,\n contextForPath(path, prefix, summaries, options.identity),\n );\n }\n }\n\n const dirtyWorktree = status.files.length > 0;\n if (unresolved) attention.add(\"unresolved-operation\");\n if (detached) attention.add(\"detached-head\");\n if (wrongBranch) attention.add(\"wrong-branch\");\n\n const remotes = await options.repository.remotes();\n const hasRemote = remotes.some((remote) => remote.name === options.remote);\n let fetchFailed = false;\n if (options.refresh && options.mode === \"managed\" && hasRemote) {\n try {\n await options.repository.fetch(options.remote);\n } catch {\n fetchFailed = true;\n attention.add(\"fetch-failed\");\n }\n }\n const branches = await options.repository.branches();\n const localIdentity = branches.all.includes(options.identity.login);\n const remoteIdentity = `remotes/${options.remote}/${options.identity.login}`;\n const remotePrimary = `remotes/${options.remote}/${options.primaryBranch}`;\n const hasRemoteIdentity = hasRemote && branches.all.includes(remoteIdentity);\n const hasRemotePrimary = hasRemote && branches.all.includes(remotePrimary);\n let updateCheck: RepositorySummary[\"updateCheck\"] =\n options.mode === \"manual\" ? \"not-checked\" : \"unavailable\";\n\n if (options.mode === \"managed\") {\n if (fetchFailed) {\n updateCheck = \"attention\";\n } else if (!hasRemote) {\n updateCheck = \"unavailable\";\n } else if (!hasRemotePrimary || !localIdentity) {\n updateCheck = \"attention\";\n attention.add(\"missing-primary\");\n } else {\n try {\n const primaryRelation = await refRelation(\n options.repository,\n options.identity.login,\n remotePrimary,\n );\n updateCheck =\n primaryRelation.behind > 0 ? \"updates-available\" : \"up-to-date\";\n if (primaryRelation.behind > 0) {\n if (dirtyWorktree) attention.add(\"dirty-worktree\");\n addDiffChanges(\n changes,\n await diffPaths(\n options.repository,\n `${options.identity.login}..${remotePrimary}`,\n ),\n \"updates-available\",\n prefix,\n summaries,\n options.identity,\n );\n }\n } catch {\n updateCheck = \"attention\";\n attention.add(\"unknown\");\n }\n }\n\n if (hasRemoteIdentity && localIdentity) {\n try {\n const identityRelation = await refRelation(\n options.repository,\n options.identity.login,\n remoteIdentity,\n );\n if (identityRelation.ahead > 0) {\n addDiffChanges(\n changes,\n await diffPaths(\n options.repository,\n `${remoteIdentity}..${options.identity.login}`,\n ),\n \"committed-unpushed\",\n prefix,\n summaries,\n options.identity,\n );\n }\n } catch {\n attention.add(\"unknown\");\n }\n }\n }\n\n let readiness: RepositorySummary[\"readiness\"] = \"ready\";\n if (detached || wrongBranch || unresolved) {\n readiness = \"blocked\";\n } else if (!status.clean) {\n readiness = \"attention\";\n }\n return {\n mode: options.mode,\n ...(options.preparationAttention === undefined\n ? {}\n : { preparationAttention: options.preparationAttention }),\n ...(options.preparationPending === undefined\n ? {}\n : { preparationPending: options.preparationPending }),\n ...(options.restartRequired === undefined\n ? {}\n : { restartRequired: options.restartRequired }),\n readiness,\n updateCheck,\n changes: [...changes.values()].sort((left, right) =>\n left.path < right.path ? -1 : left.path > right.path ? 1 : 0,\n ),\n attention: [...attention].sort(),\n capabilities: getGitManagementCapabilities(options.mode),\n };\n}\n"]}
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { type GitRepository } from "../git-repository.js";
|
|
2
|
-
export type SynchronizationDeferredReason = "dirty-worktree" | "fetch-failed" | "merge-conflict";
|
|
3
|
-
export type SynchronizationUnsafeReason = "detached-head" | "unresolved-operation" | "missing-primary" | "identity-divergence" | "failed-abort" | "merge-failed" | "push-failed";
|
|
2
|
+
export type SynchronizationDeferredReason = "dirty-worktree" | "fetch-failed" | "merge-conflict" | "protected-collision";
|
|
3
|
+
export type SynchronizationUnsafeReason = "detached-head" | "branch-changed" | "unresolved-operation" | "missing-primary" | "identity-divergence" | "failed-abort" | "merge-failed" | "push-failed" | "reconciliation-failed";
|
|
4
4
|
type SynchronizationBase = {
|
|
5
5
|
branch: string | null;
|
|
6
6
|
changed: boolean;
|
|
7
|
-
|
|
7
|
+
/** Always false: update reconciliation never publishes the identity branch. */
|
|
8
|
+
pushed: false;
|
|
8
9
|
affectedPaths: readonly string[];
|
|
10
|
+
platformPaths: readonly string[];
|
|
11
|
+
dependencyPaths: readonly string[];
|
|
12
|
+
restartRequired: boolean;
|
|
13
|
+
preparationRequired: boolean;
|
|
9
14
|
};
|
|
10
15
|
export type RepositorySynchronizationResult = (SynchronizationBase & {
|
|
11
16
|
status: "synchronized";
|
|
@@ -20,15 +25,15 @@ export type SynchronizeIdentityBranchOptions = {
|
|
|
20
25
|
identityBranch: string;
|
|
21
26
|
primaryBranch: string;
|
|
22
27
|
remote: string;
|
|
28
|
+
/** Repository-relative configured prototype root. */
|
|
29
|
+
prototypeRoot: string;
|
|
23
30
|
};
|
|
24
31
|
export declare function hasUnresolvedRepositoryOperation(repository: GitRepository): Promise<boolean>;
|
|
25
32
|
export declare function assertIdentityBranchReady(repository: GitRepository, identityBranch: string): Promise<void>;
|
|
26
33
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* unsafe repository state so callers can keep authentication/read-only flows
|
|
31
|
-
* available.
|
|
34
|
+
* Reconcile only the committed platform baseline. Tree composition uses
|
|
35
|
+
* private indexes; the guarded Git index/worktree patch then updates the
|
|
36
|
+
* active index and platform worktree while leaving the namespace layer alone.
|
|
32
37
|
*/
|
|
33
38
|
export declare function synchronizeIdentityBranch(repository: GitRepository, options: SynchronizeIdentityBranchOptions): Promise<RepositorySynchronizationResult>;
|
|
34
39
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"repository-synchronization.d.ts","sourceRoot":"","sources":["../../src/lifecycle/repository-synchronization.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAE9B,MAAM,MAAM,6BAA6B,GACrC,gBAAgB,GAChB,cAAc,GACd,gBAAgB,CAAC;AAErB,MAAM,MAAM,2BAA2B,GACnC,eAAe,GACf,sBAAsB,GACtB,iBAAiB,GACjB,qBAAqB,GACrB,cAAc,GACd,cAAc,GACd,aAAa,CAAC;AAElB,KAAK,mBAAmB,GAAG;IACzB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC,CAAC;AAEF,MAAM,MAAM,+BAA+B,GACvC,CAAC,mBAAmB,GAAG;IACrB,MAAM,EAAE,cAAc,CAAC;CACxB,CAAC,GACF,CAAC,mBAAmB,GAAG;IACrB,MAAM,EAAE,UAAU,CAAC;IACnB,MAAM,EAAE,6BAA6B,CAAC;CACvC,CAAC,GACF,CAAC,mBAAmB,GAAG;IACrB,MAAM,EAAE,QAAQ,CAAC;IACjB,MAAM,EAAE,2BAA2B,CAAC;CACrC,CAAC,CAAC;AAEP,MAAM,MAAM,gCAAgC,GAAG;IAC7C,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAgDF,wBAAsB,gCAAgC,CACpD,UAAU,EAAE,aAAa,oBAO1B;AAkFD,wBAAsB,yBAAyB,CAC7C,UAAU,EAAE,aAAa,EACzB,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,IAAI,CAAC,CAgBf;AAED;;;;;;GAMG;AACH,wBAAsB,yBAAyB,CAC7C,UAAU,EAAE,aAAa,EACzB,OAAO,EAAE,gCAAgC,GACxC,OAAO,CAAC,+BAA+B,CAAC,CAgL1C","sourcesContent":["import { lstat } from \"node:fs/promises\";\nimport { isAbsolute, resolve } from \"node:path\";\nimport {\n GitRepositoryError,\n type GitRepository,\n} from \"../git-repository.js\";\n\nexport type SynchronizationDeferredReason =\n | \"dirty-worktree\"\n | \"fetch-failed\"\n | \"merge-conflict\";\n\nexport type SynchronizationUnsafeReason =\n | \"detached-head\"\n | \"unresolved-operation\"\n | \"missing-primary\"\n | \"identity-divergence\"\n | \"failed-abort\"\n | \"merge-failed\"\n | \"push-failed\";\n\ntype SynchronizationBase = {\n branch: string | null;\n changed: boolean;\n pushed: boolean;\n affectedPaths: readonly string[];\n};\n\nexport type RepositorySynchronizationResult =\n | (SynchronizationBase & {\n status: \"synchronized\";\n })\n | (SynchronizationBase & {\n status: \"deferred\";\n reason: SynchronizationDeferredReason;\n })\n | (SynchronizationBase & {\n status: \"unsafe\";\n reason: SynchronizationUnsafeReason;\n });\n\nexport type SynchronizeIdentityBranchOptions = {\n identityBranch: string;\n primaryBranch: string;\n remote: string;\n};\n\nconst OPERATION_MARKERS = [\n \"MERGE_HEAD\",\n \"CHERRY_PICK_HEAD\",\n \"REVERT_HEAD\",\n \"rebase-merge\",\n \"rebase-apply\",\n] as const;\n\nfunction assertBranch(value: string, option: string) {\n if (\n value.length === 0 ||\n value.startsWith(\"-\") ||\n value.includes(\"\\0\") ||\n /\\s/u.test(value) ||\n value.includes(\"@{\") ||\n value.split(/[\\\\/]/u).some((segment) => segment === \"..\")\n ) {\n throw new GitRepositoryError(\"invalid-configuration\", option);\n }\n}\n\nfunction remoteRef(remote: string, branch: string) {\n return `remotes/${remote}/${branch}`;\n}\n\nfunction branchRef(branch: string) {\n return `refs/heads/${branch}`;\n}\n\nasync function markerExists(repository: GitRepository, marker: string) {\n const markerPath = (await repository.raw([\"rev-parse\", \"--git-path\", marker])).trim();\n if (markerPath.length === 0) return false;\n const absolutePath = isAbsolute(markerPath)\n ? markerPath\n : resolve(repository.repositoryRoot, markerPath);\n try {\n await lstat(absolutePath);\n return true;\n } catch (error) {\n if (error && typeof error === \"object\" && \"code\" in error && error.code === \"ENOENT\") {\n return false;\n }\n throw error;\n }\n}\n\nexport async function hasUnresolvedRepositoryOperation(\n repository: GitRepository,\n) {\n for (const marker of OPERATION_MARKERS) {\n if (await markerExists(repository, marker)) return true;\n }\n const status = await repository.status();\n return hasUnmergedFiles(status.files);\n}\n\nfunction hasUnmergedFiles(files: readonly { index: string; workingDirectory: string }[]) {\n return files.some(\n (file) => file.index.includes(\"U\") || file.workingDirectory.includes(\"U\"),\n );\n}\n\nasync function refCommit(repository: GitRepository, ref: string) {\n const value = await repository.raw([\"rev-parse\", \"--verify\", `${ref}^{commit}`]);\n const commit = value.trim();\n if (!/^[0-9a-f]{40}$/iu.test(commit)) {\n throw new GitRepositoryError(\"malformed-output\", \"ref-commit\");\n }\n return commit;\n}\n\nasync function compareRefs(\n repository: GitRepository,\n left: string,\n right: string,\n) {\n const output = await repository.raw([\n \"rev-list\",\n \"--left-right\",\n \"--count\",\n `${left}...${right}`,\n ]);\n const values = output.trim().split(/\\s+/u).map(Number);\n if (\n values.length !== 2 ||\n values.some((value) => !Number.isSafeInteger(value) || value < 0)\n ) {\n throw new GitRepositoryError(\"malformed-output\", \"ref-relation\");\n }\n return { ahead: values[0]!, behind: values[1]! };\n}\n\nasync function safelyAdvanceLocalPrimary(\n repository: GitRepository,\n primaryBranch: string,\n primaryRemoteRef: string,\n localPrimaryCommit: string,\n) {\n await repository.raw([\n \"update-ref\",\n branchRef(primaryBranch),\n primaryRemoteRef,\n localPrimaryCommit,\n ]);\n}\n\nfunction deferred(\n branch: string | null,\n reason: SynchronizationDeferredReason,\n affectedPaths: readonly string[] = [],\n): RepositorySynchronizationResult {\n return {\n status: \"deferred\",\n branch,\n changed: false,\n pushed: false,\n affectedPaths,\n reason,\n };\n}\n\nfunction unsafe(\n branch: string | null,\n reason: SynchronizationUnsafeReason,\n options: Partial<SynchronizationBase> = {},\n): RepositorySynchronizationResult {\n return {\n status: \"unsafe\",\n branch,\n changed: options.changed ?? false,\n pushed: options.pushed ?? false,\n affectedPaths: options.affectedPaths ?? [],\n reason,\n };\n}\n\nexport async function assertIdentityBranchReady(\n repository: GitRepository,\n identityBranch: string,\n): Promise<void> {\n assertBranch(identityBranch, \"identity-branch\");\n const currentBranch = await repository.currentBranch();\n if (currentBranch === \"HEAD\" || currentBranch.length === 0) {\n throw new GitRepositoryError(\"conflict\", \"current-branch\");\n }\n if (currentBranch !== identityBranch) {\n throw new GitRepositoryError(\"conflict\", \"identity-branch\");\n }\n if (await hasUnresolvedRepositoryOperation(repository)) {\n throw new GitRepositoryError(\"conflict\", \"unresolved-operation\");\n }\n const status = await repository.status();\n if (hasUnmergedFiles(status.files)) {\n throw new GitRepositoryError(\"conflict\", \"unresolved-operation\");\n }\n}\n\n/**\n * Synchronize an identity branch from primary without checking out local\n * primary. Remote-tracking refs are the source of truth whenever a configured\n * remote is available. The result distinguishes non-blocking attention from\n * unsafe repository state so callers can keep authentication/read-only flows\n * available.\n */\nexport async function synchronizeIdentityBranch(\n repository: GitRepository,\n options: SynchronizeIdentityBranchOptions,\n): Promise<RepositorySynchronizationResult> {\n assertBranch(options.identityBranch, \"identity-branch\");\n assertBranch(options.primaryBranch, \"primary-branch\");\n assertBranch(options.remote, \"remote\");\n\n if (options.identityBranch === options.primaryBranch) {\n throw new GitRepositoryError(\"invalid-configuration\", \"identity-primary-branch\");\n }\n\n const currentBranch = await repository.currentBranch();\n if (currentBranch === \"HEAD\" || currentBranch.length === 0) {\n return unsafe(null, \"detached-head\");\n }\n\n if (await hasUnresolvedRepositoryOperation(repository)) {\n return unsafe(currentBranch, \"unresolved-operation\");\n }\n\n const status = await repository.status();\n if (hasUnmergedFiles(status.files)) {\n return unsafe(currentBranch, \"unresolved-operation\", {\n affectedPaths: status.files.map((file) => file.path),\n });\n }\n if (!status.clean) {\n return deferred(\n currentBranch,\n \"dirty-worktree\",\n status.files.map((file) => file.path),\n );\n }\n\n const configuredRemote = (await repository.remotes()).some(\n (remote) => remote.name === options.remote,\n );\n if (configuredRemote) {\n try {\n await repository.fetch(options.remote);\n } catch (error) {\n if (\n error instanceof GitRepositoryError &&\n error.kind === \"invalid-configuration\"\n ) {\n throw error;\n }\n return deferred(currentBranch, \"fetch-failed\");\n }\n }\n\n const branches = await repository.branches();\n const localPrimary = branches.all.includes(options.primaryBranch);\n const primaryRemote = remoteRef(options.remote, options.primaryBranch);\n const remotePrimary = configuredRemote && branches.all.includes(primaryRemote);\n if (configuredRemote ? !remotePrimary : !localPrimary) {\n return unsafe(currentBranch, \"missing-primary\");\n }\n\n const localIdentity = branches.all.includes(options.identityBranch);\n const identityRemote = remoteRef(options.remote, options.identityBranch);\n const remoteIdentity =\n configuredRemote && branches.all.includes(identityRemote);\n\n if (localIdentity && remoteIdentity) {\n const relation = await compareRefs(\n repository,\n options.identityBranch,\n identityRemote,\n );\n if (relation.ahead > 0 && relation.behind > 0) {\n return unsafe(currentBranch, \"identity-divergence\");\n }\n }\n\n let primaryAdvance:\n | { localCommit: string; remoteRef: string }\n | undefined;\n if (localPrimary && remotePrimary) {\n const relation = await compareRefs(\n repository,\n options.primaryBranch,\n primaryRemote,\n );\n if (relation.ahead === 0 && relation.behind > 0) {\n primaryAdvance = {\n localCommit: await refCommit(repository, options.primaryBranch),\n remoteRef: primaryRemote,\n };\n }\n }\n\n const sourceRef = remotePrimary ? primaryRemote : options.primaryBranch;\n if (!localIdentity) {\n if (remoteIdentity) {\n await repository.checkout([\n \"-b\",\n options.identityBranch,\n \"--track\",\n `${options.remote}/${options.identityBranch}`,\n ]);\n } else {\n await repository.checkout([\"-b\", options.identityBranch, sourceRef]);\n }\n } else if (currentBranch !== options.identityBranch) {\n await repository.checkout([options.identityBranch]);\n }\n\n if (primaryAdvance && currentBranch !== options.primaryBranch) {\n await safelyAdvanceLocalPrimary(\n repository,\n options.primaryBranch,\n primaryAdvance.remoteRef,\n primaryAdvance.localCommit,\n );\n }\n\n if (localIdentity && remoteIdentity) {\n const relation = await compareRefs(\n repository,\n options.identityBranch,\n identityRemote,\n );\n if (relation.ahead === 0 && relation.behind > 0) {\n await repository.merge([identityRemote, \"--ff-only\"]);\n }\n }\n\n const beforeMerge = await refCommit(repository, options.identityBranch);\n try {\n await repository.merge([sourceRef, \"--no-edit\"]);\n } catch (error) {\n if (!(error instanceof GitRepositoryError) || error.kind !== \"conflict\") {\n return unsafe(options.identityBranch, \"merge-failed\");\n }\n let affectedPaths: readonly string[] = [];\n try {\n affectedPaths = (await repository.status()).files.map((file) => file.path);\n } catch {\n // Preserve the conflict classification even if status cannot be read.\n }\n try {\n await repository.raw([\"merge\", \"--abort\"]);\n } catch {\n return unsafe(options.identityBranch, \"failed-abort\", {\n affectedPaths,\n });\n }\n return deferred(options.identityBranch, \"merge-conflict\", affectedPaths);\n }\n\n const afterMerge = await refCommit(repository, options.identityBranch);\n const changed = beforeMerge !== afterMerge;\n if (!changed || !configuredRemote) {\n return {\n status: \"synchronized\",\n branch: options.identityBranch,\n changed,\n pushed: false,\n affectedPaths: [],\n };\n }\n\n try {\n await repository.push(options.remote, options.identityBranch, [\n \"--set-upstream\",\n ]);\n } catch {\n return unsafe(options.identityBranch, \"push-failed\", { changed });\n }\n\n return {\n status: \"synchronized\",\n branch: options.identityBranch,\n changed: true,\n pushed: true,\n affectedPaths: [],\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"repository-synchronization.d.ts","sourceRoot":"","sources":["../../src/lifecycle/repository-synchronization.ts"],"names":[],"mappings":"AAGA,OAAO,EAAsB,KAAK,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAM9E,MAAM,MAAM,6BAA6B,GACvC,gBAAgB,GAAG,cAAc,GAAG,gBAAgB,GAAG,qBAAqB,CAAC;AAE/E,MAAM,MAAM,2BAA2B,GACnC,eAAe,GACf,gBAAgB,GAChB,sBAAsB,GACtB,iBAAiB,GACjB,qBAAqB,GACrB,cAAc,GACd,cAAc,GACd,aAAa,GACb,uBAAuB,CAAC;AAE5B,KAAK,mBAAmB,GAAG;IACzB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,OAAO,EAAE,OAAO,CAAC;IACjB,+EAA+E;IAC/E,MAAM,EAAE,KAAK,CAAC;IACd,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,eAAe,EAAE,OAAO,CAAC;IACzB,mBAAmB,EAAE,OAAO,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,+BAA+B,GACvC,CAAC,mBAAmB,GAAG;IAAE,MAAM,EAAE,cAAc,CAAA;CAAE,CAAC,GAClD,CAAC,mBAAmB,GAAG;IACrB,MAAM,EAAE,UAAU,CAAC;IACnB,MAAM,EAAE,6BAA6B,CAAC;CACvC,CAAC,GACF,CAAC,mBAAmB,GAAG;IACrB,MAAM,EAAE,QAAQ,CAAC;IACjB,MAAM,EAAE,2BAA2B,CAAC;CACrC,CAAC,CAAC;AAEP,MAAM,MAAM,gCAAgC,GAAG;IAC7C,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,qDAAqD;IACrD,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAmHF,wBAAsB,gCAAgC,CACpD,UAAU,EAAE,aAAa,oBAI1B;AAoZD,wBAAsB,yBAAyB,CAC7C,UAAU,EAAE,aAAa,EACzB,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,IAAI,CAAC,CAOf;AAED;;;;GAIG;AACH,wBAAsB,yBAAyB,CAC7C,UAAU,EAAE,aAAa,EACzB,OAAO,EAAE,gCAAgC,GACxC,OAAO,CAAC,+BAA+B,CAAC,CAihB1C","sourcesContent":["import { lstat, mkdir, mkdtemp, open, rm, writeFile } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { dirname, isAbsolute, join, relative, resolve } from \"node:path\";\nimport { GitRepositoryError, type GitRepository } from \"../git-repository.js\";\nimport {\n isProtectedPrototypePath,\n isRegularPrototypeMode,\n} from \"./prototype-path-policy.js\";\n\nexport type SynchronizationDeferredReason =\n \"dirty-worktree\" | \"fetch-failed\" | \"merge-conflict\" | \"protected-collision\";\n\nexport type SynchronizationUnsafeReason =\n | \"detached-head\"\n | \"branch-changed\"\n | \"unresolved-operation\"\n | \"missing-primary\"\n | \"identity-divergence\"\n | \"failed-abort\"\n | \"merge-failed\"\n | \"push-failed\"\n | \"reconciliation-failed\";\n\ntype SynchronizationBase = {\n branch: string | null;\n changed: boolean;\n /** Always false: update reconciliation never publishes the identity branch. */\n pushed: false;\n affectedPaths: readonly string[];\n platformPaths: readonly string[];\n dependencyPaths: readonly string[];\n restartRequired: boolean;\n preparationRequired: boolean;\n};\n\nexport type RepositorySynchronizationResult =\n | (SynchronizationBase & { status: \"synchronized\" })\n | (SynchronizationBase & {\n status: \"deferred\";\n reason: SynchronizationDeferredReason;\n })\n | (SynchronizationBase & {\n status: \"unsafe\";\n reason: SynchronizationUnsafeReason;\n });\n\nexport type SynchronizeIdentityBranchOptions = {\n identityBranch: string;\n primaryBranch: string;\n remote: string;\n /** Repository-relative configured prototype root. */\n prototypeRoot: string;\n};\n\nconst OPERATION_MARKERS = [\n \"MERGE_HEAD\",\n \"CHERRY_PICK_HEAD\",\n \"REVERT_HEAD\",\n \"rebase-merge\",\n \"rebase-apply\",\n] as const;\n\nconst DEPENDENCY_NAMES = new Set([\n \"package.json\",\n \"pnpm-lock.yaml\",\n \"pnpm-lock.yml\",\n \"pnpm-workspace.yaml\",\n \"package-lock.json\",\n \"npm-shrinkwrap.json\",\n \"yarn.lock\",\n \"bun.lock\",\n \"bun.lockb\",\n]);\n\nfunction assertBranch(value: string, option: string) {\n if (\n value.length === 0 ||\n value.startsWith(\"-\") ||\n value.includes(\"\\0\") ||\n /\\s/u.test(value) ||\n value.includes(\"@{\") ||\n value.split(/[\\\\/]/u).some((segment) => segment === \"..\")\n ) {\n throw new GitRepositoryError(\"invalid-configuration\", option);\n }\n}\n\nfunction normalize(path: string) {\n return path.replaceAll(\"\\\\\", \"/\").replace(/^\\.\\//u, \"\");\n}\n\nfunction namespaceFor(repository: GitRepository, root: string, login: string) {\n if (\n root.length === 0 ||\n root.includes(\"\\0\") ||\n root.split(/[\\\\/]/u).some((segment) => segment === \"..\")\n ) {\n throw new GitRepositoryError(\"invalid-configuration\", \"prototype-root\");\n }\n assertBranch(login, \"identity-login\");\n const absolute = isAbsolute(root)\n ? resolve(root, login)\n : resolve(repository.repositoryRoot, root, login);\n const relativePath = normalize(relative(repository.repositoryRoot, absolute));\n if (\n relativePath.length === 0 ||\n relativePath === \"..\" ||\n relativePath.startsWith(\"../\")\n ) {\n throw new GitRepositoryError(\"invalid-configuration\", \"prototype-root\");\n }\n return relativePath;\n}\n\nfunction remoteRef(remote: string, branch: string) {\n return `remotes/${remote}/${branch}`;\n}\nfunction branchRef(branch: string) {\n return `refs/heads/${branch}`;\n}\n\nasync function markerPathExists(repository: GitRepository, markerPath: string) {\n if (markerPath.length === 0) return false;\n const absolutePath = isAbsolute(markerPath)\n ? markerPath\n : resolve(repository.repositoryRoot, markerPath);\n try {\n await lstat(absolutePath);\n return true;\n } catch (error) {\n if (\n error &&\n typeof error === \"object\" &&\n \"code\" in error &&\n error.code === \"ENOENT\"\n )\n return false;\n throw error;\n }\n}\n\n/** Resolve every marker path in one Git process; each is still checked on disk. */\nasync function anyMarkerExists(\n repository: GitRepository,\n markers: readonly string[],\n) {\n const markerPaths = (\n await repository.raw([\n \"rev-parse\",\n ...markers.flatMap((marker) => [\"--git-path\", marker]),\n ])\n )\n .split(\"\\n\")\n .map((markerPath) => markerPath.trim());\n for (const markerPath of markerPaths)\n if (await markerPathExists(repository, markerPath)) return true;\n return false;\n}\n\nfunction hasUnmergedFiles(\n files: readonly { index: string; workingDirectory: string }[],\n) {\n return files.some(\n (file) => file.index.includes(\"U\") || file.workingDirectory.includes(\"U\"),\n );\n}\n\nexport async function hasUnresolvedRepositoryOperation(\n repository: GitRepository,\n) {\n if (await anyMarkerExists(repository, OPERATION_MARKERS)) return true;\n return hasUnmergedFiles((await repository.status()).files);\n}\n\nfunction assertNoUnresolved(status: {\n files: readonly { index: string; workingDirectory: string }[];\n}) {\n if (hasUnmergedFiles(status.files))\n throw new GitRepositoryError(\"conflict\", \"unresolved-operation\");\n}\n\nasync function refCommit(repository: GitRepository, ref: string) {\n const value = (\n await repository.raw([\"rev-parse\", \"--verify\", `${ref}^{commit}`])\n ).trim();\n if (!/^[0-9a-f]{40}$/iu.test(value))\n throw new GitRepositoryError(\"malformed-output\", \"ref-commit\");\n return value;\n}\n\n/** Hold Git's own ref lock so another Git process cannot move a checkout target. */\nasync function withLockedBranch(\n repository: GitRepository,\n branch: string,\n action: () => Promise<void>,\n) {\n const gitPath = (\n await repository.raw([\n \"rev-parse\",\n \"--git-path\",\n `${branchRef(branch)}.lock`,\n ])\n ).trim();\n if (gitPath.length === 0)\n throw new GitRepositoryError(\"malformed-output\", \"branch-lock\");\n const lockPath = resolve(repository.repositoryRoot, gitPath);\n await mkdir(dirname(lockPath), { recursive: true });\n // Exclusive creation also respects a lock already held by an external writer.\n const lock = await open(lockPath, \"wx\");\n try {\n await action();\n } finally {\n await lock.close();\n await rm(lockPath);\n }\n}\n\nasync function compareRefs(\n repository: GitRepository,\n left: string,\n right: string,\n) {\n const values = (\n await repository.raw([\n \"rev-list\",\n \"--left-right\",\n \"--count\",\n `${left}...${right}`,\n ])\n )\n .trim()\n .split(/\\s+/u)\n .map(Number);\n if (\n values.length !== 2 ||\n values.some((value) => !Number.isSafeInteger(value) || value < 0)\n ) {\n throw new GitRepositoryError(\"malformed-output\", \"ref-relation\");\n }\n return { ahead: values[0]!, behind: values[1]! };\n}\n\nfunction pathsFromStatus(files: readonly { path: string; from?: string }[]) {\n return [\n ...new Set(\n files.flatMap((file) => [\n normalize(file.path),\n ...(file.from ? [normalize(file.from)] : []),\n ]),\n ),\n ];\n}\n\nfunction isWithin(path: string, parent: string) {\n return path === parent || path.startsWith(`${parent}/`);\n}\n\nfunction dependencyPath(path: string) {\n const name = path.slice(path.lastIndexOf(\"/\") + 1);\n return DEPENDENCY_NAMES.has(name);\n}\n\nfunction resultBase(\n branch: string | null,\n values: Partial<Omit<SynchronizationBase, \"branch\" | \"pushed\">> = {},\n): SynchronizationBase {\n return {\n branch,\n changed: values.changed ?? false,\n pushed: false,\n affectedPaths: values.affectedPaths ?? [],\n platformPaths: values.platformPaths ?? [],\n dependencyPaths: values.dependencyPaths ?? [],\n restartRequired: values.restartRequired ?? false,\n preparationRequired: values.preparationRequired ?? false,\n };\n}\n\nfunction deferred(\n branch: string | null,\n reason: SynchronizationDeferredReason,\n values: Partial<Omit<SynchronizationBase, \"branch\" | \"pushed\">> = {},\n): RepositorySynchronizationResult {\n return { ...resultBase(branch, values), status: \"deferred\", reason };\n}\nfunction unsafe(\n branch: string | null,\n reason: SynchronizationUnsafeReason,\n values: Partial<Omit<SynchronizationBase, \"branch\" | \"pushed\">> = {},\n): RepositorySynchronizationResult {\n return { ...resultBase(branch, values), status: \"unsafe\", reason };\n}\n\nfunction parseTree(output: string) {\n return output\n .split(\"\\0\")\n .filter(Boolean)\n .flatMap((entry) => {\n const tab = entry.indexOf(\"\\t\");\n if (tab < 0) return [];\n const header = entry.slice(0, tab).split(/\\s+/u);\n const mode = header[0];\n const object = header[2];\n const path = normalize(entry.slice(tab + 1));\n return mode && object && path ? [{ mode, object, path }] : [];\n });\n}\n\nasync function treeEntries(\n repository: GitRepository,\n tree: string,\n namespace?: string,\n) {\n const args = [\"ls-tree\", \"-r\", \"-z\", tree];\n if (namespace) args.push(\"--\", namespace);\n return parseTree(await repository.raw(args));\n}\n\ntype IndexEntry = {\n mode: string;\n object: string;\n stage: string;\n path: string;\n};\n\nfunction parseIndex(output: string): IndexEntry[] {\n return output\n .split(\"\\0\")\n .filter(Boolean)\n .flatMap((entry) => {\n const tab = entry.indexOf(\"\\t\");\n if (tab < 0) return [];\n const header = entry.slice(0, tab).split(/\\s+/u);\n const [mode, object, stage] = header;\n const path = normalize(entry.slice(tab + 1));\n return mode && object && stage && path\n ? [{ mode, object, stage, path }]\n : [];\n });\n}\n\nasync function rawWithIndex(\n repository: GitRepository,\n indexFile: string,\n args: readonly string[],\n) {\n if (!repository.rawWithEnv)\n throw new GitRepositoryError(\"invalid-configuration\", \"private-index\");\n return repository.rawWithEnv(args, { GIT_INDEX_FILE: indexFile });\n}\n\nasync function privateTree(\n repository: GitRepository,\n baseTree: string,\n namespace: string,\n replacement: readonly IndexEntry[],\n) {\n const directory = await mkdtemp(join(tmpdir(), \"bonfire-index-\"));\n const indexFile = join(directory, \"index\");\n try {\n await rawWithIndex(repository, indexFile, [\n \"read-tree\",\n \"--reset\",\n baseTree,\n ]);\n const loaded = parseIndex(\n await rawWithIndex(repository, indexFile, [\n \"ls-files\",\n \"-s\",\n \"-z\",\n \"--\",\n namespace,\n ]),\n );\n for (const entry of loaded) {\n await rawWithIndex(repository, indexFile, [\n \"update-index\",\n \"--force-remove\",\n \"--\",\n entry.path,\n ]);\n }\n for (const entry of replacement) {\n if (entry.stage !== \"0\")\n throw new GitRepositoryError(\"conflict\", \"unmerged-index\");\n await rawWithIndex(repository, indexFile, [\n \"update-index\",\n \"--add\",\n \"--cacheinfo\",\n `${entry.mode},${entry.object},${entry.path}`,\n ]);\n }\n return (await rawWithIndex(repository, indexFile, [\"write-tree\"])).trim();\n } finally {\n await rm(directory, { recursive: true, force: true });\n }\n}\n\n/** Replace the complete namespace, rather than only paths found in HEAD. */\nasync function overlayIndexNamespace(\n repository: GitRepository,\n baseTree: string,\n namespace: string,\n replacement: readonly IndexEntry[],\n) {\n return privateTree(repository, baseTree, namespace, replacement);\n}\n\nasync function indexNamespace(repository: GitRepository, namespace: string) {\n return parseIndex(\n await repository.raw([\"ls-files\", \"-s\", \"-z\", \"--\", namespace]),\n );\n}\n\nasync function composeTree(\n repository: GitRepository,\n primary: string,\n ownTree: string,\n namespace: string,\n) {\n // The replacement is loaded from the committed identity tree. In\n // particular, removing the complete namespace of `primary` prevents\n // primary-only paths from leaking into the designer-owned baseline.\n return overlayIndexNamespace(\n repository,\n primary,\n namespace,\n (await treeEntries(repository, ownTree, namespace)).map((entry) => ({\n ...entry,\n stage: \"0\",\n })),\n );\n}\n\nasync function treeChanges(\n repository: GitRepository,\n from: string,\n to: string,\n namespace?: string,\n) {\n const output = await repository.raw([\n \"diff\",\n \"--name-only\",\n \"-z\",\n `${from}..${to}`,\n \"--\",\n \".\",\n ]);\n return output\n .split(\"\\0\")\n .filter(Boolean)\n .map(normalize)\n .filter((path) => namespace === undefined || !isWithin(path, namespace));\n}\n\nasync function platformChanges(\n repository: GitRepository,\n head: string,\n primary: string,\n namespace: string,\n) {\n return treeChanges(repository, head, primary, namespace);\n}\n\nasync function filesystemPlatformCollision(\n repository: GitRepository,\n head: string,\n primary: string,\n paths: readonly string[],\n) {\n const headEntries = new Map(\n (await treeEntries(repository, head)).map((entry) => [entry.path, entry]),\n );\n const primaryEntries = new Map(\n (await treeEntries(repository, primary)).map((entry) => [\n entry.path,\n entry,\n ]),\n );\n const collisions: string[] = [];\n for (const path of paths) {\n const segments = path.split(\"/\");\n for (let index = 1; index < segments.length; index += 1) {\n const ancestor = segments.slice(0, index).join(\"/\");\n try {\n const stats = await lstat(\n resolve(repository.repositoryRoot, ...ancestor.split(\"/\")),\n );\n if (stats.isSymbolicLink() || !stats.isDirectory())\n collisions.push(path);\n } catch (error) {\n if (!(\n error &&\n typeof error === \"object\" &&\n \"code\" in error &&\n error.code === \"ENOENT\"\n ))\n collisions.push(path);\n }\n }\n const before = headEntries.get(path);\n const after = primaryEntries.get(path);\n const absolute = resolve(repository.repositoryRoot, ...path.split(\"/\"));\n let current;\n try {\n current = await lstat(absolute);\n } catch (error) {\n if (\n error &&\n typeof error === \"object\" &&\n \"code\" in error &&\n error.code === \"ENOENT\"\n )\n continue;\n collisions.push(path);\n continue;\n }\n // A path absent from HEAD is not necessarily safe merely because status\n // omits it: ignored files and symlink/special parents are real state.\n const trackedRegular =\n before !== undefined && isRegularPrototypeMode(before.mode);\n const desiredRegular =\n after !== undefined && isRegularPrototypeMode(after.mode);\n if (\n current.isSymbolicLink() ||\n (!current.isFile() && !current.isDirectory())\n ) {\n collisions.push(path);\n continue;\n }\n if (current.isDirectory()) {\n // A tracked file-to-directory replacement is only safe when the\n // destination path is absent; even an empty directory can make restore\n // fail midway, while a non-empty one may contain ignored state.\n if (desiredRegular) collisions.push(path);\n continue;\n }\n // A tracked regular file may be removed by the guarded tree transition;\n // an untracked/ignored file is never safe to overwrite or remove.\n if (!trackedRegular) collisions.push(path);\n }\n return [...new Set(collisions)];\n}\n\nasync function protectedPlatformCollision(\n repository: GitRepository,\n head: string,\n primary: string,\n namespace: string,\n paths: readonly string[],\n excludeNamespace = true,\n) {\n const headEntries = new Map(\n (await treeEntries(repository, head)).map((entry) => [entry.path, entry]),\n );\n const primaryEntries = new Map(\n (await treeEntries(repository, primary)).map((entry) => [\n entry.path,\n entry,\n ]),\n );\n return paths.filter((path) => {\n if (excludeNamespace && isWithin(path, namespace)) return false;\n const before = headEntries.get(path);\n const after = primaryEntries.get(path);\n const typeChangedToUnsafe =\n (before !== undefined && !isRegularPrototypeMode(before.mode)) ||\n (after !== undefined && !isRegularPrototypeMode(after.mode));\n return (\n typeChangedToUnsafe ||\n (isProtectedPrototypePath(path) &&\n (before?.mode !== after?.mode || before?.object !== after?.object))\n );\n });\n}\n\nexport async function assertIdentityBranchReady(\n repository: GitRepository,\n identityBranch: string,\n): Promise<void> {\n assertBranch(identityBranch, \"identity-branch\");\n const branch = await repository.currentBranch();\n if (branch === \"HEAD\" || branch.length === 0 || branch !== identityBranch)\n throw new GitRepositoryError(\"conflict\", \"identity-branch\");\n if (await hasUnresolvedRepositoryOperation(repository))\n throw new GitRepositoryError(\"conflict\", \"unresolved-operation\");\n}\n\n/**\n * Reconcile only the committed platform baseline. Tree composition uses\n * private indexes; the guarded Git index/worktree patch then updates the\n * active index and platform worktree while leaving the namespace layer alone.\n */\nexport async function synchronizeIdentityBranch(\n repository: GitRepository,\n options: SynchronizeIdentityBranchOptions,\n): Promise<RepositorySynchronizationResult> {\n assertBranch(options.identityBranch, \"identity-branch\");\n assertBranch(options.primaryBranch, \"primary-branch\");\n assertBranch(options.remote, \"remote\");\n if (options.identityBranch === options.primaryBranch)\n throw new GitRepositoryError(\n \"invalid-configuration\",\n \"identity-primary-branch\",\n );\n const namespace = namespaceFor(\n repository,\n options.prototypeRoot,\n options.identityBranch,\n );\n\n let currentBranch = await repository.currentBranch();\n if (currentBranch === \"HEAD\" || currentBranch.length === 0)\n return unsafe(null, \"detached-head\");\n const originalBranch = currentBranch;\n const originalHead = await refCommit(repository, \"HEAD\");\n if (await hasUnresolvedRepositoryOperation(repository))\n return unsafe(currentBranch, \"unresolved-operation\");\n\n const configuredRemote = (await repository.remotes()).some(\n (remote) => remote.name === options.remote,\n );\n if (configuredRemote) {\n try {\n await repository.fetch(options.remote);\n } catch (error) {\n if (\n error instanceof GitRepositoryError &&\n error.kind === \"invalid-configuration\"\n )\n throw error;\n return deferred(currentBranch, \"fetch-failed\");\n }\n }\n const afterFetchBranch = await repository.currentBranch();\n if (afterFetchBranch !== currentBranch)\n return unsafe(afterFetchBranch, \"branch-changed\");\n currentBranch = afterFetchBranch;\n if (await hasUnresolvedRepositoryOperation(repository))\n return unsafe(currentBranch, \"unresolved-operation\");\n\n const branches = await repository.branches();\n const localPrimary = branches.all.includes(options.primaryBranch);\n const primaryRemote = remoteRef(options.remote, options.primaryBranch);\n const remotePrimary =\n configuredRemote && branches.all.includes(primaryRemote);\n if (configuredRemote ? !remotePrimary : !localPrimary)\n return unsafe(currentBranch, \"missing-primary\");\n const sourceRef = remotePrimary ? primaryRemote : options.primaryBranch;\n // Pin the source before planning. Never compose from a mutable remote ref.\n const primarySha = await refCommit(repository, sourceRef);\n const localIdentity = branches.all.includes(options.identityBranch);\n const identityRemote = remoteRef(options.remote, options.identityBranch);\n const remoteIdentity =\n configuredRemote && branches.all.includes(identityRemote);\n // Match the legacy branch-preparation behavior: a remote-only identity is\n // based on its remote namespace, not silently recreated from primary.\n const identityBaseRef = localIdentity\n ? options.identityBranch\n : remoteIdentity\n ? identityRemote\n : sourceRef;\n if (localIdentity && remoteIdentity) {\n const relation = await compareRefs(\n repository,\n options.identityBranch,\n identityRemote,\n );\n if (relation.ahead > 0 && relation.behind > 0)\n return unsafe(currentBranch, \"identity-divergence\");\n }\n\n let status = await repository.status();\n assertNoUnresolved(status);\n const dirtyPaths = pathsFromStatus(status.files);\n const dirtyOutside = dirtyPaths.filter((path) => !isWithin(path, namespace));\n if (dirtyOutside.length > 0 || (!status.clean && dirtyPaths.length === 0)) {\n return deferred(currentBranch, \"dirty-worktree\", {\n affectedPaths: dirtyPaths,\n });\n }\n // A checkout is itself a tree transition. Even namespace-only dirt is not\n // safe to carry across it: checkout can replace ignored files that status\n // does not report. Defer before selecting/creating the identity branch.\n if (!localIdentity && dirtyPaths.length > 0) {\n return deferred(currentBranch, \"dirty-worktree\", {\n affectedPaths: dirtyPaths,\n });\n }\n const head = await refCommit(repository, \"HEAD\");\n const primaryPaths = await platformChanges(\n repository,\n head,\n primarySha,\n namespace,\n );\n const protectedPaths = await protectedPlatformCollision(\n repository,\n head,\n primarySha,\n namespace,\n primaryPaths,\n );\n const filesystemPaths = await filesystemPlatformCollision(\n repository,\n head,\n primarySha,\n primaryPaths,\n );\n if (protectedPaths.length > 0 || filesystemPaths.length > 0)\n return deferred(currentBranch, \"protected-collision\", {\n affectedPaths: [...new Set([...protectedPaths, ...filesystemPaths])],\n });\n\n const targetHead = await refCommit(repository, identityBaseRef);\n const checkoutPaths =\n targetHead === originalHead\n ? []\n : await treeChanges(repository, originalHead, targetHead);\n const checkoutPlatformPaths = checkoutPaths.filter(\n (path) => !isWithin(path, namespace),\n );\n const checkoutDependencyPaths = checkoutPlatformPaths.filter(dependencyPath);\n if (targetHead !== originalHead) {\n // This preflight includes the target identity namespace. In particular,\n // ignored files absent from the current tree must not be overwritten by\n // selecting an existing remote/local identity branch.\n const checkoutCollisions = await filesystemPlatformCollision(\n repository,\n originalHead,\n targetHead,\n checkoutPaths,\n );\n const checkoutProtected = await protectedPlatformCollision(\n repository,\n originalHead,\n targetHead,\n namespace,\n checkoutPaths,\n false,\n );\n if (checkoutCollisions.length > 0 || checkoutProtected.length > 0)\n return deferred(currentBranch, \"protected-collision\", {\n affectedPaths: [\n ...new Set([...checkoutCollisions, ...checkoutProtected]),\n ],\n });\n }\n\n if (currentBranch !== options.identityBranch && dirtyPaths.length > 0) {\n return deferred(currentBranch, \"dirty-worktree\", {\n affectedPaths: dirtyPaths,\n });\n }\n\n if (!localIdentity) {\n try {\n // Create from the exact preflighted commit, never a moving remote ref.\n // -b refuses a concurrently created local branch instead of replacing it.\n await repository.checkout([\n \"--no-overwrite-ignore\",\n \"--no-track\",\n \"-b\",\n options.identityBranch,\n targetHead,\n ]);\n currentBranch = options.identityBranch;\n if (remoteIdentity)\n await repository.raw([\n \"branch\",\n `--set-upstream-to=${identityRemote}`,\n options.identityBranch,\n ]);\n } catch {\n const observedBranch = await repository.currentBranch();\n return unsafe(observedBranch || currentBranch, \"reconciliation-failed\", {\n changed: observedBranch !== originalBranch,\n affectedPaths: checkoutPaths,\n platformPaths: checkoutPlatformPaths,\n dependencyPaths: checkoutDependencyPaths,\n restartRequired: checkoutPlatformPaths.length > 0,\n preparationRequired: checkoutDependencyPaths.length > 0,\n });\n }\n } else if (currentBranch !== options.identityBranch) {\n try {\n await withLockedBranch(repository, options.identityBranch, async () => {\n // Validation without the lock would leave another check/use race.\n // Existing-branch checkout does not write the branch ref, so Git's ref\n // lock can remain held throughout the checkout itself.\n if (\n (await refCommit(repository, branchRef(options.identityBranch))) !==\n targetHead ||\n (await repository.currentBranch()) !== originalBranch ||\n (await refCommit(repository, \"HEAD\")) !== originalHead ||\n !(await repository.status()).clean\n )\n throw new GitRepositoryError(\"conflict\", \"checkout-target-changed\");\n await repository.checkout([\n \"--no-overwrite-ignore\",\n options.identityBranch,\n ]);\n currentBranch = options.identityBranch;\n });\n } catch {\n const observedBranch = await repository.currentBranch();\n return unsafe(observedBranch || currentBranch, \"reconciliation-failed\", {\n changed: observedBranch !== originalBranch,\n affectedPaths: checkoutPaths,\n platformPaths: checkoutPlatformPaths,\n dependencyPaths: checkoutDependencyPaths,\n restartRequired: checkoutPlatformPaths.length > 0,\n preparationRequired: checkoutDependencyPaths.length > 0,\n });\n }\n }\n\n // Re-read after branch movement and before touching the index. This catches\n // an external command or a fetch hook that changed the checkout meanwhile.\n const branchBeforeMutation = await repository.currentBranch();\n const checkoutChanged = currentBranch !== originalBranch;\n const checkoutEffects = {\n changed: checkoutChanged,\n affectedPaths: checkoutPaths,\n platformPaths: checkoutPlatformPaths,\n dependencyPaths: checkoutDependencyPaths,\n restartRequired: checkoutPlatformPaths.length > 0,\n preparationRequired: checkoutDependencyPaths.length > 0,\n };\n if (\n branchBeforeMutation !== currentBranch ||\n branchBeforeMutation === \"HEAD\"\n ) {\n return unsafe(branchBeforeMutation || null, \"branch-changed\", {\n ...checkoutEffects,\n });\n }\n status = await repository.status();\n if (\n !status.clean &&\n pathsFromStatus(status.files).some((path) => !isWithin(path, namespace))\n ) {\n return deferred(currentBranch, \"dirty-worktree\", {\n ...checkoutEffects,\n affectedPaths: [\n ...new Set([...checkoutPaths, ...pathsFromStatus(status.files)]),\n ],\n });\n }\n if (await hasUnresolvedRepositoryOperation(repository))\n return unsafe(currentBranch, \"unresolved-operation\", checkoutEffects);\n const latestHead = await refCommit(repository, \"HEAD\");\n if (latestHead !== targetHead)\n return unsafe(currentBranch, \"reconciliation-failed\", {\n ...checkoutEffects,\n changed: true,\n });\n const latestPrimarySha = await refCommit(repository, sourceRef);\n const branchSelectionChanged =\n currentBranch !== originalBranch || latestHead !== originalHead;\n if (latestPrimarySha !== primarySha)\n return unsafe(currentBranch, \"reconciliation-failed\", {\n ...checkoutEffects,\n affectedPaths: [...new Set([...checkoutPaths, ...primaryPaths])],\n });\n const latestPrimaryPaths = await platformChanges(\n repository,\n latestHead,\n primarySha,\n namespace,\n );\n const latestProtected = await protectedPlatformCollision(\n repository,\n latestHead,\n primarySha,\n namespace,\n latestPrimaryPaths,\n );\n const latestFilesystem = await filesystemPlatformCollision(\n repository,\n latestHead,\n primarySha,\n latestPrimaryPaths,\n );\n if (latestProtected.length > 0 || latestFilesystem.length > 0)\n return deferred(currentBranch, \"protected-collision\", {\n ...checkoutEffects,\n affectedPaths: [\n ...new Set([...checkoutPaths, ...latestProtected, ...latestFilesystem]),\n ],\n });\n const plannedPlatformPaths = [\n ...new Set([...latestPrimaryPaths, ...checkoutPlatformPaths]),\n ];\n const plannedDependencyPaths = [\n ...new Set([\n ...checkoutDependencyPaths,\n ...latestPrimaryPaths.filter(dependencyPath),\n ]),\n ];\n if (latestPrimaryPaths.length === 0) {\n return {\n ...resultBase(currentBranch, {\n changed: branchSelectionChanged,\n affectedPaths: checkoutPaths,\n platformPaths: checkoutPlatformPaths,\n dependencyPaths: checkoutDependencyPaths,\n restartRequired: checkoutPlatformPaths.length > 0,\n preparationRequired: checkoutDependencyPaths.length > 0,\n }),\n status: \"synchronized\",\n };\n }\n\n const indexTree = (await repository.raw([\"write-tree\"])).trim();\n const stagedNamespace = await indexNamespace(repository, namespace);\n let baselineCommit: string;\n let desiredIndexTree: string | undefined;\n let patchDirectory: string | undefined;\n let patchFile: string | undefined;\n const dependencies = plannedDependencyPaths;\n try {\n // All object creation and tree composition happen in temporary indexes.\n // The live index is never used as a tree builder.\n const composedTree = await composeTree(\n repository,\n primarySha,\n latestHead,\n namespace,\n );\n baselineCommit = (\n await repository.raw([\n \"commit-tree\",\n composedTree,\n \"-p\",\n latestHead,\n \"-m\",\n \"chore: update committed platform baseline\",\n ])\n ).trim();\n if (!/^[0-9a-f]{40}$/iu.test(baselineCommit))\n throw new GitRepositoryError(\"malformed-output\", \"baseline-commit\");\n\n if (\n (await repository.currentBranch()) !== currentBranch ||\n (await refCommit(repository, \"HEAD\")) !== latestHead ||\n (await refCommit(repository, sourceRef)) !== primarySha ||\n (await repository.raw([\"write-tree\"])).trim() !== indexTree\n ) {\n throw new GitRepositoryError(\"conflict\", \"external-state\");\n }\n desiredIndexTree = await overlayIndexNamespace(\n repository,\n baselineCommit,\n namespace,\n stagedNamespace,\n );\n\n // Apply the tree delta through Git's guarded index/worktree primitive.\n // Unlike checkout/read-tree -u, git apply --index refuses to overwrite an\n // untracked or ignored path that appears between preflight and mutation.\n // The trees carry the same staged namespace, and the patch excludes that\n // namespace, so staged and unstaged prototype files are not touched.\n if ((await repository.raw([\"write-tree\"])).trim() !== indexTree)\n throw new GitRepositoryError(\"conflict\", \"external-index\");\n patchDirectory = await mkdtemp(join(tmpdir(), \"bonfire-patch-\"));\n patchFile = join(patchDirectory, \"platform.patch\");\n const patch = await repository.raw([\n \"diff\",\n \"--binary\",\n indexTree,\n desiredIndexTree,\n \"--\",\n \".\",\n `:(exclude)${namespace}`,\n ]);\n await writeFile(patchFile, patch);\n await repository.raw([\"apply\", \"--index\", patchFile]);\n\n if (\n (await repository.currentBranch()) !== currentBranch ||\n (await refCommit(repository, \"HEAD\")) !== latestHead ||\n (await refCommit(repository, sourceRef)) !== primarySha ||\n (await repository.raw([\"write-tree\"])).trim() !== desiredIndexTree\n )\n throw new GitRepositoryError(\"conflict\", \"external-state\");\n\n // Ref movement is last. The expected old value is a CAS guard against an\n // external branch writer.\n await repository.raw([\n \"update-ref\",\n branchRef(options.identityBranch),\n baselineCommit,\n latestHead,\n ]);\n const landedHead = await refCommit(repository, \"HEAD\");\n if (landedHead !== baselineCommit)\n throw new GitRepositoryError(\"conflict\", \"ref-transition\");\n if (\n (await repository.currentBranch()) !== currentBranch ||\n (await refCommit(repository, sourceRef)) !== primarySha ||\n (await repository.raw([\"write-tree\"])).trim() !== desiredIndexTree\n )\n throw new GitRepositoryError(\"conflict\", \"external-state\");\n const finalStatus = await repository.status();\n const unexpectedFinalPaths = pathsFromStatus(finalStatus.files).filter(\n (path) => !isWithin(path, namespace),\n );\n if (unexpectedFinalPaths.length > 0)\n throw new GitRepositoryError(\"conflict\", \"external-worktree\");\n } catch {\n // A command wrapper may throw after running a command. First try the\n // narrowly guarded inverse of our original patch. This is allowed only\n // while the original branch/ref/source and exact intended index/worktree\n // state still hold; git apply itself then protects intervening edits.\n let recovered = false;\n if (patchFile !== undefined && desiredIndexTree !== undefined) {\n try {\n const sameState =\n (await repository.currentBranch()) === currentBranch &&\n (await refCommit(repository, \"HEAD\")) === latestHead &&\n (await refCommit(repository, sourceRef)) === primarySha &&\n (await repository.raw([\"write-tree\"])).trim() === desiredIndexTree;\n if (sameState) {\n await repository.raw([\n \"diff\",\n \"--quiet\",\n desiredIndexTree,\n \"--\",\n \".\",\n `:(exclude)${namespace}`,\n ]);\n const observed = await repository.status();\n const unexpected = pathsFromStatus(observed.files).filter(\n (path) =>\n !isWithin(path, namespace) &&\n !latestPrimaryPaths.some(\n (expected) =>\n isWithin(path, expected) || isWithin(expected, path),\n ),\n );\n if (unexpected.length === 0) {\n await repository.raw([\"apply\", \"--reverse\", \"--index\", patchFile]);\n const restored = await repository.status();\n recovered =\n (await repository.raw([\"write-tree\"])).trim() === indexTree &&\n (await refCommit(repository, \"HEAD\")) === latestHead &&\n (await repository.currentBranch()) === currentBranch &&\n (await repository.raw([\n \"diff\",\n \"--quiet\",\n indexTree,\n \"--\",\n \".\",\n `:(exclude)${namespace}`,\n ])) === \"\" &&\n pathsFromStatus(restored.files).every((path) =>\n isWithin(path, namespace),\n );\n }\n }\n } catch {\n // Preserve the partial transition. Manual recovery must begin from\n // the reported old HEAD and inspect the exact platform paths before\n // applying the original patch in reverse; never reset or clean here.\n }\n }\n if (patchDirectory !== undefined)\n await rm(patchDirectory, { recursive: true, force: true }).catch(\n () => undefined,\n );\n\n let landed: boolean;\n let indexChanged = false;\n let worktreeChanged = false;\n try {\n landed = (await refCommit(repository, \"HEAD\")) !== latestHead;\n } catch {\n // A malformed/missing ref is itself an incomplete transition.\n landed = true;\n }\n if (desiredIndexTree !== undefined) {\n try {\n indexChanged =\n (await repository.raw([\"write-tree\"])).trim() !== indexTree;\n } catch {\n indexChanged = true;\n }\n // The guarded --index patch changes the index and worktree as one Git\n // operation. An index-tree change is therefore the reliable\n // observable for a transition that ran before an acknowledgement failed;\n // a pure pre-side-effect rejection leaves both layers unchanged.\n worktreeChanged = indexChanged;\n }\n const changed = recovered\n ? branchSelectionChanged\n : branchSelectionChanged || landed || indexChanged || worktreeChanged;\n return unsafe(currentBranch, \"reconciliation-failed\", {\n changed,\n affectedPaths: [...new Set([...checkoutPaths, ...latestPrimaryPaths])],\n dependencyPaths: dependencies,\n platformPaths: plannedPlatformPaths,\n restartRequired:\n checkoutPlatformPaths.length > 0 ||\n (!recovered && (worktreeChanged || landed)),\n preparationRequired:\n dependencies.length > 0 &&\n (checkoutDependencyPaths.length > 0 ||\n (!recovered && (landed || worktreeChanged))),\n });\n }\n\n if (patchDirectory !== undefined)\n await rm(patchDirectory, { recursive: true, force: true });\n return {\n ...resultBase(currentBranch, {\n changed: true,\n affectedPaths: [...new Set([...checkoutPaths, ...latestPrimaryPaths])],\n platformPaths: plannedPlatformPaths,\n dependencyPaths: dependencies,\n restartRequired: plannedPlatformPaths.length > 0,\n preparationRequired: dependencies.length > 0,\n }),\n status: \"synchronized\",\n };\n}\n"]}
|