@ferrule-io/ok-fine 0.3.8 → 0.3.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/http/rest.js +14 -0
- package/dist/http/ui.js +47 -33
- package/dist/mcp/server.js +33 -3
- package/dist/okf/log-file.js +3 -0
- package/dist/server.js +8 -2
- package/dist/service/knowledge-service.js +111 -3
- package/dist/store/git-backend.js +248 -52
- package/dist/store/git.js +3 -2
- package/dist/ui/assets/highlight-DkgVr4Tn.js +153 -0
- package/dist/ui/assets/{index-0b2NNRC5.js → index-C2IYVK_7.js} +33 -33
- package/dist/ui/index.html +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -1
- package/dist/ui/assets/highlight-CW19vWvw.js +0 -153
|
@@ -10,6 +10,48 @@ import { Mutex } from "./mutex.js";
|
|
|
10
10
|
import { PathIndex } from "./path-index.js";
|
|
11
11
|
const REMOTE_BASE_REF = "refs/ok-fine/remote-base";
|
|
12
12
|
const REMOTE_REWRITTEN_MSG = "remote history was rewritten; syncing halted. Stop ok-fine, remove DATA_DIR/repo, and restart to re-clone";
|
|
13
|
+
// One ref per (project, conflict): refs/heads/ok-fine/conflict/<project>/<id>. The commit stays reachable while
|
|
14
|
+
// any project's ref remains, so resolving one project never drops another project's preserved writes.
|
|
15
|
+
const CONFLICT_REF_PREFIX = "refs/heads/ok-fine/conflict/";
|
|
16
|
+
// Pre-split format: one branch for all projects. Migrated to per-project refs on open.
|
|
17
|
+
const LEGACY_CONFLICT_REF_PREFIX = "refs/heads/ok-fine/conflict-";
|
|
18
|
+
// Resolved conflicts whose remote branch still has to be deleted.
|
|
19
|
+
const RESOLVED_REF_PREFIX = "refs/ok-fine/resolved/";
|
|
20
|
+
// Holds preserved commits that touch no project directory, so they stay reachable.
|
|
21
|
+
const REPOSITORY_CONFLICT_SCOPE = "_repository";
|
|
22
|
+
/** `<UTC stamp>-<12 hex of the preserved tip>`; legacy stamps have no milliseconds. */
|
|
23
|
+
const CONFLICT_ID_RE = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(\.\d{3})?Z-[0-9a-f]{12}$/;
|
|
24
|
+
const HISTORY_FORMAT = "--format=%H%x1f%aI%x1f%an%x1f%s%x1f%(trailers:key=Okf-Principal,valueonly)%x1e";
|
|
25
|
+
function conflictRef(project, id) {
|
|
26
|
+
return `${CONFLICT_REF_PREFIX}${project}/${id}`;
|
|
27
|
+
}
|
|
28
|
+
function conflictDetectedAt(id) {
|
|
29
|
+
const m = CONFLICT_ID_RE.exec(id);
|
|
30
|
+
if (!m)
|
|
31
|
+
return "";
|
|
32
|
+
return `${m[1]}-${m[2]}-${m[3]}T${m[4]}:${m[5]}:${m[6]}${m[7] ?? ""}Z`;
|
|
33
|
+
}
|
|
34
|
+
/** Server-generated files: rebuilt after every sync, so a conflict on them never needs a client merge. */
|
|
35
|
+
function isGeneratedPath(path) {
|
|
36
|
+
return path === "log.md" || path === "index.md" || path.endsWith("/index.md");
|
|
37
|
+
}
|
|
38
|
+
function parseHistory(stdout) {
|
|
39
|
+
const commits = [];
|
|
40
|
+
for (const entry of stdout.split("\x1e")) {
|
|
41
|
+
if (entry.trim().length === 0)
|
|
42
|
+
continue;
|
|
43
|
+
const parts = entry.trim().split("\x1f");
|
|
44
|
+
const principalRaw = parts[4]?.trim();
|
|
45
|
+
commits.push({
|
|
46
|
+
sha: parts[0] ?? "",
|
|
47
|
+
at: parts[1] ?? "",
|
|
48
|
+
actor: parts[2] ?? "",
|
|
49
|
+
subject: parts[3] ?? "",
|
|
50
|
+
principal: principalRaw && principalRaw.length > 0 ? principalRaw : null,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return commits;
|
|
54
|
+
}
|
|
13
55
|
function extractChangedProjects(diffOutput) {
|
|
14
56
|
const lines = diffOutput
|
|
15
57
|
.split(/\r?\n/)
|
|
@@ -29,6 +71,8 @@ class GitTx {
|
|
|
29
71
|
repoDir;
|
|
30
72
|
index;
|
|
31
73
|
touched = false;
|
|
74
|
+
/** Applied by GitBackend.transaction only after the transaction succeeds. */
|
|
75
|
+
resolutions = [];
|
|
32
76
|
constructor(repoDir, index) {
|
|
33
77
|
this.repoDir = repoDir;
|
|
34
78
|
this.index = index;
|
|
@@ -107,6 +151,11 @@ class GitTx {
|
|
|
107
151
|
this.index.setProject(project, written);
|
|
108
152
|
this.touched = true;
|
|
109
153
|
}
|
|
154
|
+
async resolveConflict(project, id) {
|
|
155
|
+
if (!this.resolutions.some((r) => r.project === project && r.id === id)) {
|
|
156
|
+
this.resolutions.push({ project, id });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
110
159
|
}
|
|
111
160
|
export class GitBackend {
|
|
112
161
|
config;
|
|
@@ -119,6 +168,8 @@ export class GitBackend {
|
|
|
119
168
|
resyncHandler = null;
|
|
120
169
|
lastSyncAt = null;
|
|
121
170
|
lastError = null;
|
|
171
|
+
/** project -> conflict id -> preserved tip. Only this class creates or deletes conflict refs. */
|
|
172
|
+
conflictRefs = new Map();
|
|
122
173
|
constructor(config, log, repoDir, homeDir) {
|
|
123
174
|
this.config = config;
|
|
124
175
|
this.log = log;
|
|
@@ -303,6 +354,8 @@ export class GitBackend {
|
|
|
303
354
|
}
|
|
304
355
|
}
|
|
305
356
|
}
|
|
357
|
+
await repo.migrateLegacyConflicts();
|
|
358
|
+
await repo.loadConflictRefs();
|
|
306
359
|
await repo.reindexAll();
|
|
307
360
|
return repo;
|
|
308
361
|
}
|
|
@@ -419,30 +472,187 @@ export class GitBackend {
|
|
|
419
472
|
const rev = await this.git.run(["rev-parse", "HEAD"]);
|
|
420
473
|
return rev.stdout.trim();
|
|
421
474
|
}
|
|
475
|
+
async mergeBase(a, b) {
|
|
476
|
+
const res = await this.git.run(["merge-base", a, b], { allowFail: true });
|
|
477
|
+
return res.code === 0 ? res.stdout.trim() : null;
|
|
478
|
+
}
|
|
479
|
+
/** Projects whose directories `tip` changed since its merge-base with `against`. */
|
|
480
|
+
async projectsChangedSince(tip, against) {
|
|
481
|
+
const base = await this.mergeBase(tip, against);
|
|
482
|
+
const res = base
|
|
483
|
+
? await this.git.run(["diff", "--name-only", "--no-renames", "-z", base, tip])
|
|
484
|
+
: await this.git.run(["ls-tree", "-r", "--name-only", "-z", tip]);
|
|
485
|
+
const projects = new Set();
|
|
486
|
+
for (const path of res.stdout.split("\0")) {
|
|
487
|
+
const slash = path.indexOf("/");
|
|
488
|
+
if (slash > 0 && PROJECT_RE.test(path.slice(0, slash)))
|
|
489
|
+
projects.add(path.slice(0, slash));
|
|
490
|
+
}
|
|
491
|
+
return [...projects].sort();
|
|
492
|
+
}
|
|
422
493
|
/**
|
|
423
|
-
* Aborts a failed rebase
|
|
424
|
-
*
|
|
425
|
-
* Returns a human-readable description of where the commits were preserved.
|
|
494
|
+
* Aborts a failed rebase and keeps HEAD as one conflict per touched project (local refs, plus remote branches
|
|
495
|
+
* when the push succeeds), then resets to the remote branch. Never overwrites an existing conflict.
|
|
426
496
|
*/
|
|
427
497
|
async preserveConflict() {
|
|
428
498
|
await this.git.run(["rebase", "--abort"], { allowFail: true });
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
.
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
499
|
+
const upstream = `origin/${this.config.gitBranch}`;
|
|
500
|
+
const tip = (await this.git.run(["rev-parse", "HEAD"])).stdout.trim();
|
|
501
|
+
const projects = await this.projectsChangedSince(tip, upstream);
|
|
502
|
+
if (projects.length === 0)
|
|
503
|
+
projects.push(REPOSITORY_CONFLICT_SCOPE);
|
|
504
|
+
// Millisecond stamp plus tip prefix; update-ref with an empty old value fails rather than overwrite.
|
|
505
|
+
const id = `${new Date().toISOString().replace(/[-:]/g, "")}-${tip.slice(0, 12)}`;
|
|
506
|
+
const conflicts = projects.map((project) => ({ project, id }));
|
|
507
|
+
for (const { project } of conflicts) {
|
|
508
|
+
await this.git.run(["update-ref", conflictRef(project, id), tip, ""]);
|
|
509
|
+
let byId = this.conflictRefs.get(project);
|
|
510
|
+
if (!byId) {
|
|
511
|
+
byId = new Map();
|
|
512
|
+
this.conflictRefs.set(project, byId);
|
|
513
|
+
}
|
|
514
|
+
byId.set(id, tip);
|
|
515
|
+
}
|
|
516
|
+
const pushRes = await this.git.run(["push", "origin", ...conflicts.map(({ project }) => `${conflictRef(project, id)}:${conflictRef(project, id)}`)], { allowFail: true });
|
|
517
|
+
await this.git.run(["reset", "--hard", upstream]);
|
|
518
|
+
let message = `local commits preserved as conflict ${id} in ${projects.join(", ")}`;
|
|
440
519
|
if (pushRes.code !== 0) {
|
|
441
520
|
const reason = pushRes.stderr.trim().split("\n")[0] ?? "";
|
|
442
|
-
message += ` (
|
|
521
|
+
message += ` (kept locally only; pushing the conflict branches failed: ${reason})`;
|
|
443
522
|
}
|
|
444
523
|
this.log.error({ stderr: pushRes.code !== 0 ? pushRes.stderr : undefined }, `rebase conflict; ${message}`);
|
|
445
|
-
return message;
|
|
524
|
+
return { message, conflicts };
|
|
525
|
+
}
|
|
526
|
+
/** Splits single-branch `ok-fine/conflict-<stamp>` conflicts into per-project refs. Remote copies are kept. */
|
|
527
|
+
async migrateLegacyConflicts() {
|
|
528
|
+
const res = await this.git.run([
|
|
529
|
+
"for-each-ref",
|
|
530
|
+
"--format=%(objectname) %(refname)",
|
|
531
|
+
`${LEGACY_CONFLICT_REF_PREFIX}*`,
|
|
532
|
+
]);
|
|
533
|
+
for (const line of res.stdout.split("\n")) {
|
|
534
|
+
const [tip, ref] = line.split(" ");
|
|
535
|
+
if (!tip || !ref)
|
|
536
|
+
continue;
|
|
537
|
+
const id = `${ref.slice(LEGACY_CONFLICT_REF_PREFIX.length)}-${tip.slice(0, 12)}`;
|
|
538
|
+
if (!CONFLICT_ID_RE.test(id)) {
|
|
539
|
+
this.log.warn({ ref }, "leaving unrecognized legacy conflict branch in place");
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
const projects = await this.projectsChangedSince(tip, "HEAD");
|
|
543
|
+
if (projects.length === 0)
|
|
544
|
+
projects.push(REPOSITORY_CONFLICT_SCOPE);
|
|
545
|
+
for (const project of projects) {
|
|
546
|
+
const target = conflictRef(project, id);
|
|
547
|
+
const exists = await this.git.run(["rev-parse", "--verify", "--quiet", target], { allowFail: true });
|
|
548
|
+
if (exists.code !== 0)
|
|
549
|
+
await this.git.run(["update-ref", target, tip, ""]);
|
|
550
|
+
}
|
|
551
|
+
await this.git.run(["update-ref", "-d", ref, tip]);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
async loadConflictRefs() {
|
|
555
|
+
this.conflictRefs.clear();
|
|
556
|
+
const res = await this.git.run(["for-each-ref", "--format=%(objectname) %(refname)", CONFLICT_REF_PREFIX]);
|
|
557
|
+
for (const line of res.stdout.split("\n")) {
|
|
558
|
+
const [tip, ref] = line.split(" ");
|
|
559
|
+
if (!tip || !ref)
|
|
560
|
+
continue;
|
|
561
|
+
const [project, id, ...rest] = ref.slice(CONFLICT_REF_PREFIX.length).split("/");
|
|
562
|
+
if (!project || !id || rest.length > 0 || !CONFLICT_ID_RE.test(id))
|
|
563
|
+
continue;
|
|
564
|
+
let byId = this.conflictRefs.get(project);
|
|
565
|
+
if (!byId) {
|
|
566
|
+
byId = new Map();
|
|
567
|
+
this.conflictRefs.set(project, byId);
|
|
568
|
+
}
|
|
569
|
+
byId.set(id, tip);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
/** Drops resolved conflicts locally; with a remote, keeps a marker until the remote branch is deleted too. */
|
|
573
|
+
async applyResolutions(resolutions) {
|
|
574
|
+
for (const { project, id } of resolutions) {
|
|
575
|
+
const byId = this.conflictRefs.get(project);
|
|
576
|
+
const tip = byId?.get(id);
|
|
577
|
+
if (!byId || !tip)
|
|
578
|
+
continue;
|
|
579
|
+
if (this.hasRemote)
|
|
580
|
+
await this.git.run(["update-ref", `${RESOLVED_REF_PREFIX}${project}/${id}`, tip]);
|
|
581
|
+
await this.git.run(["update-ref", "-d", conflictRef(project, id), tip]);
|
|
582
|
+
byId.delete(id);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
/** Deletes remote branches of resolved conflicts; returns one message per deletion still pending. */
|
|
586
|
+
async pushResolutions() {
|
|
587
|
+
const res = await this.git.run(["for-each-ref", "--format=%(refname)", RESOLVED_REF_PREFIX]);
|
|
588
|
+
const pending = [];
|
|
589
|
+
for (const marker of res.stdout.split("\n")) {
|
|
590
|
+
if (!marker)
|
|
591
|
+
continue;
|
|
592
|
+
const name = marker.slice(RESOLVED_REF_PREFIX.length);
|
|
593
|
+
const del = await this.git.run(["push", "origin", `:${CONFLICT_REF_PREFIX}${name}`], { allowFail: true });
|
|
594
|
+
// Never pushed (the preserving push failed) counts as deleted.
|
|
595
|
+
if (del.code === 0 || del.stderr.includes("remote ref does not exist")) {
|
|
596
|
+
await this.git.run(["update-ref", "-d", marker]);
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
const reason = del.stderr.trim().split("\n")[0] ?? "";
|
|
600
|
+
pending.push(`conflict ${name} resolved; deleting its remote branch failed, will retry on next sync: ${reason}`);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return pending;
|
|
604
|
+
}
|
|
605
|
+
async conflicts(project) {
|
|
606
|
+
const byId = this.conflictRefs.get(project);
|
|
607
|
+
if (!byId || byId.size === 0)
|
|
608
|
+
return [];
|
|
609
|
+
const scope = `${project}/`;
|
|
610
|
+
const out = [];
|
|
611
|
+
for (const id of [...byId.keys()].sort()) {
|
|
612
|
+
const tip = byId.get(id);
|
|
613
|
+
if (!tip)
|
|
614
|
+
continue;
|
|
615
|
+
const base = await this.mergeBase(tip, "HEAD");
|
|
616
|
+
const files = [];
|
|
617
|
+
if (base) {
|
|
618
|
+
const current = await this.git.run(["diff", "--name-only", "--no-renames", "-z", base, "HEAD", "--", scope]);
|
|
619
|
+
const changedHere = new Set(current.stdout.split("\0"));
|
|
620
|
+
const preserved = await this.git.run(["diff", "--name-status", "--no-renames", "-z", base, tip, "--", scope]);
|
|
621
|
+
// -z name-status output alternates status and path.
|
|
622
|
+
const fields = preserved.stdout.split("\0");
|
|
623
|
+
for (let i = 0; i + 1 < fields.length; i += 2) {
|
|
624
|
+
const status = fields[i] ?? "";
|
|
625
|
+
const full = fields[i + 1] ?? "";
|
|
626
|
+
const path = full.slice(scope.length);
|
|
627
|
+
if (isGeneratedPath(path))
|
|
628
|
+
continue;
|
|
629
|
+
const change = status === "A" ? "added" : status === "D" ? "deleted" : "modified";
|
|
630
|
+
files.push({ path, change, divergent: changedHere.has(full) });
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
else {
|
|
634
|
+
const listed = await this.git.run(["ls-tree", "-r", "--name-only", "-z", tip, "--", scope]);
|
|
635
|
+
for (const full of listed.stdout.split("\0")) {
|
|
636
|
+
const path = full.slice(scope.length);
|
|
637
|
+
if (full.length === 0 || isGeneratedPath(path))
|
|
638
|
+
continue;
|
|
639
|
+
files.push({ path, change: "added", divergent: true });
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
const log = await this.git.run(["log", "-n", "20", HISTORY_FORMAT, base ? `${base}..${tip}` : tip, "--", scope]);
|
|
643
|
+
out.push({ id, detectedAt: conflictDetectedAt(id), files, commits: parseHistory(log.stdout) });
|
|
644
|
+
}
|
|
645
|
+
return out;
|
|
646
|
+
}
|
|
647
|
+
async readConflictFile(project, id, side, path) {
|
|
648
|
+
const tip = this.conflictRefs.get(project)?.get(id);
|
|
649
|
+
if (!tip)
|
|
650
|
+
return null;
|
|
651
|
+
const rev = side === "preserved" ? tip : await this.mergeBase(tip, "HEAD");
|
|
652
|
+
if (!rev)
|
|
653
|
+
return null;
|
|
654
|
+
const res = await this.git.run(["cat-file", "blob", `${rev}:${project}/${path}`], { allowFail: true });
|
|
655
|
+
return res.code === 0 ? res.stdoutBytes : null;
|
|
446
656
|
}
|
|
447
657
|
async transaction(spec, work) {
|
|
448
658
|
return this.mutex.run(async () => {
|
|
@@ -475,7 +685,7 @@ export class GitBackend {
|
|
|
475
685
|
const before = (await this.git.run(["rev-parse", "HEAD"])).stdout.trim();
|
|
476
686
|
const rebaseRes = await this.git.run(["rebase", `origin/${this.config.gitBranch}`], { allowFail: true });
|
|
477
687
|
if (rebaseRes.code !== 0) {
|
|
478
|
-
warnings.push(await this.preserveConflict());
|
|
688
|
+
warnings.push((await this.preserveConflict()).message);
|
|
479
689
|
}
|
|
480
690
|
const after = (await this.git.run(["rev-parse", "HEAD"])).stdout.trim();
|
|
481
691
|
if (before !== after) {
|
|
@@ -488,7 +698,8 @@ export class GitBackend {
|
|
|
488
698
|
}
|
|
489
699
|
// Step 2 & 3: Run work and commit
|
|
490
700
|
try {
|
|
491
|
-
const
|
|
701
|
+
const tx = new GitTx(this.repoDir, this.index);
|
|
702
|
+
const workRes = await work(tx);
|
|
492
703
|
let commitSha = null;
|
|
493
704
|
if (workRes.commit) {
|
|
494
705
|
const addArgs = spec.projects.length > 0 ? ["add", "-A", "--", ...spec.projects] : ["add", "-A"];
|
|
@@ -549,10 +760,12 @@ export class GitBackend {
|
|
|
549
760
|
await this.resync(extractChangedProjects(diffRes.stdout));
|
|
550
761
|
}
|
|
551
762
|
else {
|
|
552
|
-
|
|
553
|
-
await this.
|
|
554
|
-
await this.
|
|
555
|
-
|
|
763
|
+
// Keep every local commit, including this write and earlier unpushed ones.
|
|
764
|
+
const preserved = await this.preserveConflict();
|
|
765
|
+
const post = (await this.git.run(["rev-parse", "HEAD"])).stdout.trim();
|
|
766
|
+
const diffRes = await this.git.run(["diff", "--name-only", pre, post]);
|
|
767
|
+
await this.resync(extractChangedProjects(diffRes.stdout));
|
|
768
|
+
throw new OkfError("upstream_conflict", 409, `change conflicts with a concurrent upstream edit; ${preserved.message}. Re-read, merge the preserved content, then resolve the conflict`, { conflicts: preserved.conflicts });
|
|
556
769
|
}
|
|
557
770
|
}
|
|
558
771
|
else {
|
|
@@ -567,6 +780,11 @@ export class GitBackend {
|
|
|
567
780
|
}
|
|
568
781
|
}
|
|
569
782
|
}
|
|
783
|
+
if (tx.resolutions.length > 0) {
|
|
784
|
+
await this.applyResolutions(tx.resolutions);
|
|
785
|
+
if (this.hasRemote && !syncHalted)
|
|
786
|
+
warnings.push(...(await this.pushResolutions()));
|
|
787
|
+
}
|
|
570
788
|
return {
|
|
571
789
|
value: workRes.value,
|
|
572
790
|
commit: commitSha,
|
|
@@ -613,7 +831,7 @@ export class GitBackend {
|
|
|
613
831
|
if (behind > 0) {
|
|
614
832
|
const rebaseRes = await this.git.run(["rebase", `origin/${this.config.gitBranch}`], { allowFail: true });
|
|
615
833
|
if (rebaseRes.code !== 0) {
|
|
616
|
-
runError = `rebase conflict; ${await this.preserveConflict()}`;
|
|
834
|
+
runError = `rebase conflict; ${(await this.preserveConflict()).message}`;
|
|
617
835
|
}
|
|
618
836
|
const after = (await this.git.run(["rev-parse", "HEAD"])).stdout.trim();
|
|
619
837
|
if (before !== after) {
|
|
@@ -641,6 +859,11 @@ export class GitBackend {
|
|
|
641
859
|
else {
|
|
642
860
|
await this.setRememberedRemoteTip(remoteTip);
|
|
643
861
|
}
|
|
862
|
+
const pendingResolutions = await this.pushResolutions();
|
|
863
|
+
if (pendingResolutions.length > 0) {
|
|
864
|
+
const joined = pendingResolutions.join("; ");
|
|
865
|
+
runError = runError ? `${runError}; ${joined}` : joined;
|
|
866
|
+
}
|
|
644
867
|
// lastError describes the most recent sync run only.
|
|
645
868
|
this.lastError = runError;
|
|
646
869
|
this.lastSyncAt = nowIso();
|
|
@@ -676,35 +899,8 @@ export class GitBackend {
|
|
|
676
899
|
}
|
|
677
900
|
async history(project, path, limit) {
|
|
678
901
|
const clampedLimit = Math.max(1, Math.min(100, limit));
|
|
679
|
-
const
|
|
680
|
-
|
|
681
|
-
"-n",
|
|
682
|
-
String(clampedLimit),
|
|
683
|
-
"--format=%H%x1f%aI%x1f%an%x1f%s%x1f%(trailers:key=Okf-Principal,valueonly)%x1e",
|
|
684
|
-
];
|
|
685
|
-
args.push("--", path === null ? `${project}/` : `${project}/${path}`);
|
|
686
|
-
const res = await this.git.run(args, { allowFail: true });
|
|
687
|
-
if (res.code !== 0) {
|
|
688
|
-
return [];
|
|
689
|
-
}
|
|
690
|
-
const entries = res.stdout.split("\x1e").filter((e) => e.trim().length > 0);
|
|
691
|
-
const commits = [];
|
|
692
|
-
for (const entry of entries) {
|
|
693
|
-
const parts = entry.trim().split("\x1f");
|
|
694
|
-
const sha = parts[0] ?? "";
|
|
695
|
-
const at = parts[1] ?? "";
|
|
696
|
-
const actor = parts[2] ?? "";
|
|
697
|
-
const subject = parts[3] ?? "";
|
|
698
|
-
const principalRaw = parts[4]?.trim();
|
|
699
|
-
commits.push({
|
|
700
|
-
sha,
|
|
701
|
-
at,
|
|
702
|
-
actor,
|
|
703
|
-
subject,
|
|
704
|
-
principal: principalRaw && principalRaw.length > 0 ? principalRaw : null,
|
|
705
|
-
});
|
|
706
|
-
}
|
|
707
|
-
return commits;
|
|
902
|
+
const res = await this.git.run(["log", "-n", String(clampedLimit), HISTORY_FORMAT, "--", path === null ? `${project}/` : `${project}/${path}`], { allowFail: true });
|
|
903
|
+
return res.code === 0 ? parseHistory(res.stdout) : [];
|
|
708
904
|
}
|
|
709
905
|
archive(project) {
|
|
710
906
|
return this.git.spawnStdout(["archive", "--format=tar.gz", "HEAD", "--", project]);
|
package/dist/store/git.js
CHANGED
|
@@ -229,13 +229,14 @@ export class Git {
|
|
|
229
229
|
if (settled)
|
|
230
230
|
return;
|
|
231
231
|
settled = true;
|
|
232
|
-
const
|
|
232
|
+
const stdoutBytes = Buffer.concat(stdoutChunks);
|
|
233
|
+
const stdout = stdoutBytes.toString("utf8");
|
|
233
234
|
const stderr = Buffer.concat(stderrChunks).toString("utf8") + extraStderr;
|
|
234
235
|
if (failed && !opts?.allowFail) {
|
|
235
236
|
reject(new GitError(args, code, stderr, this.config.gitRemoteUrl));
|
|
236
237
|
}
|
|
237
238
|
else {
|
|
238
|
-
resolve({ code, stdout, stderr });
|
|
239
|
+
resolve({ code, stdout, stdoutBytes, stderr });
|
|
239
240
|
}
|
|
240
241
|
};
|
|
241
242
|
child.on("error", (err) => settle(1, true, err.message));
|