@lizard-build/cli 4.0.72 → 4.0.73
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/commands/git-hosting.js +42 -8
- package/dist/commands/git-hosting.js.map +1 -1
- package/dist/commands/up.js +2 -1
- package/dist/commands/up.js.map +1 -1
- package/dist/lib/checkout.d.ts +46 -4
- package/dist/lib/checkout.js +126 -5
- package/dist/lib/checkout.js.map +1 -1
- package/dist/lib/updater.d.ts +1 -1
- package/dist/lib/updater.js +1 -1
- package/package.json +1 -1
- package/src/commands/git-hosting.ts +45 -9
- package/src/commands/up.ts +2 -1
- package/src/lib/checkout.ts +141 -5
- package/src/lib/updater.ts +1 -1
package/src/lib/checkout.ts
CHANGED
|
@@ -16,10 +16,11 @@ import { gitURL, lg, repoPath, sendRaw } from "./lizard-git.js";
|
|
|
16
16
|
* checkout on this machine (~/.cache/lizard/git; immutable, so it never needs invalidating),
|
|
17
17
|
* and a sparse checkout only ever fetches the folders it covers.
|
|
18
18
|
*
|
|
19
|
-
* `commit`
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
19
|
+
* `commit` records a commit here (pending, with copies of its contents under
|
|
20
|
+
* .lizard-git/objects/); `push` sends every pending commit: the server is asked which contents
|
|
21
|
+
* it lacks, those are uploaded, then one request makes the commits on the branch and moves it
|
|
22
|
+
* once (one CI run, one deploy). If the branch moved since they were made, the server places
|
|
23
|
+
* them on the new tip (merging the same files line by line); only a real conflict comes back.
|
|
23
24
|
*/
|
|
24
25
|
|
|
25
26
|
export const STATE_DIR = ".lizard-git";
|
|
@@ -32,6 +33,21 @@ export interface FileState {
|
|
|
32
33
|
t?: number;
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
/** A commit made here and not pushed yet: its files' states (full contents, by hash). */
|
|
37
|
+
export interface PendingCommit {
|
|
38
|
+
message: string;
|
|
39
|
+
time: number; // unix seconds
|
|
40
|
+
files: ({ path: string; hash: string; mode: string } | { path: string; delete: true })[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface Pending {
|
|
44
|
+
base: string; // the commit the first one was made on ("" in an empty repository)
|
|
45
|
+
newBranch?: boolean; // the branch doesn't exist on the server yet (commit -b): push creates it at base
|
|
46
|
+
fromBranch?: string; // with newBranch: the branch the checkout was on
|
|
47
|
+
commits: PendingCommit[];
|
|
48
|
+
orig: Record<string, FileState | null>; // the touched files' states at base (null: not there), for uncommit
|
|
49
|
+
}
|
|
50
|
+
|
|
35
51
|
export interface Checkout {
|
|
36
52
|
version: 1;
|
|
37
53
|
url: string;
|
|
@@ -41,6 +57,7 @@ export interface Checkout {
|
|
|
41
57
|
root: string; // its root tree
|
|
42
58
|
sparse: string[] | null; // folders (or files) checked out; null: everything
|
|
43
59
|
files: Record<string, FileState>;
|
|
60
|
+
pending?: Pending; // commits made here, not pushed
|
|
44
61
|
}
|
|
45
62
|
|
|
46
63
|
interface WireEntry {
|
|
@@ -393,6 +410,7 @@ export interface SyncResult {
|
|
|
393
410
|
* first, and the server merges.
|
|
394
411
|
*/
|
|
395
412
|
export async function sync(root: string, c: Checkout, toBranch?: string): Promise<SyncResult> {
|
|
413
|
+
needNoPending(c);
|
|
396
414
|
const branch = toBranch ?? c.branch;
|
|
397
415
|
const r = await lg.get<{ id: string; root: string }>(`${repoPath(c.repo)}/resolve?rev=${encodeURIComponent(branch)}`);
|
|
398
416
|
const res: SyncResult = { from: c.base, to: r.id, updated: [], removed: [] };
|
|
@@ -415,7 +433,7 @@ export async function sync(root: string, c: Checkout, toBranch?: string): Promis
|
|
|
415
433
|
return !(l && w && l.h === w.h && l.m === w.m); // the same change on both sides is fine
|
|
416
434
|
});
|
|
417
435
|
if (clash.length) {
|
|
418
|
-
throw new APIError(409, `${branch} changed files you changed too: ${clash.slice(0, 10).join(", ")}${clash.length > 10 ? ", ..." : ""}. Commit
|
|
436
|
+
throw new APIError(409, `${branch} changed files you changed too: ${clash.slice(0, 10).join(", ")}${clash.length > 10 ? ", ..." : ""}. Commit and push them (lizard git commit --push -m "..."): the server merges them line by line.`);
|
|
419
437
|
}
|
|
420
438
|
const blobs = await getBlobs(c.repo, toWrite.filter(([p]) => !dirty.has(p)).map(([, f]) => f.h));
|
|
421
439
|
for (const [p, f] of toWrite) {
|
|
@@ -438,6 +456,124 @@ export async function sync(root: string, c: Checkout, toBranch?: string): Promis
|
|
|
438
456
|
return res;
|
|
439
457
|
}
|
|
440
458
|
|
|
459
|
+
/** Refuse what would move this checkout's base while commits wait to be pushed. */
|
|
460
|
+
export function needNoPending(c: Checkout): void {
|
|
461
|
+
const n = c.pending?.commits.length ?? 0;
|
|
462
|
+
if (n) throw new APIError(409, `${n} commit${n === 1 ? "" : "s"} not pushed yet: lizard git push first (the server puts them on top of others' commits), or lizard git uncommit`);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function objectPath(root: string, h: string): string {
|
|
466
|
+
return join(root, STATE_DIR, "objects", h.slice(0, 2), h);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export interface LocalCommitResult {
|
|
470
|
+
branch: string;
|
|
471
|
+
files: number;
|
|
472
|
+
pending: number; // commits waiting to be pushed, this one included
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** Record a commit of the changed files (all, or those under paths) here; push sends it. */
|
|
476
|
+
export function commitLocal(root: string, c: Checkout, message: string, paths: string[], newBranch?: string): LocalCommitResult {
|
|
477
|
+
const local = scanWorking(root, c);
|
|
478
|
+
const ch = changesOf(c, local);
|
|
479
|
+
const pick = (p: string) => !paths.length || paths.some((x) => p === x || p.startsWith(x.replace(/\/+$/, "") + "/"));
|
|
480
|
+
const changed = [...ch.added, ...ch.modified].filter(pick);
|
|
481
|
+
const deleted = ch.deleted.filter(pick);
|
|
482
|
+
if (!changed.length && !deleted.length) throw new APIError(400, "nothing to commit: no changes");
|
|
483
|
+
if (newBranch && c.pending?.commits.length) throw new APIError(409, "push the commits on this branch before starting another (lizard git push)");
|
|
484
|
+
// Keep the contents as they are now: later edits don't change a commit.
|
|
485
|
+
for (const p of changed) {
|
|
486
|
+
const f = local.get(p)!;
|
|
487
|
+
const dst = objectPath(root, f.h);
|
|
488
|
+
if (existsSync(dst)) continue;
|
|
489
|
+
const full = safeJoin(root, p);
|
|
490
|
+
const content = f.m === "120000" ? Buffer.from(readlinkSync(full)) : readFileSync(full);
|
|
491
|
+
if (sha256(content) !== f.h) throw new APIError(409, `${p} changed while committing; try again`);
|
|
492
|
+
mkdirSync(dirname(dst), { recursive: true });
|
|
493
|
+
writeFileSync(dst + ".tmp", content);
|
|
494
|
+
renameSync(dst + ".tmp", dst);
|
|
495
|
+
}
|
|
496
|
+
if (!c.pending) c.pending = { base: c.base, commits: [], orig: {}, ...(newBranch ? { newBranch: true, fromBranch: c.branch } : {}) };
|
|
497
|
+
for (const p of [...changed, ...deleted]) if (!(p in c.pending.orig)) c.pending.orig[p] = c.files[p] ?? null;
|
|
498
|
+
c.pending.commits.push({
|
|
499
|
+
message,
|
|
500
|
+
time: Math.floor(Date.now() / 1000),
|
|
501
|
+
files: [
|
|
502
|
+
...changed.map((p) => ({ path: p, hash: local.get(p)!.h, mode: local.get(p)!.m })),
|
|
503
|
+
...deleted.map((p) => ({ path: p, delete: true as const })),
|
|
504
|
+
],
|
|
505
|
+
});
|
|
506
|
+
for (const p of changed) c.files[p] = { ...local.get(p)! };
|
|
507
|
+
for (const p of deleted) delete c.files[p];
|
|
508
|
+
if (newBranch) c.branch = newBranch;
|
|
509
|
+
saveCheckout(root, c);
|
|
510
|
+
return { branch: c.branch, files: changed.length + deleted.length, pending: c.pending.commits.length };
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/** Turn the commits not pushed back into changes in the files (their messages are dropped). */
|
|
514
|
+
export function uncommit(root: string, c: Checkout): number {
|
|
515
|
+
const n = c.pending?.commits.length ?? 0;
|
|
516
|
+
if (!c.pending) return 0;
|
|
517
|
+
for (const [p, f] of Object.entries(c.pending.orig)) {
|
|
518
|
+
if (f) c.files[p] = f;
|
|
519
|
+
else delete c.files[p];
|
|
520
|
+
}
|
|
521
|
+
// A branch that never reached the server: back on the one it started from.
|
|
522
|
+
if (c.pending.newBranch && c.pending.fromBranch) c.branch = c.pending.fromBranch;
|
|
523
|
+
c.pending = undefined;
|
|
524
|
+
rmSync(join(root, STATE_DIR, "objects"), { recursive: true, force: true });
|
|
525
|
+
saveCheckout(root, c);
|
|
526
|
+
return n;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
export interface PushResult {
|
|
530
|
+
shas: string[];
|
|
531
|
+
sha: string;
|
|
532
|
+
branch: string;
|
|
533
|
+
uploaded: number;
|
|
534
|
+
sync: SyncResult;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/** Send the commits not pushed: one branch move; then bring in others' commits if there were any. */
|
|
538
|
+
export async function push(root: string, c: Checkout): Promise<PushResult> {
|
|
539
|
+
const pend = c.pending;
|
|
540
|
+
if (!pend?.commits.length) throw new APIError(400, "nothing to push: no commits waiting (lizard git commit -m \"...\")");
|
|
541
|
+
const hashes = [...new Set(pend.commits.flatMap((x) => x.files.flatMap((f) => ("hash" in f ? [f.hash] : []))))];
|
|
542
|
+
const missing = new Set<string>();
|
|
543
|
+
for (let i = 0; i < hashes.length; i += 5000) {
|
|
544
|
+
const r = await lg.post<{ missing: string[] }>(`${repoPath(c.repo)}/blobs/missing`, { hashes: hashes.slice(i, i + 5000) });
|
|
545
|
+
for (const h of r.missing) missing.add(h);
|
|
546
|
+
}
|
|
547
|
+
const list = [...missing];
|
|
548
|
+
let next = 0;
|
|
549
|
+
await Promise.all(Array.from({ length: Math.min(6, list.length) }, async () => {
|
|
550
|
+
while (next < list.length) {
|
|
551
|
+
const h = list[next++];
|
|
552
|
+
const content = readFileSync(objectPath(root, h));
|
|
553
|
+
if (sha256(content) !== h) throw new APIError(409, `.lizard-git/objects/${h.slice(0, 12)} is damaged`);
|
|
554
|
+
await sendRaw("PUT", `${repoPath(c.repo)}/blobs/${h}`, content, { "Content-Type": "application/octet-stream" });
|
|
555
|
+
cachePut("b", h, content);
|
|
556
|
+
}
|
|
557
|
+
}));
|
|
558
|
+
const body: Record<string, unknown> = { branch: c.branch, commits: pend.commits };
|
|
559
|
+
if (pend.newBranch && pend.base) body.createFrom = pend.base;
|
|
560
|
+
else if (pend.base) body.expect = pend.base;
|
|
561
|
+
const r = await lg.post<{ shas: string[]; sha: string; parent?: string; root?: string }>(`${repoPath(c.repo)}/push`, body);
|
|
562
|
+
const from = pend.base;
|
|
563
|
+
c.pending = undefined;
|
|
564
|
+
rmSync(join(root, STATE_DIR, "objects"), { recursive: true, force: true });
|
|
565
|
+
// Nobody else committed meanwhile: the files here are exactly the last commit's.
|
|
566
|
+
if (r.root && (r.parent ?? "") === from) {
|
|
567
|
+
c.base = r.sha;
|
|
568
|
+
c.root = r.root;
|
|
569
|
+
saveCheckout(root, c);
|
|
570
|
+
return { shas: r.shas, sha: r.sha, branch: c.branch, uploaded: missing.size, sync: { from, to: r.sha, updated: [], removed: [] } };
|
|
571
|
+
}
|
|
572
|
+
saveCheckout(root, c);
|
|
573
|
+
const s = await sync(root, c);
|
|
574
|
+
return { shas: r.shas, sha: r.sha, branch: c.branch, uploaded: missing.size, sync: s };
|
|
575
|
+
}
|
|
576
|
+
|
|
441
577
|
export interface CommitResult {
|
|
442
578
|
sha: string;
|
|
443
579
|
branch: string;
|
package/src/lib/updater.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { join, dirname } from "node:path";
|
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
7
7
|
|
|
8
|
-
export const CURRENT_VERSION = "4.0.
|
|
8
|
+
export const CURRENT_VERSION = "4.0.73";
|
|
9
9
|
const RELEASES_API = "https://api.github.com/repos/lizard-build/lizard-cli/releases/latest";
|
|
10
10
|
const RELEASE_BASE = "https://github.com/lizard-build/lizard-cli/releases/latest/download";
|
|
11
11
|
|