@ferrule-io/ok-fine 0.3.6 → 0.3.8
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 +5 -4
- package/dist/auth/discovery.js +11 -0
- package/dist/auth/verifier.js +44 -1
- package/dist/cli-options.js +1 -1
- package/dist/cli.js +12 -14
- package/dist/config.js +130 -6
- package/dist/data-dir-lock.js +58 -19
- package/dist/http/rest.js +1 -1
- package/dist/http/ui.js +118 -0
- package/dist/local-host.js +157 -0
- package/dist/mcp/server.js +5 -4
- package/dist/okf/lint.js +20 -0
- package/dist/server.js +138 -29
- package/dist/stdio-proxy.js +349 -0
- package/dist/store/fs-util.js +59 -4
- package/dist/store/git-backend.js +208 -59
- package/dist/store/git.js +92 -19
- package/dist/ui/assets/bash-BiGZg9Om.js +1 -0
- package/dist/ui/assets/css-BGIxQoYs.js +1 -0
- package/dist/ui/assets/css-DUjzNfgv.js +1 -0
- package/dist/ui/assets/diff-woXpYk--.js +1 -0
- package/dist/ui/assets/dockerfile-IyjqRm3v.js +1 -0
- package/dist/ui/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/dist/ui/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/dist/ui/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/dist/ui/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
- package/dist/ui/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
- package/dist/ui/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
- package/dist/ui/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
- package/dist/ui/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
- package/dist/ui/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/dist/ui/assets/github-dark-default-DXG-b-1a.js +1 -0
- package/dist/ui/assets/github-light-default-BXViO-2h.js +1 -0
- package/dist/ui/assets/go-rLFTqkRN.js +1 -0
- package/dist/ui/assets/highlight-CW19vWvw.js +153 -0
- package/dist/ui/assets/html-BMn1ogTm.js +1 -0
- package/dist/ui/assets/html-BlpXnbzK.js +1 -0
- package/dist/ui/assets/index-0b2NNRC5.js +127 -0
- package/dist/ui/assets/index-CFuTAZQM.css +1 -0
- package/dist/ui/assets/java-BtIQNnN0.js +1 -0
- package/dist/ui/assets/java-DxlWrXwe.js +1 -0
- package/dist/ui/assets/javascript-BzbQS41l.js +1 -0
- package/dist/ui/assets/javascript-Yelw-5ZN.js +1 -0
- package/dist/ui/assets/json-qhed-kSA.js +1 -0
- package/dist/ui/assets/jsx-D3KKoC5L.js +1 -0
- package/dist/ui/assets/jsx-hEbJ82HF.js +1 -0
- package/dist/ui/assets/markdown-BYOwaDjH.js +1 -0
- package/dist/ui/assets/python-gzcpVVnB.js +1 -0
- package/dist/ui/assets/ruby-Di5fYh6S.js +1 -0
- package/dist/ui/assets/rust-DpiIf5JO.js +1 -0
- package/dist/ui/assets/shellscript-CqHdAlCp.js +1 -0
- package/dist/ui/assets/sql-BOsskAmu.js +1 -0
- package/dist/ui/assets/sql-CNu3Or2W.js +1 -0
- package/dist/ui/assets/toml-CcmNWLt0.js +1 -0
- package/dist/ui/assets/tsx-5f3ulCja.js +1 -0
- package/dist/ui/assets/tsx-BOvsFOI6.js +1 -0
- package/dist/ui/assets/typescript-Utq2Cl8c.js +1 -0
- package/dist/ui/assets/typescript-j_1H8WHN.js +1 -0
- package/dist/ui/assets/yaml-CxbubiML.js +1 -0
- package/dist/ui/assets/yaml-DijMHRDA.js +1 -0
- package/dist/ui/favicon.svg +11 -0
- package/dist/ui/index.html +16 -0
- package/dist/ui/theme-init.js +10 -0
- package/dist/version.js +1 -1
- package/package.json +25 -4
- package/dist/store/repo.js +0 -471
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import { mkdir, readdir, readFile, rm, rmdir
|
|
1
|
+
import { lstat, mkdir, readdir, readFile, rm, rmdir } from "node:fs/promises";
|
|
2
2
|
import { join, posix } from "node:path";
|
|
3
3
|
import { OkfError } from "../errors.js";
|
|
4
4
|
import { nowIso } from "../okf/frontmatter.js";
|
|
5
5
|
import { PROJECT_RE } from "../okf/paths.js";
|
|
6
6
|
import { parseActor } from "../okf/semantics.js";
|
|
7
|
-
import {
|
|
7
|
+
import { assertContainedPath, atomicWrite, listTreeFiles, pathExists } from "./fs-util.js";
|
|
8
8
|
import { Git, isPushRejection, redactRemote } from "./git.js";
|
|
9
9
|
import { Mutex } from "./mutex.js";
|
|
10
10
|
import { PathIndex } from "./path-index.js";
|
|
11
|
+
const REMOTE_BASE_REF = "refs/ok-fine/remote-base";
|
|
12
|
+
const REMOTE_REWRITTEN_MSG = "remote history was rewritten; syncing halted. Stop ok-fine, remove DATA_DIR/repo, and restart to re-clone";
|
|
11
13
|
function extractChangedProjects(diffOutput) {
|
|
12
14
|
const lines = diffOutput
|
|
13
15
|
.split(/\r?\n/)
|
|
@@ -32,11 +34,13 @@ class GitTx {
|
|
|
32
34
|
this.index = index;
|
|
33
35
|
}
|
|
34
36
|
async writeFile(project, path, content) {
|
|
37
|
+
await assertContainedPath(this.repoDir, project, path);
|
|
35
38
|
await atomicWrite(join(this.repoDir, project, path), content);
|
|
36
39
|
this.index.add(project, path);
|
|
37
40
|
this.touched = true;
|
|
38
41
|
}
|
|
39
42
|
async deleteFile(project, path) {
|
|
43
|
+
await assertContainedPath(this.repoDir, project, path);
|
|
40
44
|
const projectDir = join(this.repoDir, project);
|
|
41
45
|
await rm(join(projectDir, path), { force: true });
|
|
42
46
|
let dir = posix.dirname(path);
|
|
@@ -57,15 +61,43 @@ class GitTx {
|
|
|
57
61
|
this.touched = true;
|
|
58
62
|
}
|
|
59
63
|
async deleteProject(project) {
|
|
60
|
-
|
|
64
|
+
const projectDir = join(this.repoDir, project);
|
|
65
|
+
try {
|
|
66
|
+
const s = await lstat(projectDir);
|
|
67
|
+
if (s.isSymbolicLink()) {
|
|
68
|
+
await rm(projectDir, { force: true });
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
await rm(projectDir, { recursive: true, force: true });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
if (err.code !== "ENOENT") {
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
61
79
|
this.index.setProject(project, []);
|
|
62
80
|
this.touched = true;
|
|
63
81
|
}
|
|
64
82
|
async replaceProject(project, source) {
|
|
65
83
|
const projectDir = join(this.repoDir, project);
|
|
66
|
-
|
|
84
|
+
try {
|
|
85
|
+
const s = await lstat(projectDir);
|
|
86
|
+
if (s.isSymbolicLink()) {
|
|
87
|
+
await rm(projectDir, { force: true });
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
await rm(projectDir, { recursive: true, force: true });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
if (err.code !== "ENOENT") {
|
|
95
|
+
throw err;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
67
98
|
const written = [];
|
|
68
99
|
for (const path of source.paths) {
|
|
100
|
+
await assertContainedPath(this.repoDir, project, path);
|
|
69
101
|
const content = await source.read(path);
|
|
70
102
|
if (content !== null) {
|
|
71
103
|
await atomicWrite(join(projectDir, path), content);
|
|
@@ -102,6 +134,7 @@ export class GitBackend {
|
|
|
102
134
|
}
|
|
103
135
|
async close() {
|
|
104
136
|
await this.mutex.idle();
|
|
137
|
+
await this.git.close();
|
|
105
138
|
}
|
|
106
139
|
async projects() {
|
|
107
140
|
return this.index.projects();
|
|
@@ -114,18 +147,30 @@ export class GitBackend {
|
|
|
114
147
|
return null;
|
|
115
148
|
}
|
|
116
149
|
try {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (
|
|
150
|
+
await assertContainedPath(this.repoDir, project, path);
|
|
151
|
+
const fullPath = join(this.repoDir, project, path);
|
|
152
|
+
const s = await lstat(fullPath);
|
|
153
|
+
if (s.isSymbolicLink() || !s.isFile()) {
|
|
121
154
|
return null;
|
|
122
155
|
}
|
|
123
|
-
|
|
156
|
+
return await readFile(fullPath);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return null;
|
|
124
160
|
}
|
|
125
161
|
}
|
|
126
162
|
async reindexProject(project) {
|
|
127
163
|
const dir = join(this.repoDir, project);
|
|
128
|
-
|
|
164
|
+
let paths = [];
|
|
165
|
+
try {
|
|
166
|
+
const s = await lstat(dir);
|
|
167
|
+
if (!s.isSymbolicLink() && s.isDirectory()) {
|
|
168
|
+
paths = await listTreeFiles(dir);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
paths = [];
|
|
173
|
+
}
|
|
129
174
|
this.index.setProject(project, paths);
|
|
130
175
|
}
|
|
131
176
|
async reindexAll() {
|
|
@@ -200,6 +245,7 @@ export class GitBackend {
|
|
|
200
245
|
if (remoteBranchExists) {
|
|
201
246
|
await repo.git.run(["checkout", "-B", config.gitBranch, `origin/${config.gitBranch}`]);
|
|
202
247
|
headHasCommits = true;
|
|
248
|
+
await repo.setRememberedRemoteTip(remoteBranchRev.stdout.trim());
|
|
203
249
|
}
|
|
204
250
|
else {
|
|
205
251
|
// Initialize empty bundle root files
|
|
@@ -214,6 +260,8 @@ export class GitBackend {
|
|
|
214
260
|
allowFail: true,
|
|
215
261
|
});
|
|
216
262
|
headHasCommits = true;
|
|
263
|
+
const initialHead = (await repo.git.run(["rev-parse", "HEAD"])).stdout.trim();
|
|
264
|
+
await repo.setRememberedRemoteTip(initialHead);
|
|
217
265
|
}
|
|
218
266
|
}
|
|
219
267
|
else {
|
|
@@ -245,24 +293,67 @@ export class GitBackend {
|
|
|
245
293
|
"okf: configure merge and ignore attributes",
|
|
246
294
|
]);
|
|
247
295
|
if (repo.hasRemote) {
|
|
248
|
-
await repo.git.run(["push", "origin", `HEAD:refs/heads/${config.gitBranch}`], {
|
|
296
|
+
const pushRes = await repo.git.run(["push", "origin", `HEAD:refs/heads/${config.gitBranch}`], {
|
|
249
297
|
allowFail: true,
|
|
250
298
|
});
|
|
299
|
+
if (pushRes.code === 0) {
|
|
300
|
+
const headSha = (await repo.git.run(["rev-parse", "HEAD"])).stdout.trim();
|
|
301
|
+
await repo.setRememberedRemoteTip(headSha);
|
|
302
|
+
}
|
|
251
303
|
}
|
|
252
304
|
}
|
|
253
305
|
}
|
|
254
306
|
await repo.reindexAll();
|
|
255
307
|
return repo;
|
|
256
308
|
}
|
|
309
|
+
async getRememberedRemoteTip() {
|
|
310
|
+
const res = await this.git.run(["rev-parse", "--verify", REMOTE_BASE_REF], {
|
|
311
|
+
allowFail: true,
|
|
312
|
+
});
|
|
313
|
+
if (res.code === 0 && res.stdout.trim().length > 0) {
|
|
314
|
+
return res.stdout.trim();
|
|
315
|
+
}
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
async setRememberedRemoteTip(sha) {
|
|
319
|
+
await this.git.run(["update-ref", REMOTE_BASE_REF, sha], { allowFail: true });
|
|
320
|
+
}
|
|
321
|
+
async isRemoteAncestorHalted(remoteTip) {
|
|
322
|
+
const rememberedTip = await this.getRememberedRemoteTip();
|
|
323
|
+
if (!rememberedTip) {
|
|
324
|
+
await this.setRememberedRemoteTip(remoteTip);
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
if (rememberedTip === remoteTip) {
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
const ancestorCheck = await this.git.run(["merge-base", "--is-ancestor", rememberedTip, remoteTip], {
|
|
331
|
+
allowFail: true,
|
|
332
|
+
});
|
|
333
|
+
if (ancestorCheck.code !== 0) {
|
|
334
|
+
this.lastError = REMOTE_REWRITTEN_MSG;
|
|
335
|
+
this.log.error({ rememberedTip, remoteTip }, REMOTE_REWRITTEN_MSG);
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
257
340
|
async ensureAttributesAndIgnore() {
|
|
258
341
|
let changed = false;
|
|
259
342
|
const gitattributesPath = join(this.repoDir, ".gitattributes");
|
|
260
343
|
let attrContent = "";
|
|
261
344
|
try {
|
|
262
|
-
|
|
345
|
+
const s = await lstat(gitattributesPath);
|
|
346
|
+
if (s.isSymbolicLink()) {
|
|
347
|
+
await rm(gitattributesPath, { force: true });
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
attrContent = await readFile(gitattributesPath, "utf8");
|
|
351
|
+
}
|
|
263
352
|
}
|
|
264
|
-
catch {
|
|
265
|
-
|
|
353
|
+
catch (err) {
|
|
354
|
+
if (err.code !== "ENOENT") {
|
|
355
|
+
throw err;
|
|
356
|
+
}
|
|
266
357
|
}
|
|
267
358
|
const requiredAttrLines = ["**/index.md merge=union", "**/log.md merge=union"];
|
|
268
359
|
let newAttrContent = attrContent;
|
|
@@ -275,17 +366,25 @@ export class GitBackend {
|
|
|
275
366
|
}
|
|
276
367
|
}
|
|
277
368
|
if (newAttrContent !== attrContent) {
|
|
278
|
-
await
|
|
369
|
+
await atomicWrite(gitattributesPath, newAttrContent);
|
|
279
370
|
await this.git.run(["add", ".gitattributes"]);
|
|
280
371
|
changed = true;
|
|
281
372
|
}
|
|
282
373
|
const gitignorePath = join(this.repoDir, ".gitignore");
|
|
283
374
|
let ignoreContent = "";
|
|
284
375
|
try {
|
|
285
|
-
|
|
376
|
+
const s = await lstat(gitignorePath);
|
|
377
|
+
if (s.isSymbolicLink()) {
|
|
378
|
+
await rm(gitignorePath, { force: true });
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
ignoreContent = await readFile(gitignorePath, "utf8");
|
|
382
|
+
}
|
|
286
383
|
}
|
|
287
|
-
catch {
|
|
288
|
-
|
|
384
|
+
catch (err) {
|
|
385
|
+
if (err.code !== "ENOENT") {
|
|
386
|
+
throw err;
|
|
387
|
+
}
|
|
289
388
|
}
|
|
290
389
|
const requiredIgnoreLine = ".*.tmp";
|
|
291
390
|
let newIgnoreContent = ignoreContent;
|
|
@@ -296,7 +395,7 @@ export class GitBackend {
|
|
|
296
395
|
newIgnoreContent += `${requiredIgnoreLine}\n`;
|
|
297
396
|
}
|
|
298
397
|
if (newIgnoreContent !== ignoreContent) {
|
|
299
|
-
await
|
|
398
|
+
await atomicWrite(gitignorePath, newIgnoreContent);
|
|
300
399
|
await this.git.run(["add", ".gitignore"]);
|
|
301
400
|
changed = true;
|
|
302
401
|
}
|
|
@@ -348,6 +447,7 @@ export class GitBackend {
|
|
|
348
447
|
async transaction(spec, work) {
|
|
349
448
|
return this.mutex.run(async () => {
|
|
350
449
|
const warnings = [];
|
|
450
|
+
let syncHalted = false;
|
|
351
451
|
// Step 1: Remote sync check before work
|
|
352
452
|
if (this.hasRemote) {
|
|
353
453
|
const fetchRes = await this.git.run(["fetch", "origin", this.config.gitBranch], { allowFail: true });
|
|
@@ -356,20 +456,32 @@ export class GitBackend {
|
|
|
356
456
|
warnings.push(`remote unreachable: ${firstLine}`);
|
|
357
457
|
}
|
|
358
458
|
else {
|
|
359
|
-
const
|
|
459
|
+
const remoteRevRes = await this.git.run(["rev-parse", "--verify", `origin/${this.config.gitBranch}`], {
|
|
360
460
|
allowFail: true,
|
|
361
461
|
});
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
warnings.push(await this.preserveConflict());
|
|
462
|
+
if (remoteRevRes.code === 0) {
|
|
463
|
+
const remoteTip = remoteRevRes.stdout.trim();
|
|
464
|
+
if (await this.isRemoteAncestorHalted(remoteTip)) {
|
|
465
|
+
syncHalted = true;
|
|
466
|
+
warnings.push(this.lastError ?? REMOTE_REWRITTEN_MSG);
|
|
368
467
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
468
|
+
}
|
|
469
|
+
if (!syncHalted) {
|
|
470
|
+
const behindRes = await this.git.run(["rev-list", "--count", `HEAD..origin/${this.config.gitBranch}`], {
|
|
471
|
+
allowFail: true,
|
|
472
|
+
});
|
|
473
|
+
const behind = Number.parseInt(behindRes.stdout.trim(), 10) || 0;
|
|
474
|
+
if (behind > 0) {
|
|
475
|
+
const before = (await this.git.run(["rev-parse", "HEAD"])).stdout.trim();
|
|
476
|
+
const rebaseRes = await this.git.run(["rebase", `origin/${this.config.gitBranch}`], { allowFail: true });
|
|
477
|
+
if (rebaseRes.code !== 0) {
|
|
478
|
+
warnings.push(await this.preserveConflict());
|
|
479
|
+
}
|
|
480
|
+
const after = (await this.git.run(["rev-parse", "HEAD"])).stdout.trim();
|
|
481
|
+
if (before !== after) {
|
|
482
|
+
const diffRes = await this.git.run(["diff", "--name-only", before, after]);
|
|
483
|
+
await this.resync(extractChangedProjects(diffRes.stdout));
|
|
484
|
+
}
|
|
373
485
|
}
|
|
374
486
|
}
|
|
375
487
|
}
|
|
@@ -399,42 +511,61 @@ export class GitBackend {
|
|
|
399
511
|
// Step 4: Push commit if made
|
|
400
512
|
let pushed = null;
|
|
401
513
|
if (commitSha !== null && this.hasRemote) {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
const
|
|
419
|
-
await this.
|
|
514
|
+
if (syncHalted) {
|
|
515
|
+
pushed = false;
|
|
516
|
+
}
|
|
517
|
+
else {
|
|
518
|
+
let pushSuccess = false;
|
|
519
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
520
|
+
const pushRes = await this.git.run(["push", "origin", `HEAD:refs/heads/${this.config.gitBranch}`], {
|
|
521
|
+
allowFail: true,
|
|
522
|
+
});
|
|
523
|
+
if (pushRes.code === 0) {
|
|
524
|
+
pushSuccess = true;
|
|
525
|
+
pushed = true;
|
|
526
|
+
await this.setRememberedRemoteTip(commitSha);
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
if (isPushRejection(pushRes.stderr)) {
|
|
530
|
+
const pre = (await this.git.run(["rev-parse", "HEAD"])).stdout.trim();
|
|
531
|
+
await this.git.run(["fetch", "origin", this.config.gitBranch], { allowFail: true });
|
|
532
|
+
const remoteRevRes = await this.git.run(["rev-parse", "--verify", `origin/${this.config.gitBranch}`], {
|
|
533
|
+
allowFail: true,
|
|
534
|
+
});
|
|
535
|
+
if (remoteRevRes.code === 0) {
|
|
536
|
+
const remoteTip = remoteRevRes.stdout.trim();
|
|
537
|
+
if (await this.isRemoteAncestorHalted(remoteTip)) {
|
|
538
|
+
pushed = false;
|
|
539
|
+
warnings.push(this.lastError ?? REMOTE_REWRITTEN_MSG);
|
|
540
|
+
break;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
const rebaseRes = await this.git.run(["rebase", `origin/${this.config.gitBranch}`], {
|
|
544
|
+
allowFail: true,
|
|
545
|
+
});
|
|
546
|
+
if (rebaseRes.code === 0) {
|
|
547
|
+
const post = (await this.git.run(["rev-parse", "HEAD"])).stdout.trim();
|
|
548
|
+
const diffRes = await this.git.run(["diff", "--name-only", pre, post]);
|
|
549
|
+
await this.resync(extractChangedProjects(diffRes.stdout));
|
|
550
|
+
}
|
|
551
|
+
else {
|
|
552
|
+
await this.git.run(["rebase", "--abort"], { allowFail: true });
|
|
553
|
+
await this.git.run(["reset", "--hard", `origin/${this.config.gitBranch}`]);
|
|
554
|
+
await this.resync(spec.projects);
|
|
555
|
+
throw new OkfError("upstream_conflict", 409, "change conflicts with a concurrent upstream edit; re-read and retry");
|
|
556
|
+
}
|
|
420
557
|
}
|
|
421
558
|
else {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
559
|
+
pushed = false;
|
|
560
|
+
const firstLine = pushRes.stderr.split(/\r?\n/)[0] ?? "unknown error";
|
|
561
|
+
warnings.push(`push failed; will retry on next sync: ${firstLine}`);
|
|
562
|
+
break;
|
|
426
563
|
}
|
|
427
564
|
}
|
|
428
|
-
|
|
565
|
+
if (!pushSuccess && pushed === null) {
|
|
429
566
|
pushed = false;
|
|
430
|
-
const firstLine = pushRes.stderr.split(/\r?\n/)[0] ?? "unknown error";
|
|
431
|
-
warnings.push(`push failed; will retry on next sync: ${firstLine}`);
|
|
432
|
-
break;
|
|
433
567
|
}
|
|
434
568
|
}
|
|
435
|
-
if (!pushSuccess && pushed === null) {
|
|
436
|
-
pushed = false;
|
|
437
|
-
}
|
|
438
569
|
}
|
|
439
570
|
return {
|
|
440
571
|
value: workRes.value,
|
|
@@ -463,6 +594,17 @@ export class GitBackend {
|
|
|
463
594
|
this.lastError = fetchRes.stderr.split(/\r?\n/)[0] ?? "fetch failed";
|
|
464
595
|
return this.syncStatus();
|
|
465
596
|
}
|
|
597
|
+
const remoteRevRes = await this.git.run(["rev-parse", "--verify", `origin/${this.config.gitBranch}`], {
|
|
598
|
+
allowFail: true,
|
|
599
|
+
});
|
|
600
|
+
if (remoteRevRes.code !== 0) {
|
|
601
|
+
this.lastError = "remote ref not found";
|
|
602
|
+
return this.syncStatus();
|
|
603
|
+
}
|
|
604
|
+
const remoteTip = remoteRevRes.stdout.trim();
|
|
605
|
+
if (await this.isRemoteAncestorHalted(remoteTip)) {
|
|
606
|
+
return this.syncStatus();
|
|
607
|
+
}
|
|
466
608
|
const behindRes = await this.git.run(["rev-list", "--count", `HEAD..origin/${this.config.gitBranch}`], {
|
|
467
609
|
allowFail: true,
|
|
468
610
|
});
|
|
@@ -491,6 +633,13 @@ export class GitBackend {
|
|
|
491
633
|
const pushError = `push failed: ${pushRes.stderr.split(/\r?\n/)[0] ?? "unknown error"}`;
|
|
492
634
|
runError = runError ? `${runError}; ${pushError}` : pushError;
|
|
493
635
|
}
|
|
636
|
+
else {
|
|
637
|
+
const headSha = (await this.git.run(["rev-parse", "HEAD"])).stdout.trim();
|
|
638
|
+
await this.setRememberedRemoteTip(headSha);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
else {
|
|
642
|
+
await this.setRememberedRemoteTip(remoteTip);
|
|
494
643
|
}
|
|
495
644
|
// lastError describes the most recent sync run only.
|
|
496
645
|
this.lastError = runError;
|
package/dist/store/git.js
CHANGED
|
@@ -1,21 +1,70 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { chmod, copyFile,
|
|
2
|
+
import { chmod, copyFile, mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import { pathExists } from "./fs-util.js";
|
|
5
6
|
export class GitError extends Error {
|
|
6
7
|
args;
|
|
7
8
|
code;
|
|
8
9
|
stderr;
|
|
9
|
-
constructor(args, code, stderr) {
|
|
10
|
-
|
|
10
|
+
constructor(args, code, stderr, configuredRemote) {
|
|
11
|
+
const redactedArgs = redactArgs(args, configuredRemote);
|
|
12
|
+
const redactedStderr = redactStderr(stderr);
|
|
13
|
+
super(`git ${redactedArgs.join(" ")} failed with exit code ${code}:\n${redactedStderr}`);
|
|
11
14
|
this.name = "GitError";
|
|
12
|
-
this.args =
|
|
15
|
+
this.args = redactedArgs;
|
|
13
16
|
this.code = code;
|
|
14
|
-
this.stderr =
|
|
17
|
+
this.stderr = redactedStderr;
|
|
15
18
|
}
|
|
16
19
|
}
|
|
17
20
|
export function redactRemote(url) {
|
|
18
|
-
return url.replace(/(
|
|
21
|
+
return url.replace(/([a-zA-Z][a-zA-Z0-9+.-]*:\/\/)[^/@\s]+@/g, "$1");
|
|
22
|
+
}
|
|
23
|
+
export function redactArgs(args, configuredRemote) {
|
|
24
|
+
const result = [];
|
|
25
|
+
for (let i = 0; i < args.length; i++) {
|
|
26
|
+
const arg = args[i];
|
|
27
|
+
if (arg === undefined) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (arg === "-m") {
|
|
31
|
+
result.push("-m");
|
|
32
|
+
if (i + 1 < args.length) {
|
|
33
|
+
result.push("<message>");
|
|
34
|
+
i++;
|
|
35
|
+
}
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (arg.startsWith("-m") && arg.length > 2) {
|
|
39
|
+
result.push("-m<message>");
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (arg === "--message") {
|
|
43
|
+
result.push("--message");
|
|
44
|
+
if (i + 1 < args.length) {
|
|
45
|
+
result.push("<message>");
|
|
46
|
+
i++;
|
|
47
|
+
}
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (arg.startsWith("--message=")) {
|
|
51
|
+
result.push("--message=<message>");
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if ((configuredRemote && arg === configuredRemote) ||
|
|
55
|
+
arg.includes("://") ||
|
|
56
|
+
arg.startsWith("git@") ||
|
|
57
|
+
/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(arg) ||
|
|
58
|
+
/(https?:\/\/)[^/@\s]+@/.test(arg)) {
|
|
59
|
+
result.push(redactRemote(arg));
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
result.push(arg);
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
export function redactStderr(stderr) {
|
|
67
|
+
return redactRemote(stderr);
|
|
19
68
|
}
|
|
20
69
|
export function isPushRejection(stderr) {
|
|
21
70
|
return stderr.includes("[rejected]") || stderr.includes("non-fast-forward") || stderr.includes("fetch first");
|
|
@@ -50,6 +99,7 @@ export class Git {
|
|
|
50
99
|
homeDir;
|
|
51
100
|
config;
|
|
52
101
|
env;
|
|
102
|
+
sshTempDir;
|
|
53
103
|
baseArgs;
|
|
54
104
|
constructor(repoDir, homeDir, config) {
|
|
55
105
|
this.repoDir = repoDir;
|
|
@@ -99,31 +149,54 @@ export class Git {
|
|
|
99
149
|
"-c",
|
|
100
150
|
"core.autocrlf=false",
|
|
101
151
|
"-c",
|
|
152
|
+
"core.fsmonitor=false",
|
|
153
|
+
"-c",
|
|
154
|
+
"protocol.ext.allow=never",
|
|
155
|
+
"-c",
|
|
102
156
|
"log.showSignature=false",
|
|
103
157
|
];
|
|
158
|
+
if (!config.gitHostEnv) {
|
|
159
|
+
this.baseArgs.push("-c", "core.sshCommand=ssh");
|
|
160
|
+
}
|
|
104
161
|
if (config.gitHttpUsername && config.gitHttpPassword) {
|
|
105
162
|
this.baseArgs.push("-c", "credential.helper=", "-c", "credential.helper=!f() { echo username=$GIT_HTTP_USERNAME; echo password=$GIT_HTTP_PASSWORD; }; f");
|
|
106
163
|
}
|
|
107
164
|
}
|
|
165
|
+
get sshKeyPath() {
|
|
166
|
+
return this.sshTempDir ? join(this.sshTempDir, "id_okf") : undefined;
|
|
167
|
+
}
|
|
168
|
+
async close() {
|
|
169
|
+
if (this.sshTempDir) {
|
|
170
|
+
try {
|
|
171
|
+
await rm(this.sshTempDir, { recursive: true, force: true });
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
this.sshTempDir = undefined;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
108
178
|
async prepareSsh() {
|
|
109
179
|
if (!this.config.gitSshKeyPath) {
|
|
110
180
|
return;
|
|
111
181
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
182
|
+
try {
|
|
183
|
+
await rm(join(this.homeDir, ".ssh", "id_okf"), { force: true });
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
// Ignore if homeDir or .ssh directory does not exist
|
|
187
|
+
}
|
|
188
|
+
if (!this.sshTempDir) {
|
|
189
|
+
this.sshTempDir = await mkdtemp(join(tmpdir(), "okf-ssh-"));
|
|
190
|
+
await chmod(this.sshTempDir, 0o700);
|
|
191
|
+
}
|
|
192
|
+
const targetKeyPath = join(this.sshTempDir, "id_okf");
|
|
115
193
|
await copyFile(this.config.gitSshKeyPath, targetKeyPath);
|
|
116
194
|
await chmod(targetKeyPath, 0o600);
|
|
117
|
-
let sshCmd = `ssh -i "${targetKeyPath}" -o IdentitiesOnly=yes -o BatchMode=yes`;
|
|
118
|
-
// The chart always sets the path; the Secret key is optional, so strict mode requires the file to exist.
|
|
119
195
|
const knownHosts = this.config.gitSshKnownHostsPath;
|
|
120
|
-
if (knownHosts
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
else {
|
|
124
|
-
sshCmd += ` -o UserKnownHostsFile="${join(sshDir, "known_hosts")}" -o StrictHostKeyChecking=accept-new`;
|
|
196
|
+
if (!knownHosts || !(await pathExists(knownHosts))) {
|
|
197
|
+
throw new Error(`Configured known_hosts file does not exist: ${knownHosts}`);
|
|
125
198
|
}
|
|
126
|
-
this.env.GIT_SSH_COMMAND =
|
|
199
|
+
this.env.GIT_SSH_COMMAND = `ssh -i "${targetKeyPath}" -o IdentitiesOnly=yes -o BatchMode=yes -o UserKnownHostsFile="${knownHosts}" -o StrictHostKeyChecking=yes`;
|
|
127
200
|
}
|
|
128
201
|
run(args, opts) {
|
|
129
202
|
const { promise, resolve, reject } = Promise.withResolvers();
|
|
@@ -159,7 +232,7 @@ export class Git {
|
|
|
159
232
|
const stdout = Buffer.concat(stdoutChunks).toString("utf8");
|
|
160
233
|
const stderr = Buffer.concat(stderrChunks).toString("utf8") + extraStderr;
|
|
161
234
|
if (failed && !opts?.allowFail) {
|
|
162
|
-
reject(new GitError(args, code, stderr));
|
|
235
|
+
reject(new GitError(args, code, stderr, this.config.gitRemoteUrl));
|
|
163
236
|
}
|
|
164
237
|
else {
|
|
165
238
|
resolve({ code, stdout, stderr });
|
|
@@ -192,7 +265,7 @@ export class Git {
|
|
|
192
265
|
});
|
|
193
266
|
child.on("close", (code) => {
|
|
194
267
|
if (code !== 0 && code !== null) {
|
|
195
|
-
child.stdout.destroy(new GitError(args, code, stderrBuffer));
|
|
268
|
+
child.stdout.destroy(new GitError(args, code, stderrBuffer, this.config.gitRemoteUrl));
|
|
196
269
|
}
|
|
197
270
|
});
|
|
198
271
|
return child.stdout;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{t as e}from"./shellscript-CqHdAlCp.js";export{e as default};
|