@ebay-dt/bonfire-node 0.2.0-alpha.10 → 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 +1 -1
- package/dist/deployment-provider.d.ts.map +1 -1
- package/dist/deployment-provider.js +1 -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 +14 -1
- package/dist/git-repository.d.ts.map +1 -1
- package/dist/git-repository.js +99 -3
- package/dist/git-repository.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 +5 -4
- package/dist/lifecycle/deployment-worktree.d.ts.map +1 -1
- package/dist/lifecycle/deployment-worktree.js +269 -68
- package/dist/lifecycle/deployment-worktree.js.map +1 -1
- package/dist/lifecycle/local-prototype-lifecycle.d.ts +13 -1
- package/dist/lifecycle/local-prototype-lifecycle.d.ts.map +1 -1
- package/dist/lifecycle/local-prototype-lifecycle.js +634 -110
- 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 +5 -1
- package/dist/lifecycle/repository-summary.d.ts.map +1 -1
- package/dist/lifecycle/repository-summary.js +6 -0
- package/dist/lifecycle/repository-summary.js.map +1 -1
- package/dist/lifecycle/repository-synchronization.d.ts +13 -9
- package/dist/lifecycle/repository-synchronization.d.ts.map +1 -1
- package/dist/lifecycle/repository-synchronization.js +670 -173
- package/dist/lifecycle/repository-synchronization.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { lstat } from "node:fs/promises";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { lstat, mkdir, mkdtemp, open, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
|
+
import { GitRepositoryError } from "../git-repository.js";
|
|
5
|
+
import { isProtectedPrototypePath, isRegularPrototypeMode, } from "./prototype-path-policy.js";
|
|
4
6
|
const OPERATION_MARKERS = [
|
|
5
7
|
"MERGE_HEAD",
|
|
6
8
|
"CHERRY_PICK_HEAD",
|
|
@@ -8,6 +10,17 @@ const OPERATION_MARKERS = [
|
|
|
8
10
|
"rebase-merge",
|
|
9
11
|
"rebase-apply",
|
|
10
12
|
];
|
|
13
|
+
const DEPENDENCY_NAMES = new Set([
|
|
14
|
+
"package.json",
|
|
15
|
+
"pnpm-lock.yaml",
|
|
16
|
+
"pnpm-lock.yml",
|
|
17
|
+
"pnpm-workspace.yaml",
|
|
18
|
+
"package-lock.json",
|
|
19
|
+
"npm-shrinkwrap.json",
|
|
20
|
+
"yarn.lock",
|
|
21
|
+
"bun.lock",
|
|
22
|
+
"bun.lockb",
|
|
23
|
+
]);
|
|
11
24
|
function assertBranch(value, option) {
|
|
12
25
|
if (value.length === 0 ||
|
|
13
26
|
value.startsWith("-") ||
|
|
@@ -18,14 +31,34 @@ function assertBranch(value, option) {
|
|
|
18
31
|
throw new GitRepositoryError("invalid-configuration", option);
|
|
19
32
|
}
|
|
20
33
|
}
|
|
34
|
+
function normalize(path) {
|
|
35
|
+
return path.replaceAll("\\", "/").replace(/^\.\//u, "");
|
|
36
|
+
}
|
|
37
|
+
function namespaceFor(repository, root, login) {
|
|
38
|
+
if (root.length === 0 ||
|
|
39
|
+
root.includes("\0") ||
|
|
40
|
+
root.split(/[\\/]/u).some((segment) => segment === "..")) {
|
|
41
|
+
throw new GitRepositoryError("invalid-configuration", "prototype-root");
|
|
42
|
+
}
|
|
43
|
+
assertBranch(login, "identity-login");
|
|
44
|
+
const absolute = isAbsolute(root)
|
|
45
|
+
? resolve(root, login)
|
|
46
|
+
: resolve(repository.repositoryRoot, root, login);
|
|
47
|
+
const relativePath = normalize(relative(repository.repositoryRoot, absolute));
|
|
48
|
+
if (relativePath.length === 0 ||
|
|
49
|
+
relativePath === ".." ||
|
|
50
|
+
relativePath.startsWith("../")) {
|
|
51
|
+
throw new GitRepositoryError("invalid-configuration", "prototype-root");
|
|
52
|
+
}
|
|
53
|
+
return relativePath;
|
|
54
|
+
}
|
|
21
55
|
function remoteRef(remote, branch) {
|
|
22
56
|
return `remotes/${remote}/${branch}`;
|
|
23
57
|
}
|
|
24
58
|
function branchRef(branch) {
|
|
25
59
|
return `refs/heads/${branch}`;
|
|
26
60
|
}
|
|
27
|
-
async function
|
|
28
|
-
const markerPath = (await repository.raw(["rev-parse", "--git-path", marker])).trim();
|
|
61
|
+
async function markerPathExists(repository, markerPath) {
|
|
29
62
|
if (markerPath.length === 0)
|
|
30
63
|
return false;
|
|
31
64
|
const absolutePath = isAbsolute(markerPath)
|
|
@@ -39,121 +72,337 @@ async function markerExists(repository, marker) {
|
|
|
39
72
|
if (error &&
|
|
40
73
|
typeof error === "object" &&
|
|
41
74
|
"code" in error &&
|
|
42
|
-
error.code === "ENOENT")
|
|
75
|
+
error.code === "ENOENT")
|
|
43
76
|
return false;
|
|
44
|
-
}
|
|
45
77
|
throw error;
|
|
46
78
|
}
|
|
47
79
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
80
|
+
/** Resolve every marker path in one Git process; each is still checked on disk. */
|
|
81
|
+
async function anyMarkerExists(repository, markers) {
|
|
82
|
+
const markerPaths = (await repository.raw([
|
|
83
|
+
"rev-parse",
|
|
84
|
+
...markers.flatMap((marker) => ["--git-path", marker]),
|
|
85
|
+
]))
|
|
86
|
+
.split("\n")
|
|
87
|
+
.map((markerPath) => markerPath.trim());
|
|
88
|
+
for (const markerPath of markerPaths)
|
|
89
|
+
if (await markerPathExists(repository, markerPath))
|
|
51
90
|
return true;
|
|
52
|
-
|
|
53
|
-
const status = await repository.status();
|
|
54
|
-
return hasUnmergedFiles(status.files);
|
|
91
|
+
return false;
|
|
55
92
|
}
|
|
56
93
|
function hasUnmergedFiles(files) {
|
|
57
94
|
return files.some((file) => file.index.includes("U") || file.workingDirectory.includes("U"));
|
|
58
95
|
}
|
|
96
|
+
export async function hasUnresolvedRepositoryOperation(repository) {
|
|
97
|
+
if (await anyMarkerExists(repository, OPERATION_MARKERS))
|
|
98
|
+
return true;
|
|
99
|
+
return hasUnmergedFiles((await repository.status()).files);
|
|
100
|
+
}
|
|
101
|
+
function assertNoUnresolved(status) {
|
|
102
|
+
if (hasUnmergedFiles(status.files))
|
|
103
|
+
throw new GitRepositoryError("conflict", "unresolved-operation");
|
|
104
|
+
}
|
|
59
105
|
async function refCommit(repository, ref) {
|
|
60
|
-
const value = await repository.raw([
|
|
61
|
-
|
|
62
|
-
"--verify",
|
|
63
|
-
`${ref}^{commit}`,
|
|
64
|
-
]);
|
|
65
|
-
const commit = value.trim();
|
|
66
|
-
if (!/^[0-9a-f]{40}$/iu.test(commit)) {
|
|
106
|
+
const value = (await repository.raw(["rev-parse", "--verify", `${ref}^{commit}`])).trim();
|
|
107
|
+
if (!/^[0-9a-f]{40}$/iu.test(value))
|
|
67
108
|
throw new GitRepositoryError("malformed-output", "ref-commit");
|
|
109
|
+
return value;
|
|
110
|
+
}
|
|
111
|
+
/** Hold Git's own ref lock so another Git process cannot move a checkout target. */
|
|
112
|
+
async function withLockedBranch(repository, branch, action) {
|
|
113
|
+
const gitPath = (await repository.raw([
|
|
114
|
+
"rev-parse",
|
|
115
|
+
"--git-path",
|
|
116
|
+
`${branchRef(branch)}.lock`,
|
|
117
|
+
])).trim();
|
|
118
|
+
if (gitPath.length === 0)
|
|
119
|
+
throw new GitRepositoryError("malformed-output", "branch-lock");
|
|
120
|
+
const lockPath = resolve(repository.repositoryRoot, gitPath);
|
|
121
|
+
await mkdir(dirname(lockPath), { recursive: true });
|
|
122
|
+
// Exclusive creation also respects a lock already held by an external writer.
|
|
123
|
+
const lock = await open(lockPath, "wx");
|
|
124
|
+
try {
|
|
125
|
+
await action();
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
await lock.close();
|
|
129
|
+
await rm(lockPath);
|
|
68
130
|
}
|
|
69
|
-
return commit;
|
|
70
131
|
}
|
|
71
132
|
async function compareRefs(repository, left, right) {
|
|
72
|
-
const
|
|
133
|
+
const values = (await repository.raw([
|
|
73
134
|
"rev-list",
|
|
74
135
|
"--left-right",
|
|
75
136
|
"--count",
|
|
76
137
|
`${left}...${right}`,
|
|
77
|
-
])
|
|
78
|
-
|
|
138
|
+
]))
|
|
139
|
+
.trim()
|
|
140
|
+
.split(/\s+/u)
|
|
141
|
+
.map(Number);
|
|
79
142
|
if (values.length !== 2 ||
|
|
80
143
|
values.some((value) => !Number.isSafeInteger(value) || value < 0)) {
|
|
81
144
|
throw new GitRepositoryError("malformed-output", "ref-relation");
|
|
82
145
|
}
|
|
83
146
|
return { ahead: values[0], behind: values[1] };
|
|
84
147
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
]
|
|
148
|
+
function pathsFromStatus(files) {
|
|
149
|
+
return [
|
|
150
|
+
...new Set(files.flatMap((file) => [
|
|
151
|
+
normalize(file.path),
|
|
152
|
+
...(file.from ? [normalize(file.from)] : []),
|
|
153
|
+
])),
|
|
154
|
+
];
|
|
155
|
+
}
|
|
156
|
+
function isWithin(path, parent) {
|
|
157
|
+
return path === parent || path.startsWith(`${parent}/`);
|
|
158
|
+
}
|
|
159
|
+
function dependencyPath(path) {
|
|
160
|
+
const name = path.slice(path.lastIndexOf("/") + 1);
|
|
161
|
+
return DEPENDENCY_NAMES.has(name);
|
|
92
162
|
}
|
|
93
|
-
function
|
|
163
|
+
function resultBase(branch, values = {}) {
|
|
94
164
|
return {
|
|
95
|
-
status: "deferred",
|
|
96
165
|
branch,
|
|
97
|
-
changed: false,
|
|
166
|
+
changed: values.changed ?? false,
|
|
98
167
|
pushed: false,
|
|
99
|
-
affectedPaths,
|
|
100
|
-
|
|
168
|
+
affectedPaths: values.affectedPaths ?? [],
|
|
169
|
+
platformPaths: values.platformPaths ?? [],
|
|
170
|
+
dependencyPaths: values.dependencyPaths ?? [],
|
|
171
|
+
restartRequired: values.restartRequired ?? false,
|
|
172
|
+
preparationRequired: values.preparationRequired ?? false,
|
|
101
173
|
};
|
|
102
174
|
}
|
|
103
|
-
function
|
|
104
|
-
return {
|
|
105
|
-
status: "unsafe",
|
|
106
|
-
branch,
|
|
107
|
-
changed: options.changed ?? false,
|
|
108
|
-
pushed: options.pushed ?? false,
|
|
109
|
-
affectedPaths: options.affectedPaths ?? [],
|
|
110
|
-
reason,
|
|
111
|
-
};
|
|
175
|
+
function deferred(branch, reason, values = {}) {
|
|
176
|
+
return { ...resultBase(branch, values), status: "deferred", reason };
|
|
112
177
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
178
|
+
function unsafe(branch, reason, values = {}) {
|
|
179
|
+
return { ...resultBase(branch, values), status: "unsafe", reason };
|
|
180
|
+
}
|
|
181
|
+
function parseTree(output) {
|
|
182
|
+
return output
|
|
183
|
+
.split("\0")
|
|
184
|
+
.filter(Boolean)
|
|
185
|
+
.flatMap((entry) => {
|
|
186
|
+
const tab = entry.indexOf("\t");
|
|
187
|
+
if (tab < 0)
|
|
188
|
+
return [];
|
|
189
|
+
const header = entry.slice(0, tab).split(/\s+/u);
|
|
190
|
+
const mode = header[0];
|
|
191
|
+
const object = header[2];
|
|
192
|
+
const path = normalize(entry.slice(tab + 1));
|
|
193
|
+
return mode && object && path ? [{ mode, object, path }] : [];
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
async function treeEntries(repository, tree, namespace) {
|
|
197
|
+
const args = ["ls-tree", "-r", "-z", tree];
|
|
198
|
+
if (namespace)
|
|
199
|
+
args.push("--", namespace);
|
|
200
|
+
return parseTree(await repository.raw(args));
|
|
201
|
+
}
|
|
202
|
+
function parseIndex(output) {
|
|
203
|
+
return output
|
|
204
|
+
.split("\0")
|
|
205
|
+
.filter(Boolean)
|
|
206
|
+
.flatMap((entry) => {
|
|
207
|
+
const tab = entry.indexOf("\t");
|
|
208
|
+
if (tab < 0)
|
|
209
|
+
return [];
|
|
210
|
+
const header = entry.slice(0, tab).split(/\s+/u);
|
|
211
|
+
const [mode, object, stage] = header;
|
|
212
|
+
const path = normalize(entry.slice(tab + 1));
|
|
213
|
+
return mode && object && stage && path
|
|
214
|
+
? [{ mode, object, stage, path }]
|
|
215
|
+
: [];
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
async function rawWithIndex(repository, indexFile, args) {
|
|
219
|
+
if (!repository.rawWithEnv)
|
|
220
|
+
throw new GitRepositoryError("invalid-configuration", "private-index");
|
|
221
|
+
return repository.rawWithEnv(args, { GIT_INDEX_FILE: indexFile });
|
|
222
|
+
}
|
|
223
|
+
async function privateTree(repository, baseTree, namespace, replacement) {
|
|
224
|
+
const directory = await mkdtemp(join(tmpdir(), "bonfire-index-"));
|
|
225
|
+
const indexFile = join(directory, "index");
|
|
226
|
+
try {
|
|
227
|
+
await rawWithIndex(repository, indexFile, [
|
|
228
|
+
"read-tree",
|
|
229
|
+
"--reset",
|
|
230
|
+
baseTree,
|
|
231
|
+
]);
|
|
232
|
+
const loaded = parseIndex(await rawWithIndex(repository, indexFile, [
|
|
233
|
+
"ls-files",
|
|
234
|
+
"-s",
|
|
235
|
+
"-z",
|
|
236
|
+
"--",
|
|
237
|
+
namespace,
|
|
238
|
+
]));
|
|
239
|
+
for (const entry of loaded) {
|
|
240
|
+
await rawWithIndex(repository, indexFile, [
|
|
241
|
+
"update-index",
|
|
242
|
+
"--force-remove",
|
|
243
|
+
"--",
|
|
244
|
+
entry.path,
|
|
245
|
+
]);
|
|
246
|
+
}
|
|
247
|
+
for (const entry of replacement) {
|
|
248
|
+
if (entry.stage !== "0")
|
|
249
|
+
throw new GitRepositoryError("conflict", "unmerged-index");
|
|
250
|
+
await rawWithIndex(repository, indexFile, [
|
|
251
|
+
"update-index",
|
|
252
|
+
"--add",
|
|
253
|
+
"--cacheinfo",
|
|
254
|
+
`${entry.mode},${entry.object},${entry.path}`,
|
|
255
|
+
]);
|
|
256
|
+
}
|
|
257
|
+
return (await rawWithIndex(repository, indexFile, ["write-tree"])).trim();
|
|
118
258
|
}
|
|
119
|
-
|
|
120
|
-
|
|
259
|
+
finally {
|
|
260
|
+
await rm(directory, { recursive: true, force: true });
|
|
121
261
|
}
|
|
122
|
-
|
|
123
|
-
|
|
262
|
+
}
|
|
263
|
+
/** Replace the complete namespace, rather than only paths found in HEAD. */
|
|
264
|
+
async function overlayIndexNamespace(repository, baseTree, namespace, replacement) {
|
|
265
|
+
return privateTree(repository, baseTree, namespace, replacement);
|
|
266
|
+
}
|
|
267
|
+
async function indexNamespace(repository, namespace) {
|
|
268
|
+
return parseIndex(await repository.raw(["ls-files", "-s", "-z", "--", namespace]));
|
|
269
|
+
}
|
|
270
|
+
async function composeTree(repository, primary, ownTree, namespace) {
|
|
271
|
+
// The replacement is loaded from the committed identity tree. In
|
|
272
|
+
// particular, removing the complete namespace of `primary` prevents
|
|
273
|
+
// primary-only paths from leaking into the designer-owned baseline.
|
|
274
|
+
return overlayIndexNamespace(repository, primary, namespace, (await treeEntries(repository, ownTree, namespace)).map((entry) => ({
|
|
275
|
+
...entry,
|
|
276
|
+
stage: "0",
|
|
277
|
+
})));
|
|
278
|
+
}
|
|
279
|
+
async function treeChanges(repository, from, to, namespace) {
|
|
280
|
+
const output = await repository.raw([
|
|
281
|
+
"diff",
|
|
282
|
+
"--name-only",
|
|
283
|
+
"-z",
|
|
284
|
+
`${from}..${to}`,
|
|
285
|
+
"--",
|
|
286
|
+
".",
|
|
287
|
+
]);
|
|
288
|
+
return output
|
|
289
|
+
.split("\0")
|
|
290
|
+
.filter(Boolean)
|
|
291
|
+
.map(normalize)
|
|
292
|
+
.filter((path) => namespace === undefined || !isWithin(path, namespace));
|
|
293
|
+
}
|
|
294
|
+
async function platformChanges(repository, head, primary, namespace) {
|
|
295
|
+
return treeChanges(repository, head, primary, namespace);
|
|
296
|
+
}
|
|
297
|
+
async function filesystemPlatformCollision(repository, head, primary, paths) {
|
|
298
|
+
const headEntries = new Map((await treeEntries(repository, head)).map((entry) => [entry.path, entry]));
|
|
299
|
+
const primaryEntries = new Map((await treeEntries(repository, primary)).map((entry) => [
|
|
300
|
+
entry.path,
|
|
301
|
+
entry,
|
|
302
|
+
]));
|
|
303
|
+
const collisions = [];
|
|
304
|
+
for (const path of paths) {
|
|
305
|
+
const segments = path.split("/");
|
|
306
|
+
for (let index = 1; index < segments.length; index += 1) {
|
|
307
|
+
const ancestor = segments.slice(0, index).join("/");
|
|
308
|
+
try {
|
|
309
|
+
const stats = await lstat(resolve(repository.repositoryRoot, ...ancestor.split("/")));
|
|
310
|
+
if (stats.isSymbolicLink() || !stats.isDirectory())
|
|
311
|
+
collisions.push(path);
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
if (!(error &&
|
|
315
|
+
typeof error === "object" &&
|
|
316
|
+
"code" in error &&
|
|
317
|
+
error.code === "ENOENT"))
|
|
318
|
+
collisions.push(path);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const before = headEntries.get(path);
|
|
322
|
+
const after = primaryEntries.get(path);
|
|
323
|
+
const absolute = resolve(repository.repositoryRoot, ...path.split("/"));
|
|
324
|
+
let current;
|
|
325
|
+
try {
|
|
326
|
+
current = await lstat(absolute);
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
if (error &&
|
|
330
|
+
typeof error === "object" &&
|
|
331
|
+
"code" in error &&
|
|
332
|
+
error.code === "ENOENT")
|
|
333
|
+
continue;
|
|
334
|
+
collisions.push(path);
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
// A path absent from HEAD is not necessarily safe merely because status
|
|
338
|
+
// omits it: ignored files and symlink/special parents are real state.
|
|
339
|
+
const trackedRegular = before !== undefined && isRegularPrototypeMode(before.mode);
|
|
340
|
+
const desiredRegular = after !== undefined && isRegularPrototypeMode(after.mode);
|
|
341
|
+
if (current.isSymbolicLink() ||
|
|
342
|
+
(!current.isFile() && !current.isDirectory())) {
|
|
343
|
+
collisions.push(path);
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (current.isDirectory()) {
|
|
347
|
+
// A tracked file-to-directory replacement is only safe when the
|
|
348
|
+
// destination path is absent; even an empty directory can make restore
|
|
349
|
+
// fail midway, while a non-empty one may contain ignored state.
|
|
350
|
+
if (desiredRegular)
|
|
351
|
+
collisions.push(path);
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
// A tracked regular file may be removed by the guarded tree transition;
|
|
355
|
+
// an untracked/ignored file is never safe to overwrite or remove.
|
|
356
|
+
if (!trackedRegular)
|
|
357
|
+
collisions.push(path);
|
|
124
358
|
}
|
|
125
|
-
|
|
126
|
-
|
|
359
|
+
return [...new Set(collisions)];
|
|
360
|
+
}
|
|
361
|
+
async function protectedPlatformCollision(repository, head, primary, namespace, paths, excludeNamespace = true) {
|
|
362
|
+
const headEntries = new Map((await treeEntries(repository, head)).map((entry) => [entry.path, entry]));
|
|
363
|
+
const primaryEntries = new Map((await treeEntries(repository, primary)).map((entry) => [
|
|
364
|
+
entry.path,
|
|
365
|
+
entry,
|
|
366
|
+
]));
|
|
367
|
+
return paths.filter((path) => {
|
|
368
|
+
if (excludeNamespace && isWithin(path, namespace))
|
|
369
|
+
return false;
|
|
370
|
+
const before = headEntries.get(path);
|
|
371
|
+
const after = primaryEntries.get(path);
|
|
372
|
+
const typeChangedToUnsafe = (before !== undefined && !isRegularPrototypeMode(before.mode)) ||
|
|
373
|
+
(after !== undefined && !isRegularPrototypeMode(after.mode));
|
|
374
|
+
return (typeChangedToUnsafe ||
|
|
375
|
+
(isProtectedPrototypePath(path) &&
|
|
376
|
+
(before?.mode !== after?.mode || before?.object !== after?.object)));
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
export async function assertIdentityBranchReady(repository, identityBranch) {
|
|
380
|
+
assertBranch(identityBranch, "identity-branch");
|
|
381
|
+
const branch = await repository.currentBranch();
|
|
382
|
+
if (branch === "HEAD" || branch.length === 0 || branch !== identityBranch)
|
|
383
|
+
throw new GitRepositoryError("conflict", "identity-branch");
|
|
384
|
+
if (await hasUnresolvedRepositoryOperation(repository))
|
|
127
385
|
throw new GitRepositoryError("conflict", "unresolved-operation");
|
|
128
|
-
}
|
|
129
386
|
}
|
|
130
387
|
/**
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
* unsafe repository state so callers can keep authentication/read-only flows
|
|
135
|
-
* available.
|
|
388
|
+
* Reconcile only the committed platform baseline. Tree composition uses
|
|
389
|
+
* private indexes; the guarded Git index/worktree patch then updates the
|
|
390
|
+
* active index and platform worktree while leaving the namespace layer alone.
|
|
136
391
|
*/
|
|
137
392
|
export async function synchronizeIdentityBranch(repository, options) {
|
|
138
393
|
assertBranch(options.identityBranch, "identity-branch");
|
|
139
394
|
assertBranch(options.primaryBranch, "primary-branch");
|
|
140
395
|
assertBranch(options.remote, "remote");
|
|
141
|
-
if (options.identityBranch === options.primaryBranch)
|
|
396
|
+
if (options.identityBranch === options.primaryBranch)
|
|
142
397
|
throw new GitRepositoryError("invalid-configuration", "identity-primary-branch");
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (currentBranch === "HEAD" || currentBranch.length === 0)
|
|
398
|
+
const namespace = namespaceFor(repository, options.prototypeRoot, options.identityBranch);
|
|
399
|
+
let currentBranch = await repository.currentBranch();
|
|
400
|
+
if (currentBranch === "HEAD" || currentBranch.length === 0)
|
|
146
401
|
return unsafe(null, "detached-head");
|
|
147
|
-
|
|
148
|
-
|
|
402
|
+
const originalBranch = currentBranch;
|
|
403
|
+
const originalHead = await refCommit(repository, "HEAD");
|
|
404
|
+
if (await hasUnresolvedRepositoryOperation(repository))
|
|
149
405
|
return unsafe(currentBranch, "unresolved-operation");
|
|
150
|
-
}
|
|
151
|
-
let status = await repository.status();
|
|
152
|
-
if (hasUnmergedFiles(status.files)) {
|
|
153
|
-
return unsafe(currentBranch, "unresolved-operation", {
|
|
154
|
-
affectedPaths: status.files.map((file) => file.path),
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
406
|
const configuredRemote = (await repository.remotes()).some((remote) => remote.name === options.remote);
|
|
158
407
|
if (configuredRemote) {
|
|
159
408
|
try {
|
|
@@ -161,154 +410,402 @@ export async function synchronizeIdentityBranch(repository, options) {
|
|
|
161
410
|
}
|
|
162
411
|
catch (error) {
|
|
163
412
|
if (error instanceof GitRepositoryError &&
|
|
164
|
-
error.kind === "invalid-configuration")
|
|
413
|
+
error.kind === "invalid-configuration")
|
|
165
414
|
throw error;
|
|
166
|
-
}
|
|
167
415
|
return deferred(currentBranch, "fetch-failed");
|
|
168
416
|
}
|
|
169
417
|
}
|
|
418
|
+
const afterFetchBranch = await repository.currentBranch();
|
|
419
|
+
if (afterFetchBranch !== currentBranch)
|
|
420
|
+
return unsafe(afterFetchBranch, "branch-changed");
|
|
421
|
+
currentBranch = afterFetchBranch;
|
|
422
|
+
if (await hasUnresolvedRepositoryOperation(repository))
|
|
423
|
+
return unsafe(currentBranch, "unresolved-operation");
|
|
170
424
|
const branches = await repository.branches();
|
|
171
425
|
const localPrimary = branches.all.includes(options.primaryBranch);
|
|
172
426
|
const primaryRemote = remoteRef(options.remote, options.primaryBranch);
|
|
173
427
|
const remotePrimary = configuredRemote && branches.all.includes(primaryRemote);
|
|
174
|
-
if (configuredRemote ? !remotePrimary : !localPrimary)
|
|
428
|
+
if (configuredRemote ? !remotePrimary : !localPrimary)
|
|
175
429
|
return unsafe(currentBranch, "missing-primary");
|
|
176
|
-
|
|
430
|
+
const sourceRef = remotePrimary ? primaryRemote : options.primaryBranch;
|
|
431
|
+
// Pin the source before planning. Never compose from a mutable remote ref.
|
|
432
|
+
const primarySha = await refCommit(repository, sourceRef);
|
|
177
433
|
const localIdentity = branches.all.includes(options.identityBranch);
|
|
178
434
|
const identityRemote = remoteRef(options.remote, options.identityBranch);
|
|
179
435
|
const remoteIdentity = configuredRemote && branches.all.includes(identityRemote);
|
|
436
|
+
// Match the legacy branch-preparation behavior: a remote-only identity is
|
|
437
|
+
// based on its remote namespace, not silently recreated from primary.
|
|
438
|
+
const identityBaseRef = localIdentity
|
|
439
|
+
? options.identityBranch
|
|
440
|
+
: remoteIdentity
|
|
441
|
+
? identityRemote
|
|
442
|
+
: sourceRef;
|
|
180
443
|
if (localIdentity && remoteIdentity) {
|
|
181
444
|
const relation = await compareRefs(repository, options.identityBranch, identityRemote);
|
|
182
|
-
if (relation.ahead > 0 && relation.behind > 0)
|
|
445
|
+
if (relation.ahead > 0 && relation.behind > 0)
|
|
183
446
|
return unsafe(currentBranch, "identity-divergence");
|
|
184
|
-
}
|
|
185
447
|
}
|
|
186
|
-
let
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
448
|
+
let status = await repository.status();
|
|
449
|
+
assertNoUnresolved(status);
|
|
450
|
+
const dirtyPaths = pathsFromStatus(status.files);
|
|
451
|
+
const dirtyOutside = dirtyPaths.filter((path) => !isWithin(path, namespace));
|
|
452
|
+
if (dirtyOutside.length > 0 || (!status.clean && dirtyPaths.length === 0)) {
|
|
453
|
+
return deferred(currentBranch, "dirty-worktree", {
|
|
454
|
+
affectedPaths: dirtyPaths,
|
|
455
|
+
});
|
|
195
456
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if (
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
203
|
-
|
|
457
|
+
// A checkout is itself a tree transition. Even namespace-only dirt is not
|
|
458
|
+
// safe to carry across it: checkout can replace ignored files that status
|
|
459
|
+
// does not report. Defer before selecting/creating the identity branch.
|
|
460
|
+
if (!localIdentity && dirtyPaths.length > 0) {
|
|
461
|
+
return deferred(currentBranch, "dirty-worktree", {
|
|
462
|
+
affectedPaths: dirtyPaths,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
const head = await refCommit(repository, "HEAD");
|
|
466
|
+
const primaryPaths = await platformChanges(repository, head, primarySha, namespace);
|
|
467
|
+
const protectedPaths = await protectedPlatformCollision(repository, head, primarySha, namespace, primaryPaths);
|
|
468
|
+
const filesystemPaths = await filesystemPlatformCollision(repository, head, primarySha, primaryPaths);
|
|
469
|
+
if (protectedPaths.length > 0 || filesystemPaths.length > 0)
|
|
470
|
+
return deferred(currentBranch, "protected-collision", {
|
|
471
|
+
affectedPaths: [...new Set([...protectedPaths, ...filesystemPaths])],
|
|
472
|
+
});
|
|
473
|
+
const targetHead = await refCommit(repository, identityBaseRef);
|
|
474
|
+
const checkoutPaths = targetHead === originalHead
|
|
475
|
+
? []
|
|
476
|
+
: await treeChanges(repository, originalHead, targetHead);
|
|
477
|
+
const checkoutPlatformPaths = checkoutPaths.filter((path) => !isWithin(path, namespace));
|
|
478
|
+
const checkoutDependencyPaths = checkoutPlatformPaths.filter(dependencyPath);
|
|
479
|
+
if (targetHead !== originalHead) {
|
|
480
|
+
// This preflight includes the target identity namespace. In particular,
|
|
481
|
+
// ignored files absent from the current tree must not be overwritten by
|
|
482
|
+
// selecting an existing remote/local identity branch.
|
|
483
|
+
const checkoutCollisions = await filesystemPlatformCollision(repository, originalHead, targetHead, checkoutPaths);
|
|
484
|
+
const checkoutProtected = await protectedPlatformCollision(repository, originalHead, targetHead, namespace, checkoutPaths, false);
|
|
485
|
+
if (checkoutCollisions.length > 0 || checkoutProtected.length > 0)
|
|
486
|
+
return deferred(currentBranch, "protected-collision", {
|
|
487
|
+
affectedPaths: [
|
|
488
|
+
...new Set([...checkoutCollisions, ...checkoutProtected]),
|
|
489
|
+
],
|
|
490
|
+
});
|
|
204
491
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
])
|
|
210
|
-
.filter((path) => !managedPaths.has(path));
|
|
211
|
-
if (remainingDirtyPaths.length > 0 ||
|
|
212
|
-
(!status.clean && status.files.length === 0)) {
|
|
213
|
-
return deferred(currentBranch, "dirty-worktree", remainingDirtyPaths);
|
|
492
|
+
if (currentBranch !== options.identityBranch && dirtyPaths.length > 0) {
|
|
493
|
+
return deferred(currentBranch, "dirty-worktree", {
|
|
494
|
+
affectedPaths: dirtyPaths,
|
|
495
|
+
});
|
|
214
496
|
}
|
|
215
|
-
let identityCreated = false;
|
|
216
497
|
if (!localIdentity) {
|
|
217
|
-
|
|
218
|
-
|
|
498
|
+
try {
|
|
499
|
+
// Create from the exact preflighted commit, never a moving remote ref.
|
|
500
|
+
// -b refuses a concurrently created local branch instead of replacing it.
|
|
219
501
|
await repository.checkout([
|
|
502
|
+
"--no-overwrite-ignore",
|
|
503
|
+
"--no-track",
|
|
220
504
|
"-b",
|
|
221
505
|
options.identityBranch,
|
|
222
|
-
|
|
223
|
-
`${options.remote}/${options.identityBranch}`,
|
|
506
|
+
targetHead,
|
|
224
507
|
]);
|
|
508
|
+
currentBranch = options.identityBranch;
|
|
509
|
+
if (remoteIdentity)
|
|
510
|
+
await repository.raw([
|
|
511
|
+
"branch",
|
|
512
|
+
`--set-upstream-to=${identityRemote}`,
|
|
513
|
+
options.identityBranch,
|
|
514
|
+
]);
|
|
225
515
|
}
|
|
226
|
-
|
|
227
|
-
await repository.
|
|
516
|
+
catch {
|
|
517
|
+
const observedBranch = await repository.currentBranch();
|
|
518
|
+
return unsafe(observedBranch || currentBranch, "reconciliation-failed", {
|
|
519
|
+
changed: observedBranch !== originalBranch,
|
|
520
|
+
affectedPaths: checkoutPaths,
|
|
521
|
+
platformPaths: checkoutPlatformPaths,
|
|
522
|
+
dependencyPaths: checkoutDependencyPaths,
|
|
523
|
+
restartRequired: checkoutPlatformPaths.length > 0,
|
|
524
|
+
preparationRequired: checkoutDependencyPaths.length > 0,
|
|
525
|
+
});
|
|
228
526
|
}
|
|
229
527
|
}
|
|
230
528
|
else if (currentBranch !== options.identityBranch) {
|
|
231
|
-
await repository.checkout([options.identityBranch]);
|
|
232
|
-
}
|
|
233
|
-
if (primaryAdvance && currentBranch !== options.primaryBranch) {
|
|
234
|
-
await safelyAdvanceLocalPrimary(repository, options.primaryBranch, primaryAdvance.remoteRef, primaryAdvance.localCommit);
|
|
235
|
-
}
|
|
236
|
-
if (localIdentity && remoteIdentity) {
|
|
237
|
-
const relation = await compareRefs(repository, options.identityBranch, identityRemote);
|
|
238
|
-
if (relation.ahead === 0 && relation.behind > 0) {
|
|
239
|
-
await repository.merge([identityRemote, "--ff-only"]);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
const beforeMerge = await refCommit(repository, options.identityBranch);
|
|
243
|
-
try {
|
|
244
|
-
await repository.merge([sourceRef, "--no-edit"]);
|
|
245
|
-
}
|
|
246
|
-
catch (error) {
|
|
247
|
-
if (!(error instanceof GitRepositoryError) || error.kind !== "conflict") {
|
|
248
|
-
return unsafe(options.identityBranch, "merge-failed");
|
|
249
|
-
}
|
|
250
|
-
let affectedPaths = [];
|
|
251
529
|
try {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
530
|
+
await withLockedBranch(repository, options.identityBranch, async () => {
|
|
531
|
+
// Validation without the lock would leave another check/use race.
|
|
532
|
+
// Existing-branch checkout does not write the branch ref, so Git's ref
|
|
533
|
+
// lock can remain held throughout the checkout itself.
|
|
534
|
+
if ((await refCommit(repository, branchRef(options.identityBranch))) !==
|
|
535
|
+
targetHead ||
|
|
536
|
+
(await repository.currentBranch()) !== originalBranch ||
|
|
537
|
+
(await refCommit(repository, "HEAD")) !== originalHead ||
|
|
538
|
+
!(await repository.status()).clean)
|
|
539
|
+
throw new GitRepositoryError("conflict", "checkout-target-changed");
|
|
540
|
+
await repository.checkout([
|
|
541
|
+
"--no-overwrite-ignore",
|
|
542
|
+
options.identityBranch,
|
|
543
|
+
]);
|
|
544
|
+
currentBranch = options.identityBranch;
|
|
545
|
+
});
|
|
259
546
|
}
|
|
260
547
|
catch {
|
|
261
|
-
|
|
262
|
-
|
|
548
|
+
const observedBranch = await repository.currentBranch();
|
|
549
|
+
return unsafe(observedBranch || currentBranch, "reconciliation-failed", {
|
|
550
|
+
changed: observedBranch !== originalBranch,
|
|
551
|
+
affectedPaths: checkoutPaths,
|
|
552
|
+
platformPaths: checkoutPlatformPaths,
|
|
553
|
+
dependencyPaths: checkoutDependencyPaths,
|
|
554
|
+
restartRequired: checkoutPlatformPaths.length > 0,
|
|
555
|
+
preparationRequired: checkoutDependencyPaths.length > 0,
|
|
263
556
|
});
|
|
264
557
|
}
|
|
265
|
-
return deferred(options.identityBranch, "merge-conflict", affectedPaths);
|
|
266
558
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
559
|
+
// Re-read after branch movement and before touching the index. This catches
|
|
560
|
+
// an external command or a fetch hook that changed the checkout meanwhile.
|
|
561
|
+
const branchBeforeMutation = await repository.currentBranch();
|
|
562
|
+
const checkoutChanged = currentBranch !== originalBranch;
|
|
563
|
+
const checkoutEffects = {
|
|
564
|
+
changed: checkoutChanged,
|
|
565
|
+
affectedPaths: checkoutPaths,
|
|
566
|
+
platformPaths: checkoutPlatformPaths,
|
|
567
|
+
dependencyPaths: checkoutDependencyPaths,
|
|
568
|
+
restartRequired: checkoutPlatformPaths.length > 0,
|
|
569
|
+
preparationRequired: checkoutDependencyPaths.length > 0,
|
|
570
|
+
};
|
|
571
|
+
if (branchBeforeMutation !== currentBranch ||
|
|
572
|
+
branchBeforeMutation === "HEAD") {
|
|
573
|
+
return unsafe(branchBeforeMutation || null, "branch-changed", {
|
|
574
|
+
...checkoutEffects,
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
status = await repository.status();
|
|
578
|
+
if (!status.clean &&
|
|
579
|
+
pathsFromStatus(status.files).some((path) => !isWithin(path, namespace))) {
|
|
580
|
+
return deferred(currentBranch, "dirty-worktree", {
|
|
581
|
+
...checkoutEffects,
|
|
582
|
+
affectedPaths: [
|
|
583
|
+
...new Set([...checkoutPaths, ...pathsFromStatus(status.files)]),
|
|
584
|
+
],
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
if (await hasUnresolvedRepositoryOperation(repository))
|
|
588
|
+
return unsafe(currentBranch, "unresolved-operation", checkoutEffects);
|
|
589
|
+
const latestHead = await refCommit(repository, "HEAD");
|
|
590
|
+
if (latestHead !== targetHead)
|
|
591
|
+
return unsafe(currentBranch, "reconciliation-failed", {
|
|
592
|
+
...checkoutEffects,
|
|
593
|
+
changed: true,
|
|
594
|
+
});
|
|
595
|
+
const latestPrimarySha = await refCommit(repository, sourceRef);
|
|
596
|
+
const branchSelectionChanged = currentBranch !== originalBranch || latestHead !== originalHead;
|
|
597
|
+
if (latestPrimarySha !== primarySha)
|
|
598
|
+
return unsafe(currentBranch, "reconciliation-failed", {
|
|
599
|
+
...checkoutEffects,
|
|
600
|
+
affectedPaths: [...new Set([...checkoutPaths, ...primaryPaths])],
|
|
601
|
+
});
|
|
602
|
+
const latestPrimaryPaths = await platformChanges(repository, latestHead, primarySha, namespace);
|
|
603
|
+
const latestProtected = await protectedPlatformCollision(repository, latestHead, primarySha, namespace, latestPrimaryPaths);
|
|
604
|
+
const latestFilesystem = await filesystemPlatformCollision(repository, latestHead, primarySha, latestPrimaryPaths);
|
|
605
|
+
if (latestProtected.length > 0 || latestFilesystem.length > 0)
|
|
606
|
+
return deferred(currentBranch, "protected-collision", {
|
|
607
|
+
...checkoutEffects,
|
|
608
|
+
affectedPaths: [
|
|
609
|
+
...new Set([...checkoutPaths, ...latestProtected, ...latestFilesystem]),
|
|
610
|
+
],
|
|
611
|
+
});
|
|
612
|
+
const plannedPlatformPaths = [
|
|
613
|
+
...new Set([...latestPrimaryPaths, ...checkoutPlatformPaths]),
|
|
614
|
+
];
|
|
615
|
+
const plannedDependencyPaths = [
|
|
280
616
|
...new Set([
|
|
281
|
-
...
|
|
282
|
-
...
|
|
283
|
-
...(identityCreated ? [...managedPaths] : []),
|
|
617
|
+
...checkoutDependencyPaths,
|
|
618
|
+
...latestPrimaryPaths.filter(dependencyPath),
|
|
284
619
|
]),
|
|
285
620
|
];
|
|
286
|
-
if (
|
|
621
|
+
if (latestPrimaryPaths.length === 0) {
|
|
287
622
|
return {
|
|
623
|
+
...resultBase(currentBranch, {
|
|
624
|
+
changed: branchSelectionChanged,
|
|
625
|
+
affectedPaths: checkoutPaths,
|
|
626
|
+
platformPaths: checkoutPlatformPaths,
|
|
627
|
+
dependencyPaths: checkoutDependencyPaths,
|
|
628
|
+
restartRequired: checkoutPlatformPaths.length > 0,
|
|
629
|
+
preparationRequired: checkoutDependencyPaths.length > 0,
|
|
630
|
+
}),
|
|
288
631
|
status: "synchronized",
|
|
289
|
-
branch: options.identityBranch,
|
|
290
|
-
changed,
|
|
291
|
-
pushed: false,
|
|
292
|
-
affectedPaths,
|
|
293
632
|
};
|
|
294
633
|
}
|
|
634
|
+
const indexTree = (await repository.raw(["write-tree"])).trim();
|
|
635
|
+
const stagedNamespace = await indexNamespace(repository, namespace);
|
|
636
|
+
let baselineCommit;
|
|
637
|
+
let desiredIndexTree;
|
|
638
|
+
let patchDirectory;
|
|
639
|
+
let patchFile;
|
|
640
|
+
const dependencies = plannedDependencyPaths;
|
|
295
641
|
try {
|
|
296
|
-
|
|
297
|
-
|
|
642
|
+
// All object creation and tree composition happen in temporary indexes.
|
|
643
|
+
// The live index is never used as a tree builder.
|
|
644
|
+
const composedTree = await composeTree(repository, primarySha, latestHead, namespace);
|
|
645
|
+
baselineCommit = (await repository.raw([
|
|
646
|
+
"commit-tree",
|
|
647
|
+
composedTree,
|
|
648
|
+
"-p",
|
|
649
|
+
latestHead,
|
|
650
|
+
"-m",
|
|
651
|
+
"chore: update committed platform baseline",
|
|
652
|
+
])).trim();
|
|
653
|
+
if (!/^[0-9a-f]{40}$/iu.test(baselineCommit))
|
|
654
|
+
throw new GitRepositoryError("malformed-output", "baseline-commit");
|
|
655
|
+
if ((await repository.currentBranch()) !== currentBranch ||
|
|
656
|
+
(await refCommit(repository, "HEAD")) !== latestHead ||
|
|
657
|
+
(await refCommit(repository, sourceRef)) !== primarySha ||
|
|
658
|
+
(await repository.raw(["write-tree"])).trim() !== indexTree) {
|
|
659
|
+
throw new GitRepositoryError("conflict", "external-state");
|
|
660
|
+
}
|
|
661
|
+
desiredIndexTree = await overlayIndexNamespace(repository, baselineCommit, namespace, stagedNamespace);
|
|
662
|
+
// Apply the tree delta through Git's guarded index/worktree primitive.
|
|
663
|
+
// Unlike checkout/read-tree -u, git apply --index refuses to overwrite an
|
|
664
|
+
// untracked or ignored path that appears between preflight and mutation.
|
|
665
|
+
// The trees carry the same staged namespace, and the patch excludes that
|
|
666
|
+
// namespace, so staged and unstaged prototype files are not touched.
|
|
667
|
+
if ((await repository.raw(["write-tree"])).trim() !== indexTree)
|
|
668
|
+
throw new GitRepositoryError("conflict", "external-index");
|
|
669
|
+
patchDirectory = await mkdtemp(join(tmpdir(), "bonfire-patch-"));
|
|
670
|
+
patchFile = join(patchDirectory, "platform.patch");
|
|
671
|
+
const patch = await repository.raw([
|
|
672
|
+
"diff",
|
|
673
|
+
"--binary",
|
|
674
|
+
indexTree,
|
|
675
|
+
desiredIndexTree,
|
|
676
|
+
"--",
|
|
677
|
+
".",
|
|
678
|
+
`:(exclude)${namespace}`,
|
|
679
|
+
]);
|
|
680
|
+
await writeFile(patchFile, patch);
|
|
681
|
+
await repository.raw(["apply", "--index", patchFile]);
|
|
682
|
+
if ((await repository.currentBranch()) !== currentBranch ||
|
|
683
|
+
(await refCommit(repository, "HEAD")) !== latestHead ||
|
|
684
|
+
(await refCommit(repository, sourceRef)) !== primarySha ||
|
|
685
|
+
(await repository.raw(["write-tree"])).trim() !== desiredIndexTree)
|
|
686
|
+
throw new GitRepositoryError("conflict", "external-state");
|
|
687
|
+
// Ref movement is last. The expected old value is a CAS guard against an
|
|
688
|
+
// external branch writer.
|
|
689
|
+
await repository.raw([
|
|
690
|
+
"update-ref",
|
|
691
|
+
branchRef(options.identityBranch),
|
|
692
|
+
baselineCommit,
|
|
693
|
+
latestHead,
|
|
298
694
|
]);
|
|
695
|
+
const landedHead = await refCommit(repository, "HEAD");
|
|
696
|
+
if (landedHead !== baselineCommit)
|
|
697
|
+
throw new GitRepositoryError("conflict", "ref-transition");
|
|
698
|
+
if ((await repository.currentBranch()) !== currentBranch ||
|
|
699
|
+
(await refCommit(repository, sourceRef)) !== primarySha ||
|
|
700
|
+
(await repository.raw(["write-tree"])).trim() !== desiredIndexTree)
|
|
701
|
+
throw new GitRepositoryError("conflict", "external-state");
|
|
702
|
+
const finalStatus = await repository.status();
|
|
703
|
+
const unexpectedFinalPaths = pathsFromStatus(finalStatus.files).filter((path) => !isWithin(path, namespace));
|
|
704
|
+
if (unexpectedFinalPaths.length > 0)
|
|
705
|
+
throw new GitRepositoryError("conflict", "external-worktree");
|
|
299
706
|
}
|
|
300
707
|
catch {
|
|
301
|
-
|
|
708
|
+
// A command wrapper may throw after running a command. First try the
|
|
709
|
+
// narrowly guarded inverse of our original patch. This is allowed only
|
|
710
|
+
// while the original branch/ref/source and exact intended index/worktree
|
|
711
|
+
// state still hold; git apply itself then protects intervening edits.
|
|
712
|
+
let recovered = false;
|
|
713
|
+
if (patchFile !== undefined && desiredIndexTree !== undefined) {
|
|
714
|
+
try {
|
|
715
|
+
const sameState = (await repository.currentBranch()) === currentBranch &&
|
|
716
|
+
(await refCommit(repository, "HEAD")) === latestHead &&
|
|
717
|
+
(await refCommit(repository, sourceRef)) === primarySha &&
|
|
718
|
+
(await repository.raw(["write-tree"])).trim() === desiredIndexTree;
|
|
719
|
+
if (sameState) {
|
|
720
|
+
await repository.raw([
|
|
721
|
+
"diff",
|
|
722
|
+
"--quiet",
|
|
723
|
+
desiredIndexTree,
|
|
724
|
+
"--",
|
|
725
|
+
".",
|
|
726
|
+
`:(exclude)${namespace}`,
|
|
727
|
+
]);
|
|
728
|
+
const observed = await repository.status();
|
|
729
|
+
const unexpected = pathsFromStatus(observed.files).filter((path) => !isWithin(path, namespace) &&
|
|
730
|
+
!latestPrimaryPaths.some((expected) => isWithin(path, expected) || isWithin(expected, path)));
|
|
731
|
+
if (unexpected.length === 0) {
|
|
732
|
+
await repository.raw(["apply", "--reverse", "--index", patchFile]);
|
|
733
|
+
const restored = await repository.status();
|
|
734
|
+
recovered =
|
|
735
|
+
(await repository.raw(["write-tree"])).trim() === indexTree &&
|
|
736
|
+
(await refCommit(repository, "HEAD")) === latestHead &&
|
|
737
|
+
(await repository.currentBranch()) === currentBranch &&
|
|
738
|
+
(await repository.raw([
|
|
739
|
+
"diff",
|
|
740
|
+
"--quiet",
|
|
741
|
+
indexTree,
|
|
742
|
+
"--",
|
|
743
|
+
".",
|
|
744
|
+
`:(exclude)${namespace}`,
|
|
745
|
+
])) === "" &&
|
|
746
|
+
pathsFromStatus(restored.files).every((path) => isWithin(path, namespace));
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
catch {
|
|
751
|
+
// Preserve the partial transition. Manual recovery must begin from
|
|
752
|
+
// the reported old HEAD and inspect the exact platform paths before
|
|
753
|
+
// applying the original patch in reverse; never reset or clean here.
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
if (patchDirectory !== undefined)
|
|
757
|
+
await rm(patchDirectory, { recursive: true, force: true }).catch(() => undefined);
|
|
758
|
+
let landed;
|
|
759
|
+
let indexChanged = false;
|
|
760
|
+
let worktreeChanged = false;
|
|
761
|
+
try {
|
|
762
|
+
landed = (await refCommit(repository, "HEAD")) !== latestHead;
|
|
763
|
+
}
|
|
764
|
+
catch {
|
|
765
|
+
// A malformed/missing ref is itself an incomplete transition.
|
|
766
|
+
landed = true;
|
|
767
|
+
}
|
|
768
|
+
if (desiredIndexTree !== undefined) {
|
|
769
|
+
try {
|
|
770
|
+
indexChanged =
|
|
771
|
+
(await repository.raw(["write-tree"])).trim() !== indexTree;
|
|
772
|
+
}
|
|
773
|
+
catch {
|
|
774
|
+
indexChanged = true;
|
|
775
|
+
}
|
|
776
|
+
// The guarded --index patch changes the index and worktree as one Git
|
|
777
|
+
// operation. An index-tree change is therefore the reliable
|
|
778
|
+
// observable for a transition that ran before an acknowledgement failed;
|
|
779
|
+
// a pure pre-side-effect rejection leaves both layers unchanged.
|
|
780
|
+
worktreeChanged = indexChanged;
|
|
781
|
+
}
|
|
782
|
+
const changed = recovered
|
|
783
|
+
? branchSelectionChanged
|
|
784
|
+
: branchSelectionChanged || landed || indexChanged || worktreeChanged;
|
|
785
|
+
return unsafe(currentBranch, "reconciliation-failed", {
|
|
302
786
|
changed,
|
|
303
|
-
affectedPaths,
|
|
787
|
+
affectedPaths: [...new Set([...checkoutPaths, ...latestPrimaryPaths])],
|
|
788
|
+
dependencyPaths: dependencies,
|
|
789
|
+
platformPaths: plannedPlatformPaths,
|
|
790
|
+
restartRequired: checkoutPlatformPaths.length > 0 ||
|
|
791
|
+
(!recovered && (worktreeChanged || landed)),
|
|
792
|
+
preparationRequired: dependencies.length > 0 &&
|
|
793
|
+
(checkoutDependencyPaths.length > 0 ||
|
|
794
|
+
(!recovered && (landed || worktreeChanged))),
|
|
304
795
|
});
|
|
305
796
|
}
|
|
797
|
+
if (patchDirectory !== undefined)
|
|
798
|
+
await rm(patchDirectory, { recursive: true, force: true });
|
|
306
799
|
return {
|
|
800
|
+
...resultBase(currentBranch, {
|
|
801
|
+
changed: true,
|
|
802
|
+
affectedPaths: [...new Set([...checkoutPaths, ...latestPrimaryPaths])],
|
|
803
|
+
platformPaths: plannedPlatformPaths,
|
|
804
|
+
dependencyPaths: dependencies,
|
|
805
|
+
restartRequired: plannedPlatformPaths.length > 0,
|
|
806
|
+
preparationRequired: dependencies.length > 0,
|
|
807
|
+
}),
|
|
307
808
|
status: "synchronized",
|
|
308
|
-
branch: options.identityBranch,
|
|
309
|
-
changed: true,
|
|
310
|
-
pushed: true,
|
|
311
|
-
affectedPaths,
|
|
312
809
|
};
|
|
313
810
|
}
|
|
314
811
|
//# sourceMappingURL=repository-synchronization.js.map
|