@lizard-build/cli 4.0.67 → 4.0.68
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.d.ts +2 -0
- package/dist/commands/git-hosting.js +166 -81
- package/dist/commands/git-hosting.js.map +1 -1
- package/dist/commands/git.js +4 -2
- package/dist/commands/git.js.map +1 -1
- package/dist/index.js +2 -3
- package/dist/index.js.map +1 -1
- package/dist/lib/checkout.d.ts +104 -0
- package/dist/lib/checkout.js +545 -0
- package/dist/lib/checkout.js.map +1 -0
- package/dist/lib/lizard-git.d.ts +3 -6
- package/dist/lib/lizard-git.js +19 -46
- package/dist/lib/lizard-git.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 +164 -75
- package/src/commands/git.ts +3 -2
- package/src/index.ts +2 -3
- package/src/lib/checkout.ts +564 -0
- package/src/lib/lizard-git.ts +18 -43
- package/src/lib/updater.ts +1 -1
- package/test/unit/lizard-git.test.ts +63 -22
package/src/lib/lizard-git.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
3
|
import { apiErrorFrom, clientHeaders, getRequestToken, APIError } from "./api.js";
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -38,6 +38,11 @@ export function gitToken(): string | null {
|
|
|
38
38
|
return process.env.LIZARD_GIT_TOKEN || getRequestToken();
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/** One request with the Lizard Git sign-in; non-2xx answers throw. */
|
|
42
|
+
export function sendRaw(method: string, path: string, body?: Buffer, headers: Record<string, string> = {}): Promise<Response> {
|
|
43
|
+
return send(method, path, body === undefined ? undefined : new Uint8Array(body), headers);
|
|
44
|
+
}
|
|
45
|
+
|
|
41
46
|
async function send(method: string, path: string, body?: BodyInit, headers: Record<string, string> = {}): Promise<Response> {
|
|
42
47
|
const token = gitToken();
|
|
43
48
|
const h: Record<string, string> = { ...clientHeaders(), ...headers };
|
|
@@ -93,35 +98,21 @@ export async function myLogin(): Promise<string> {
|
|
|
93
98
|
return me.login;
|
|
94
99
|
}
|
|
95
100
|
|
|
96
|
-
/**
|
|
97
|
-
/** Every address a Lizard Git remote may use: LIZARD_GIT_URL, or lizard.build/git and the old one. */
|
|
101
|
+
/** Every address Lizard Git has had: LIZARD_GIT_URL, or lizard.build/git and the old one. */
|
|
98
102
|
export function gitURLs(): string[] {
|
|
99
103
|
const u = gitURL();
|
|
100
104
|
return u === DEFAULT_GIT_URL ? [u, OLD_GIT_URL] : [u];
|
|
101
105
|
}
|
|
102
106
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
for (
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
107
|
+
/** owner/name of the Lizard Git checkout this directory is in (.lizard-git/checkout.json), or null. */
|
|
108
|
+
function checkoutRepo(): string | null {
|
|
109
|
+
for (let d = process.cwd(); ; d = dirname(d)) {
|
|
110
|
+
try {
|
|
111
|
+
const c = JSON.parse(readFileSync(join(d, ".lizard-git", "checkout.json"), "utf8"));
|
|
112
|
+
if (typeof c.repo === "string" && /^[^/\s]+\/[^/\s]+$/.test(c.repo)) return c.repo;
|
|
113
|
+
} catch {}
|
|
114
|
+
if (dirname(d) === d) return null;
|
|
110
115
|
}
|
|
111
|
-
return null;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function originRepo(): string | null {
|
|
115
|
-
try {
|
|
116
|
-
const remotes = execFileSync("git", ["remote", "-v"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
117
|
-
const lines = remotes.split("\n").filter((l) => l.includes("(fetch)"));
|
|
118
|
-
const ordered = [...lines.filter((l) => l.startsWith("origin\t")), ...lines];
|
|
119
|
-
for (const l of ordered) {
|
|
120
|
-
const r = repoFromRemote(l.split(/\s+/)[1] ?? "");
|
|
121
|
-
if (r) return r;
|
|
122
|
-
}
|
|
123
|
-
} catch {}
|
|
124
|
-
return null;
|
|
125
116
|
}
|
|
126
117
|
|
|
127
118
|
/**
|
|
@@ -136,9 +127,9 @@ export async function resolveRepo(flag?: string): Promise<string> {
|
|
|
136
127
|
if (ok(f)) return `${await myLogin()}/${f}`;
|
|
137
128
|
throw new APIError(400, `--repo is owner/name or name, got "${flag}"`);
|
|
138
129
|
}
|
|
139
|
-
const r =
|
|
130
|
+
const r = checkoutRepo();
|
|
140
131
|
if (r) return r;
|
|
141
|
-
throw new APIError(400, `Which repository? Pass --repo owner/name, or run this inside a
|
|
132
|
+
throw new APIError(400, `Which repository? Pass --repo owner/name, or run this inside a checkout (lizard git clone owner/name).`);
|
|
142
133
|
}
|
|
143
134
|
|
|
144
135
|
export function repoPath(repo: string): string {
|
|
@@ -146,22 +137,6 @@ export function repoPath(repo: string): string {
|
|
|
146
137
|
return `/api/repos/${seg(owner)}/${seg(name)}`;
|
|
147
138
|
}
|
|
148
139
|
|
|
149
|
-
export function cloneURL(repo: string): string {
|
|
150
|
-
return `${gitURL()}/${repo}.git`;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/** How git should run this CLI as its credential helper. */
|
|
154
|
-
export function helperCommand(): string {
|
|
155
|
-
// `npm i -g` runs us through a bin symlink with no .js name (/usr/local/bin/lizard): resolve it,
|
|
156
|
-
// or the helper would be `node git credential`, which can't start.
|
|
157
|
-
let script = process.argv[1] ?? "";
|
|
158
|
-
try { script = realpathSync(script); } catch { /* keep argv[1] */ }
|
|
159
|
-
const q = (s: string) => `'${s.replace(/'/g, `'\\''`)}'`;
|
|
160
|
-
// npm install: node + the script. Standalone binary: the binary itself.
|
|
161
|
-
if (/\.(c|m)?js$/.test(script)) return `!${q(process.execPath)} ${q(script)} git credential`;
|
|
162
|
-
return `!${q(process.execPath)} git credential`;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
140
|
/** Seconds (Lizard Git) to "3m ago". */
|
|
166
141
|
export function ago(sec: number | null | undefined): string {
|
|
167
142
|
if (!sec) return "";
|
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.68";
|
|
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
|
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { describe, it, expect, afterEach, vi } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import { mkdtempSync, mkdirSync, writeFileSync, symlinkSync, unlinkSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { qs, gitURL, resolveRepo, lg, segs } from "../../src/lib/lizard-git.js";
|
|
6
|
+
import { changesOf, scanWorking, saveCheckout, findCheckout, sha256, unifiedDiff, type Checkout } from "../../src/lib/checkout.js";
|
|
3
7
|
|
|
4
8
|
afterEach(() => {
|
|
5
9
|
vi.unstubAllGlobals();
|
|
@@ -7,18 +11,6 @@ afterEach(() => {
|
|
|
7
11
|
});
|
|
8
12
|
|
|
9
13
|
describe("lizard git helpers", () => {
|
|
10
|
-
it("reads owner/name from Lizard Git remotes only", () => {
|
|
11
|
-
expect(repoFromRemote("https://lizard.build/git/alice/app.git")).toBe("alice/app");
|
|
12
|
-
expect(repoFromRemote("https://lizard:liz_x@lizard.build/git/alice/app")).toBe("alice/app");
|
|
13
|
-
expect(repoFromRemote("https://git.lizard.build/alice/app.git")).toBe("alice/app"); // older clones
|
|
14
|
-
expect(repoFromRemote("https://lizard:liz_x@git.lizard.build/alice/app")).toBe("alice/app");
|
|
15
|
-
expect(repoFromRemote("https://github.com/alice/app.git")).toBeNull();
|
|
16
|
-
expect(repoFromRemote("git@github.com:alice/app.git")).toBeNull();
|
|
17
|
-
expect(repoFromRemote("https://lizard.build/alice/app.git")).toBeNull();
|
|
18
|
-
expect(repoFromRemote("https://lizard.build/gitx/alice/app.git")).toBeNull();
|
|
19
|
-
expect(repoFromRemote("https://lizard.build/git/alice/app/extra.git")).toBeNull();
|
|
20
|
-
});
|
|
21
|
-
|
|
22
14
|
it("drops empty query values", () => {
|
|
23
15
|
expect(qs("/x", { a: "1", b: undefined, c: "", d: false, e: 2 })).toBe("/x?a=1&e=2");
|
|
24
16
|
});
|
|
@@ -26,12 +18,6 @@ describe("lizard git helpers", () => {
|
|
|
26
18
|
it("honours LIZARD_GIT_URL", () => {
|
|
27
19
|
vi.stubEnv("LIZARD_GIT_URL", "http://localhost:8080/");
|
|
28
20
|
expect(gitURL()).toBe("http://localhost:8080");
|
|
29
|
-
expect(repoFromRemote("http://localhost:8080/a/b.git")).toBe("a/b");
|
|
30
|
-
expect(repoFromRemote("https://git.lizard.build/a/b.git")).toBeNull(); // another server: only it
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it("runs itself as git's credential helper", () => {
|
|
34
|
-
expect(helperCommand()).toMatch(/^!'.+' .*git credential$/);
|
|
35
21
|
});
|
|
36
22
|
|
|
37
23
|
it("takes owner/name or a bare name (yours) for --repo", async () => {
|
|
@@ -66,8 +52,63 @@ describe("lizard git safety", () => {
|
|
|
66
52
|
await expect(resolveRepo("a/..")).rejects.toThrow();
|
|
67
53
|
expect(() => segs("../../api/user/tokens")).toThrow(/bad path/);
|
|
68
54
|
expect(segs("src/a b.js")).toBe("src/a%20b.js");
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("lizard git checkouts", () => {
|
|
59
|
+
const make = (files: Record<string, string>, state: Partial<Checkout> = {}) => {
|
|
60
|
+
const root = mkdtempSync(join(tmpdir(), "lzg-"));
|
|
61
|
+
const c: Checkout = { version: 1, url: "x", repo: "alice/app", branch: "main", base: "b".repeat(64), root: "r".repeat(64), sparse: null, files: {}, ...state };
|
|
62
|
+
for (const [p, body] of Object.entries(files)) {
|
|
63
|
+
mkdirSync(join(root, p, ".."), { recursive: true });
|
|
64
|
+
writeFileSync(join(root, p), body);
|
|
65
|
+
c.files[p] = { h: sha256(body), m: "100644" };
|
|
66
|
+
}
|
|
67
|
+
saveCheckout(root, c);
|
|
68
|
+
return { root, c };
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
it("finds its checkout from a subdirectory and the repository from it", async () => {
|
|
72
|
+
const { root } = make({ "src/a.ts": "a" });
|
|
73
|
+
expect(findCheckout(join(root, "src"))).toBe(root);
|
|
74
|
+
const cwd = process.cwd();
|
|
75
|
+
process.chdir(join(root, "src"));
|
|
76
|
+
try {
|
|
77
|
+
expect(await resolveRepo()).toBe("alice/app");
|
|
78
|
+
} finally {
|
|
79
|
+
process.chdir(cwd);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("sees added, modified and deleted files, and nothing it shouldn't", () => {
|
|
84
|
+
const { root, c } = make({ "a.txt": "a", "src/b.txt": "b", "gone.txt": "g" });
|
|
85
|
+
writeFileSync(join(root, "a.txt"), "a2");
|
|
86
|
+
writeFileSync(join(root, "new.txt"), "n");
|
|
87
|
+
mkdirSync(join(root, ".lizard/workflows"), { recursive: true }); // a repository's CI: content like any other
|
|
88
|
+
writeFileSync(join(root, ".lizard/workflows/ci.yml"), "on: push\n");
|
|
89
|
+
writeFileSync(join(root, ".gitignore"), "*.log\nbuild/\n");
|
|
90
|
+
writeFileSync(join(root, "debug.log"), "x");
|
|
91
|
+
mkdirSync(join(root, "build"));
|
|
92
|
+
writeFileSync(join(root, "build/out.js"), "x");
|
|
93
|
+
mkdirSync(join(root, "node_modules/x"), { recursive: true });
|
|
94
|
+
writeFileSync(join(root, "node_modules/x/i.js"), "x");
|
|
95
|
+
symlinkSync("a.txt", join(root, "link"));
|
|
96
|
+
unlinkSync(join(root, "gone.txt"));
|
|
97
|
+
const ch = changesOf(c, scanWorking(root, c));
|
|
98
|
+
expect(ch).toEqual({ added: [".gitignore", ".lizard/workflows/ci.yml", "link", "new.txt"], modified: ["a.txt"], deleted: ["gone.txt"] });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("a sparse checkout only looks inside its folders", () => {
|
|
102
|
+
const { root, c } = make({ "src/a.txt": "a" }, { sparse: ["src"] });
|
|
103
|
+
writeFileSync(join(root, "outside.txt"), "o");
|
|
104
|
+
writeFileSync(join(root, "src/b.txt"), "b");
|
|
105
|
+
expect(changesOf(c, scanWorking(root, c)).added).toEqual(["src/b.txt"]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("diffs lines", () => {
|
|
109
|
+
const d = unifiedDiff("a\nb\nc\n", "a\nB\nc\n", "f");
|
|
110
|
+
expect(d).toContain("@@ -1,4 +1,4 @@");
|
|
111
|
+
expect(d).toContain("-b");
|
|
112
|
+
expect(d).toContain("+B");
|
|
72
113
|
});
|
|
73
114
|
});
|