@micacms/runtime 0.4.1 → 0.5.0-rc.2
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 +2 -2
- package/dist/app.d.ts +8 -37
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +7 -42
- package/dist/app.js.map +1 -1
- package/dist/config.d.ts +12 -82
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +14 -82
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +37 -66
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +24 -75
- package/dist/context.js.map +1 -1
- package/dist/http.d.ts +8 -16
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +6 -13
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +13 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -14
- package/dist/index.js.map +1 -1
- package/dist/lib/errors.js.map +1 -1
- package/dist/lib/file-type.js.map +1 -1
- package/dist/lib/id.js.map +1 -1
- package/dist/lib/logger.js.map +1 -1
- package/dist/lib/ports.js.map +1 -1
- package/dist/lib/rate-limit.js.map +1 -1
- package/dist/lib/schema-stats.js.map +1 -1
- package/dist/lib/version.js.map +1 -1
- package/dist/middleware/index.js.map +1 -1
- package/dist/routes/admin-content.d.ts +4 -17
- package/dist/routes/admin-content.d.ts.map +1 -1
- package/dist/routes/admin-content.js +11 -51
- package/dist/routes/admin-content.js.map +1 -1
- package/dist/routes/admin-media.d.ts +3 -14
- package/dist/routes/admin-media.d.ts.map +1 -1
- package/dist/routes/admin-media.js +7 -29
- package/dist/routes/admin-media.js.map +1 -1
- package/dist/routes/admin-spa.js.map +1 -1
- package/dist/routes/admin-staging.d.ts +4 -9
- package/dist/routes/admin-staging.d.ts.map +1 -1
- package/dist/routes/admin-staging.js +31 -18
- package/dist/routes/admin-staging.js.map +1 -1
- package/dist/routes/admin-system.d.ts +2 -8
- package/dist/routes/admin-system.d.ts.map +1 -1
- package/dist/routes/admin-system.js +9 -44
- package/dist/routes/admin-system.js.map +1 -1
- package/dist/routes/health.d.ts +3 -10
- package/dist/routes/health.d.ts.map +1 -1
- package/dist/routes/health.js +9 -26
- package/dist/routes/health.js.map +1 -1
- package/dist/schema.d.ts +10 -38
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +14 -65
- package/dist/schema.js.map +1 -1
- package/dist/server.d.ts +2 -10
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +3 -9
- package/dist/server.js.map +1 -1
- package/dist/services/changes.d.ts +150 -0
- package/dist/services/changes.d.ts.map +1 -0
- package/dist/services/changes.js +339 -0
- package/dist/services/changes.js.map +1 -0
- package/dist/services/content-store.d.ts +61 -0
- package/dist/services/content-store.d.ts.map +1 -0
- package/dist/services/content-store.js +330 -0
- package/dist/services/content-store.js.map +1 -0
- package/dist/services/media.d.ts +9 -29
- package/dist/services/media.d.ts.map +1 -1
- package/dist/services/media.js +17 -57
- package/dist/services/media.js.map +1 -1
- package/dist/services/staging.d.ts +35 -167
- package/dist/services/staging.d.ts.map +1 -1
- package/dist/services/staging.js +195 -458
- package/dist/services/staging.js.map +1 -1
- package/dist/workspace/developer.d.ts +14 -0
- package/dist/workspace/developer.d.ts.map +1 -0
- package/dist/workspace/developer.js +31 -0
- package/dist/workspace/developer.js.map +1 -0
- package/dist/workspace/files.d.ts +70 -0
- package/dist/workspace/files.d.ts.map +1 -0
- package/dist/workspace/files.js +176 -0
- package/dist/workspace/files.js.map +1 -0
- package/dist/workspace/git.d.ts +45 -0
- package/dist/workspace/git.d.ts.map +1 -0
- package/dist/workspace/git.js +68 -0
- package/dist/workspace/git.js.map +1 -0
- package/dist/workspace/locks.d.ts +33 -0
- package/dist/workspace/locks.d.ts.map +1 -0
- package/dist/workspace/locks.js +76 -0
- package/dist/workspace/locks.js.map +1 -0
- package/dist/workspace/policy.d.ts +92 -0
- package/dist/workspace/policy.d.ts.map +1 -0
- package/dist/workspace/policy.js +7 -0
- package/dist/workspace/policy.js.map +1 -0
- package/dist/workspace/tree.d.ts +73 -0
- package/dist/workspace/tree.d.ts.map +1 -0
- package/dist/workspace/tree.js +229 -0
- package/dist/workspace/tree.js.map +1 -0
- package/package.json +2 -3
- package/dist/db/driver.d.ts +0 -35
- package/dist/db/driver.d.ts.map +0 -1
- package/dist/db/driver.js +0 -95
- package/dist/db/driver.js.map +0 -1
- package/dist/lib/git.d.ts +0 -29
- package/dist/lib/git.d.ts.map +0 -1
- package/dist/lib/git.js +0 -62
- package/dist/lib/git.js.map +0 -1
- package/dist/services/staging-local.d.ts +0 -58
- package/dist/services/staging-local.d.ts.map +0 -1
- package/dist/services/staging-local.js +0 -391
- package/dist/services/staging-local.js.map +0 -1
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two locks of a shared working tree: `remote` (fetch and push, one at a
|
|
3
|
+
* time) and `tree` (read/write; saves, discards, commits and fast-forwards
|
|
4
|
+
* write). A save during a commit could otherwise put half an entry into the
|
|
5
|
+
* commit, and a read during a fast-forward could see a half-written file.
|
|
6
|
+
*
|
|
7
|
+
* Both wait, they do not refuse. Take `remote` first, then `tree`, so two
|
|
8
|
+
* operations never wait on each other. No network call may run while `tree` is
|
|
9
|
+
* held: a slow push must not block autosave.
|
|
10
|
+
*/
|
|
11
|
+
export function createWorkspaceLocks() {
|
|
12
|
+
return { remote: createMutex(), tree: createReadWriteLock() };
|
|
13
|
+
}
|
|
14
|
+
export function createMutex() {
|
|
15
|
+
const lock = createReadWriteLock();
|
|
16
|
+
return { run: (fn) => lock.write(fn) };
|
|
17
|
+
}
|
|
18
|
+
export function createReadWriteLock() {
|
|
19
|
+
let readers = 0;
|
|
20
|
+
let writing = false;
|
|
21
|
+
const queue = [];
|
|
22
|
+
const admit = () => {
|
|
23
|
+
while (queue.length > 0) {
|
|
24
|
+
const next = queue[0];
|
|
25
|
+
if (next.kind === "write") {
|
|
26
|
+
if (writing || readers > 0)
|
|
27
|
+
return;
|
|
28
|
+
queue.shift();
|
|
29
|
+
writing = true;
|
|
30
|
+
next.start();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (writing)
|
|
34
|
+
return;
|
|
35
|
+
queue.shift();
|
|
36
|
+
readers += 1;
|
|
37
|
+
next.start();
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const acquire = (kind) => new Promise((resolve) => {
|
|
41
|
+
queue.push({ kind, start: resolve });
|
|
42
|
+
admit();
|
|
43
|
+
});
|
|
44
|
+
const release = (kind) => {
|
|
45
|
+
if (kind === "write")
|
|
46
|
+
writing = false;
|
|
47
|
+
else
|
|
48
|
+
readers -= 1;
|
|
49
|
+
admit();
|
|
50
|
+
};
|
|
51
|
+
const hold = (kind) => async (fn) => {
|
|
52
|
+
await acquire(kind);
|
|
53
|
+
try {
|
|
54
|
+
return await fn();
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
release(kind);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
return { read: hold("read"), write: hold("write") };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Files behind the tree lock. The staging service does not use it: it holds the
|
|
64
|
+
* lock around whole operations.
|
|
65
|
+
*/
|
|
66
|
+
export function lockedFiles(files, tree) {
|
|
67
|
+
return {
|
|
68
|
+
origin: files.origin,
|
|
69
|
+
read: (path) => tree.read(() => files.read(path)),
|
|
70
|
+
list: (path) => tree.read(() => files.list(path)),
|
|
71
|
+
readBinary: (path) => tree.read(() => files.readBinary(path)),
|
|
72
|
+
revision: (path) => tree.read(() => files.revision(path)),
|
|
73
|
+
apply: (input) => tree.write(() => files.apply(input)),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=locks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"locks.js","sourceRoot":"","sources":["../../src/workspace/locks.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The two locks of a shared working tree: `remote` (fetch and push, one at a\n * time) and `tree` (read/write; saves, discards, commits and fast-forwards\n * write). A save during a commit could otherwise put half an entry into the\n * commit, and a read during a fast-forward could see a half-written file.\n *\n * Both wait, they do not refuse. Take `remote` first, then `tree`, so two\n * operations never wait on each other. No network call may run while `tree` is\n * held: a slow push must not block autosave.\n */\n\nimport type { ContentFiles } from \"./files.js\";\n\n/** Runs one function at a time, in arrival order. */\nexport interface Mutex {\n run<T>(fn: () => Promise<T>): Promise<T>;\n}\n\n/** Many readers or one writer, in arrival order. A waiting writer holds back later readers. */\nexport interface ReadWriteLock {\n read<T>(fn: () => Promise<T>): Promise<T>;\n write<T>(fn: () => Promise<T>): Promise<T>;\n}\n\nexport interface WorkspaceLocks {\n remote: Mutex;\n tree: ReadWriteLock;\n}\n\nexport function createWorkspaceLocks(): WorkspaceLocks {\n return { remote: createMutex(), tree: createReadWriteLock() };\n}\n\nexport function createMutex(): Mutex {\n const lock = createReadWriteLock();\n return { run: (fn) => lock.write(fn) };\n}\n\nexport function createReadWriteLock(): ReadWriteLock {\n let readers = 0;\n let writing = false;\n const queue: Array<{ kind: \"read\" | \"write\"; start: () => void }> = [];\n\n const admit = (): void => {\n while (queue.length > 0) {\n const next = queue[0]!;\n if (next.kind === \"write\") {\n if (writing || readers > 0) return;\n queue.shift();\n writing = true;\n next.start();\n return;\n }\n if (writing) return;\n queue.shift();\n readers += 1;\n next.start();\n }\n };\n\n const acquire = (kind: \"read\" | \"write\"): Promise<void> =>\n new Promise((resolve) => {\n queue.push({ kind, start: resolve });\n admit();\n });\n\n const release = (kind: \"read\" | \"write\"): void => {\n if (kind === \"write\") writing = false;\n else readers -= 1;\n admit();\n };\n\n const hold =\n (kind: \"read\" | \"write\") =>\n async <T>(fn: () => Promise<T>): Promise<T> => {\n await acquire(kind);\n try {\n return await fn();\n } finally {\n release(kind);\n }\n };\n\n return { read: hold(\"read\"), write: hold(\"write\") };\n}\n\n/**\n * Files behind the tree lock. The staging service does not use it: it holds the\n * lock around whole operations.\n */\nexport function lockedFiles(\n files: ContentFiles,\n tree: ReadWriteLock,\n): ContentFiles {\n return {\n origin: files.origin,\n read: (path) => tree.read(() => files.read(path)),\n list: (path) => tree.read(() => files.list(path)),\n readBinary: (path) => tree.read(() => files.readBinary(path)),\n revision: (path) => tree.read(() => files.revision(path)),\n apply: (input) => tree.write(() => files.apply(input)),\n };\n}\n"]}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What changes with the owner of a working tree: whether Publish may run and
|
|
3
|
+
* how a commit reaches the remote. The app that keeps a managed clone supplies
|
|
4
|
+
* its policy; `developer.ts` is the developer's.
|
|
5
|
+
*/
|
|
6
|
+
import type { PublishResult } from "@micacms/core";
|
|
7
|
+
import type { Author } from "./files.js";
|
|
8
|
+
import type { GitTree } from "./tree.js";
|
|
9
|
+
export type PublishAvailability = {
|
|
10
|
+
enabled: true;
|
|
11
|
+
} | {
|
|
12
|
+
enabled: false;
|
|
13
|
+
reason: string;
|
|
14
|
+
};
|
|
15
|
+
/** One piece of content that an editor changed and the website changed too. */
|
|
16
|
+
export interface SyncConflict {
|
|
17
|
+
/** Identifies the group in `resolve`. */
|
|
18
|
+
group: string;
|
|
19
|
+
label: string;
|
|
20
|
+
/** Content-relative paths of the group, on either side. */
|
|
21
|
+
paths: string[];
|
|
22
|
+
upstream: "changed" | "deleted" | "added";
|
|
23
|
+
/** What the comparison shows, when the group is one document. */
|
|
24
|
+
target?: {
|
|
25
|
+
kind: "entry";
|
|
26
|
+
collection: string;
|
|
27
|
+
slug: string;
|
|
28
|
+
} | {
|
|
29
|
+
kind: "singleton";
|
|
30
|
+
name: string;
|
|
31
|
+
} | {
|
|
32
|
+
kind: "order";
|
|
33
|
+
collection: string;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** Where a clone that Mica keeps stands against its remote. Managed owners only. */
|
|
37
|
+
export interface SyncStatus {
|
|
38
|
+
lastSyncAt: string | null;
|
|
39
|
+
/** Upstream commits not yet in the clone. */
|
|
40
|
+
behind: number;
|
|
41
|
+
error: {
|
|
42
|
+
code: string;
|
|
43
|
+
message: string;
|
|
44
|
+
since: string;
|
|
45
|
+
} | null;
|
|
46
|
+
/** The content model changed upstream; a restart applies it. */
|
|
47
|
+
restartRequired: boolean;
|
|
48
|
+
/** A publication committed and not yet pushed. Mica retries it. */
|
|
49
|
+
pendingPush: {
|
|
50
|
+
revision: string;
|
|
51
|
+
reason: string;
|
|
52
|
+
} | null;
|
|
53
|
+
/** The last publication the host refused; it is a draft again. */
|
|
54
|
+
refused: {
|
|
55
|
+
reason: string;
|
|
56
|
+
at: string;
|
|
57
|
+
} | null;
|
|
58
|
+
conflicts: SyncConflict[];
|
|
59
|
+
}
|
|
60
|
+
/** What the System page says about the repository, beyond what the runtime knows. */
|
|
61
|
+
export interface RepositoryDescription {
|
|
62
|
+
/** The remote URL, without credentials. */
|
|
63
|
+
url: string;
|
|
64
|
+
branch: string;
|
|
65
|
+
/** Bytes the clone takes on the data volume. */
|
|
66
|
+
cloneBytes: number | null;
|
|
67
|
+
}
|
|
68
|
+
export interface OwnerPolicy {
|
|
69
|
+
readonly owner: "developer" | "managed";
|
|
70
|
+
publishAvailability(): PublishAvailability;
|
|
71
|
+
/**
|
|
72
|
+
* Runs with the remote lock held, before the tree lock. A managed clone syncs
|
|
73
|
+
* here and must not take the remote lock again. Throws an `ApiError` when
|
|
74
|
+
* Publish must wait.
|
|
75
|
+
*/
|
|
76
|
+
prepare(tree: GitTree): Promise<void>;
|
|
77
|
+
/** Who a commit names as its author, given the signed-in editor. Undefined leaves it to Git. */
|
|
78
|
+
commitAuthor(editor: Author | undefined): Author | undefined;
|
|
79
|
+
/**
|
|
80
|
+
* Sends HEAD to the remote. A failure returns `committed-pending-push`, not
|
|
81
|
+
* an exception: the commit is real.
|
|
82
|
+
*/
|
|
83
|
+
push(tree: GitTree, revision: string): Promise<PublishResult>;
|
|
84
|
+
syncStatus?(): SyncStatus;
|
|
85
|
+
sync?(): Promise<void>;
|
|
86
|
+
/** Keeps the editor's version of a conflicting group, or takes the website's. */
|
|
87
|
+
resolve?(group: string, choice: "mine" | "website"): Promise<void>;
|
|
88
|
+
/** The ref that holds the website's version, for a conflict's comparison. */
|
|
89
|
+
upstreamRef?: string;
|
|
90
|
+
describe?(): Promise<RepositoryDescription>;
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../../src/workspace/policy.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEzC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,MAAM,MAAM,mBAAmB,GAAG;IAAE,OAAO,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzF,+EAA+E;AAC/E,MAAM,WAAW,YAAY;IAC3B,yCAAyC;IACzC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,2DAA2D;IAC3D,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,EAAE,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;IAC1C,iEAAiE;IACjE,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG;QAAE,IAAI,EAAE,WAAW,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;CAC5I;AAED,oFAAoF;AACpF,MAAM,WAAW,UAAU;IACzB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,6CAA6C;IAC7C,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC/D,gEAAgE;IAChE,eAAe,EAAE,OAAO,CAAC;IACzB,mEAAmE;IACnE,WAAW,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACzD,kEAAkE;IAClE,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC/C,SAAS,EAAE,YAAY,EAAE,CAAC;CAC3B;AAED,qFAAqF;AACrF,MAAM,WAAW,qBAAqB;IACpC,2CAA2C;IAC3C,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,gDAAgD;IAChD,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,KAAK,EAAE,WAAW,GAAG,SAAS,CAAC;IACxC,mBAAmB,IAAI,mBAAmB,CAAC;IAC3C;;;;OAIG;IACH,OAAO,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,gGAAgG;IAChG,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAAC;IAC7D;;;OAGG;IACH,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;IAI9D,UAAU,CAAC,IAAI,UAAU,CAAC;IAC1B,IAAI,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,iFAAiF;IACjF,OAAO,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnE,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,IAAI,OAAO,CAAC,qBAAqB,CAAC,CAAC;CAC7C"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What changes with the owner of a working tree: whether Publish may run and
|
|
3
|
+
* how a commit reaches the remote. The app that keeps a managed clone supplies
|
|
4
|
+
* its policy; `developer.ts` is the developer's.
|
|
5
|
+
*/
|
|
6
|
+
export {};
|
|
7
|
+
//# sourceMappingURL=policy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"policy.js","sourceRoot":"","sources":["../../src/workspace/policy.ts"],"names":[],"mappings":"AAAA;;;;GAIG","sourcesContent":["/**\n * What changes with the owner of a working tree: whether Publish may run and\n * how a commit reaches the remote. The app that keeps a managed clone supplies\n * its policy; `developer.ts` is the developer's.\n */\n\nimport type { PublishResult } from \"@micacms/core\";\n\nimport type { Author } from \"./files.js\";\n\nimport type { GitTree } from \"./tree.js\";\n\nexport type PublishAvailability = { enabled: true } | { enabled: false; reason: string };\n\n/** One piece of content that an editor changed and the website changed too. */\nexport interface SyncConflict {\n /** Identifies the group in `resolve`. */\n group: string;\n label: string;\n /** Content-relative paths of the group, on either side. */\n paths: string[];\n upstream: \"changed\" | \"deleted\" | \"added\";\n /** What the comparison shows, when the group is one document. */\n target?: { kind: \"entry\"; collection: string; slug: string } | { kind: \"singleton\"; name: string } | { kind: \"order\"; collection: string };\n}\n\n/** Where a clone that Mica keeps stands against its remote. Managed owners only. */\nexport interface SyncStatus {\n lastSyncAt: string | null;\n /** Upstream commits not yet in the clone. */\n behind: number;\n error: { code: string; message: string; since: string } | null;\n /** The content model changed upstream; a restart applies it. */\n restartRequired: boolean;\n /** A publication committed and not yet pushed. Mica retries it. */\n pendingPush: { revision: string; reason: string } | null;\n /** The last publication the host refused; it is a draft again. */\n refused: { reason: string; at: string } | null;\n conflicts: SyncConflict[];\n}\n\n/** What the System page says about the repository, beyond what the runtime knows. */\nexport interface RepositoryDescription {\n /** The remote URL, without credentials. */\n url: string;\n branch: string;\n /** Bytes the clone takes on the data volume. */\n cloneBytes: number | null;\n}\n\nexport interface OwnerPolicy {\n readonly owner: \"developer\" | \"managed\";\n publishAvailability(): PublishAvailability;\n /**\n * Runs with the remote lock held, before the tree lock. A managed clone syncs\n * here and must not take the remote lock again. Throws an `ApiError` when\n * Publish must wait.\n */\n prepare(tree: GitTree): Promise<void>;\n /** Who a commit names as its author, given the signed-in editor. Undefined leaves it to Git. */\n commitAuthor(editor: Author | undefined): Author | undefined;\n /**\n * Sends HEAD to the remote. A failure returns `committed-pending-push`, not\n * an exception: the commit is real.\n */\n push(tree: GitTree, revision: string): Promise<PublishResult>;\n\n /* A clone that Mica keeps answers these; the developer's checkout does not. */\n\n syncStatus?(): SyncStatus;\n sync?(): Promise<void>;\n /** Keeps the editor's version of a conflicting group, or takes the website's. */\n resolve?(group: string, choice: \"mine\" | \"website\"): Promise<void>;\n /** The ref that holds the website's version, for a conflict's comparison. */\n upstreamRef?: string;\n describe?(): Promise<RepositoryDescription>;\n}\n"]}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git operations on a working tree whose content root is the draft workspace.
|
|
3
|
+
* The owner policy does the push. Every operation is limited to paths under the
|
|
4
|
+
* content root, never `git add -A` over the repository, so other work in the
|
|
5
|
+
* same checkout is never swept into a commit.
|
|
6
|
+
*/
|
|
7
|
+
import type { ContentChange, ContentSource } from "@micacms/core";
|
|
8
|
+
import type { Author } from "./files.js";
|
|
9
|
+
import { type GitRunner, type GitRunnerOptions } from "./git.js";
|
|
10
|
+
/**
|
|
11
|
+
* Makes every path argument literal: `tour[1].json` is not a pattern that also
|
|
12
|
+
* matches `tour1.json`.
|
|
13
|
+
*/
|
|
14
|
+
export declare const LITERAL = "--literal-pathspecs";
|
|
15
|
+
/** A content root inside a Git repository. */
|
|
16
|
+
export interface GitTree {
|
|
17
|
+
/** Runs in the repository root. */
|
|
18
|
+
git: GitRunner;
|
|
19
|
+
/** Content root relative to the repository root; "" when they coincide. */
|
|
20
|
+
prefix: string;
|
|
21
|
+
/** Absolute path of the content root. */
|
|
22
|
+
contentRoot: string;
|
|
23
|
+
}
|
|
24
|
+
/** A changed file plus what only Git status knows about it. */
|
|
25
|
+
export interface TreeChange extends ContentChange {
|
|
26
|
+
/** Not yet known to Git — discarding means deleting, not restoring. */
|
|
27
|
+
untracked: boolean;
|
|
28
|
+
/** Mid-merge conflict markers. Unpublishable until resolved with Git tools. */
|
|
29
|
+
conflicted: boolean;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Finds the repository the content root sits in, freshly each time: a
|
|
33
|
+
* `git init` after startup should start working, not require a restart.
|
|
34
|
+
*/
|
|
35
|
+
export declare function locateTree(contentRoot: string, options?: GitRunnerOptions): Promise<GitTree | {
|
|
36
|
+
reason: string;
|
|
37
|
+
}>;
|
|
38
|
+
/** The content root's changes, as `git status` sees them. */
|
|
39
|
+
export declare function listTreeChanges(tree: GitTree): Promise<TreeChange[]>;
|
|
40
|
+
/**
|
|
41
|
+
* Stages and commits only `paths` (content-root relative) and returns the new
|
|
42
|
+
* HEAD. The pathspec limits both steps, so all other staged or unstaged work
|
|
43
|
+
* stays. Without `author`, Git's configured identity applies.
|
|
44
|
+
*/
|
|
45
|
+
export declare function commitPaths(tree: GitTree, paths: string[], message: string, author?: Author): Promise<string>;
|
|
46
|
+
/**
|
|
47
|
+
* Commits `paths` again with the message, author and date of `original`, after
|
|
48
|
+
* a rejected push. Returns the new HEAD.
|
|
49
|
+
*/
|
|
50
|
+
export declare function recommitPaths(tree: GitTree, paths: string[], original: string): Promise<string>;
|
|
51
|
+
/** Puts one changed file back to its HEAD version, or removes it if HEAD has none. */
|
|
52
|
+
export declare function discardChange(tree: GitTree, change: TreeChange): Promise<void>;
|
|
53
|
+
/** HEAD's commit id, or null before the first commit. */
|
|
54
|
+
export declare function headRevision(tree: GitTree): Promise<string | null>;
|
|
55
|
+
/** The checked-out branch, or null when there is none to name. */
|
|
56
|
+
export declare function currentBranch(tree: GitTree): Promise<string | null>;
|
|
57
|
+
/** Commits waiting to be pushed, or null when there is no upstream to say. */
|
|
58
|
+
export declare function aheadOfUpstream(tree: GitTree): Promise<number | null>;
|
|
59
|
+
/**
|
|
60
|
+
* The content root as `ref` has it, read from Git's object store, because the
|
|
61
|
+
* working tree holds the drafts.
|
|
62
|
+
*/
|
|
63
|
+
export declare function headSource(tree: GitTree, ref?: string): ContentSource;
|
|
64
|
+
/**
|
|
65
|
+
* Parses `git status --porcelain=v1 -z` into content-root-relative changes.
|
|
66
|
+
* `-z` never changes a path; a rename is two fields and becomes a create and a
|
|
67
|
+
* delete. `.mica/` is excluded: the developer commits the manifest, and the
|
|
68
|
+
* drift check guards model changes.
|
|
69
|
+
*/
|
|
70
|
+
export declare function parseStatus(raw: string, prefix: string): TreeChange[];
|
|
71
|
+
/** A content-root-relative path as Git sees it, from the repository root. */
|
|
72
|
+
export declare function inRepo(prefix: string, path: string): string;
|
|
73
|
+
//# sourceMappingURL=tree.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tree.d.ts","sourceRoot":"","sources":["../../src/workspace/tree.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAkB,MAAM,eAAe,CAAC;AAGlF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAuB,KAAK,SAAS,EAAE,KAAK,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAEtF;;;GAGG;AACH,eAAO,MAAM,OAAO,wBAAwB,CAAC;AAE7C,8CAA8C;AAC9C,MAAM,WAAW,OAAO;IACtB,mCAAmC;IACnC,GAAG,EAAE,SAAS,CAAC;IACf,2EAA2E;IAC3E,MAAM,EAAE,MAAM,CAAC;IACf,yCAAyC;IACzC,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,+DAA+D;AAC/D,MAAM,WAAW,UAAW,SAAQ,aAAa;IAC/C,uEAAuE;IACvE,SAAS,EAAE,OAAO,CAAC;IACnB,+EAA+E;IAC/E,UAAU,EAAE,OAAO,CAAC;CACrB;AAED;;;GAGG;AACH,wBAAsB,UAAU,CAC9B,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,OAAO,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAoBvC;AAED,6DAA6D;AAC7D,wBAAsB,eAAe,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAQ1E;AAED;;;;GAIG;AACH,wBAAsB,WAAW,CAC/B,IAAI,EAAE,OAAO,EACb,KAAK,EAAE,MAAM,EAAE,EACf,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,MAAM,CAAC,CAKjB;AAED;;;GAGG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAIrG;AAsBD,sFAAsF;AACtF,wBAAsB,aAAa,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAkBpF;AAED,yDAAyD;AACzD,wBAAgB,YAAY,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAElE;AAED,kEAAkE;AAClE,wBAAgB,aAAa,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAEnE;AAED,8EAA8E;AAC9E,wBAAsB,eAAe,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAK3E;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,SAAS,GAAG,aAAa,CAyBrE;AAQD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,UAAU,EAAE,CAsDrE;AAQD,6EAA6E;AAC7E,wBAAgB,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAE3D"}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git operations on a working tree whose content root is the draft workspace.
|
|
3
|
+
* The owner policy does the push. Every operation is limited to paths under the
|
|
4
|
+
* content root, never `git add -A` over the repository, so other work in the
|
|
5
|
+
* same checkout is never swept into a commit.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync } from "node:fs";
|
|
8
|
+
import { realpath, rm } from "node:fs/promises";
|
|
9
|
+
import { join, relative, sep } from "node:path";
|
|
10
|
+
import { within } from "@micacms/core/node";
|
|
11
|
+
import { GitError, gitRunner } from "./git.js";
|
|
12
|
+
/**
|
|
13
|
+
* Makes every path argument literal: `tour[1].json` is not a pattern that also
|
|
14
|
+
* matches `tour1.json`.
|
|
15
|
+
*/
|
|
16
|
+
export const LITERAL = "--literal-pathspecs";
|
|
17
|
+
/**
|
|
18
|
+
* Finds the repository the content root sits in, freshly each time: a
|
|
19
|
+
* `git init` after startup should start working, not require a restart.
|
|
20
|
+
*/
|
|
21
|
+
export async function locateTree(contentRoot, options = {}) {
|
|
22
|
+
const probe = gitRunner(contentRoot, options);
|
|
23
|
+
try {
|
|
24
|
+
const repoRoot = await probe.run(["rev-parse", "--show-toplevel"]);
|
|
25
|
+
// Git resolves symlinks in the toplevel (`/private/var` for `/var` on
|
|
26
|
+
// macOS); do the same, or the prefix is wrong.
|
|
27
|
+
const resolved = await realpath(contentRoot);
|
|
28
|
+
return {
|
|
29
|
+
git: gitRunner(repoRoot, options),
|
|
30
|
+
prefix: toPosix(relative(repoRoot, resolved)),
|
|
31
|
+
contentRoot,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
return {
|
|
36
|
+
reason: error instanceof GitError && error.exitCode === null
|
|
37
|
+
? error.message
|
|
38
|
+
: "The content directory is not inside a Git repository.",
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** The content root's changes, as `git status` sees them. */
|
|
43
|
+
export async function listTreeChanges(tree) {
|
|
44
|
+
// `-uall`: Git otherwise collapses a new, untracked entry directory to one line.
|
|
45
|
+
// `GIT_OPTIONAL_LOCKS=0`: no `index.lock`, so a parallel commit cannot fail on it.
|
|
46
|
+
const raw = await tree.git.run([LITERAL, "status", "--porcelain=v1", "-z", "-uall", "--", tree.prefix === "" ? "." : tree.prefix], { env: { GIT_OPTIONAL_LOCKS: "0" } });
|
|
47
|
+
return parseStatus(raw, tree.prefix);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Stages and commits only `paths` (content-root relative) and returns the new
|
|
51
|
+
* HEAD. The pathspec limits both steps, so all other staged or unstaged work
|
|
52
|
+
* stays. Without `author`, Git's configured identity applies.
|
|
53
|
+
*/
|
|
54
|
+
export async function commitPaths(tree, paths, message, author) {
|
|
55
|
+
const inRepository = await stagePaths(tree, paths);
|
|
56
|
+
const as = author ? [`--author=${author.name} <${author.email}>`] : [];
|
|
57
|
+
await tree.git.run([LITERAL, "commit", "-m", message, ...as, "--", ...inRepository]);
|
|
58
|
+
return tree.git.run(["rev-parse", "HEAD"]);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Commits `paths` again with the message, author and date of `original`, after
|
|
62
|
+
* a rejected push. Returns the new HEAD.
|
|
63
|
+
*/
|
|
64
|
+
export async function recommitPaths(tree, paths, original) {
|
|
65
|
+
const inRepository = await stagePaths(tree, paths);
|
|
66
|
+
await tree.git.run([LITERAL, "commit", "-C", original, "--", ...inRepository]);
|
|
67
|
+
return tree.git.run(["rev-parse", "HEAD"]);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Stages `paths`, because `git commit -- <paths>` refuses a file Git has never
|
|
71
|
+
* seen. A path gone from the disk uses `rm --cached`, because `git add` refuses
|
|
72
|
+
* a deletion that is already staged (as `reset --soft` leaves it).
|
|
73
|
+
*/
|
|
74
|
+
async function stagePaths(tree, paths) {
|
|
75
|
+
const present = [];
|
|
76
|
+
const gone = [];
|
|
77
|
+
for (const path of paths) {
|
|
78
|
+
(existsSync(within(tree.contentRoot, path)) ? present : gone).push(inRepo(tree.prefix, path));
|
|
79
|
+
}
|
|
80
|
+
for (const batch of chunked(present, 500)) {
|
|
81
|
+
await tree.git.run([LITERAL, "add", "-A", "--", ...batch]);
|
|
82
|
+
}
|
|
83
|
+
for (const batch of chunked(gone, 500)) {
|
|
84
|
+
await tree.git.run([LITERAL, "rm", "--cached", "--ignore-unmatch", "--quiet", "--", ...batch]);
|
|
85
|
+
}
|
|
86
|
+
return paths.map((path) => inRepo(tree.prefix, path));
|
|
87
|
+
}
|
|
88
|
+
/** Puts one changed file back to its HEAD version, or removes it if HEAD has none. */
|
|
89
|
+
export async function discardChange(tree, change) {
|
|
90
|
+
if (change.kind === "create") {
|
|
91
|
+
// Unstage a staged creation first, so the index does not point at a deleted file.
|
|
92
|
+
if (!change.untracked) {
|
|
93
|
+
await tree.git.tryRun([LITERAL, "rm", "--cached", "--force", "--", inRepo(tree.prefix, change.path)]);
|
|
94
|
+
}
|
|
95
|
+
await rm(within(tree.contentRoot, change.path), { force: true });
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
await tree.git.run([
|
|
99
|
+
LITERAL,
|
|
100
|
+
"restore",
|
|
101
|
+
"--source=HEAD",
|
|
102
|
+
"--staged",
|
|
103
|
+
"--worktree",
|
|
104
|
+
"--",
|
|
105
|
+
inRepo(tree.prefix, change.path),
|
|
106
|
+
]);
|
|
107
|
+
}
|
|
108
|
+
/** HEAD's commit id, or null before the first commit. */
|
|
109
|
+
export function headRevision(tree) {
|
|
110
|
+
return tree.git.tryRun(["rev-parse", "HEAD"]);
|
|
111
|
+
}
|
|
112
|
+
/** The checked-out branch, or null when there is none to name. */
|
|
113
|
+
export function currentBranch(tree) {
|
|
114
|
+
return tree.git.tryRun(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
115
|
+
}
|
|
116
|
+
/** Commits waiting to be pushed, or null when there is no upstream to say. */
|
|
117
|
+
export async function aheadOfUpstream(tree) {
|
|
118
|
+
const counted = await tree.git.tryRun(["rev-list", "--count", "@{upstream}..HEAD"]);
|
|
119
|
+
if (counted === null)
|
|
120
|
+
return null;
|
|
121
|
+
const ahead = Number(counted);
|
|
122
|
+
return Number.isFinite(ahead) ? ahead : null;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The content root as `ref` has it, read from Git's object store, because the
|
|
126
|
+
* working tree holds the drafts.
|
|
127
|
+
*/
|
|
128
|
+
export function headSource(tree, ref = "HEAD") {
|
|
129
|
+
const at = (path) => `${ref}:${path === "" ? tree.prefix : inRepo(tree.prefix, path)}`;
|
|
130
|
+
return {
|
|
131
|
+
origin: ref,
|
|
132
|
+
async read(path) {
|
|
133
|
+
// "Not in HEAD" and "no commits yet" both mean that there is no published version.
|
|
134
|
+
return tree.git.tryRun(["show", at(path)]);
|
|
135
|
+
},
|
|
136
|
+
async list(path) {
|
|
137
|
+
const raw = await tree.git.tryRun(["ls-tree", "-z", at(path)]);
|
|
138
|
+
if (raw === null)
|
|
139
|
+
return [];
|
|
140
|
+
const entries = [];
|
|
141
|
+
for (const field of raw.split("\0")) {
|
|
142
|
+
const tab = field.indexOf("\t");
|
|
143
|
+
if (tab === -1)
|
|
144
|
+
continue;
|
|
145
|
+
const type = field.slice(0, tab).split(" ")[1];
|
|
146
|
+
entries.push({ name: field.slice(tab + 1), type: type === "tree" ? "directory" : "file" });
|
|
147
|
+
}
|
|
148
|
+
return entries;
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* A save's temporary file (`*.mica-<pid>-<n>`), seen during a save or after a
|
|
154
|
+
* crash. Never content.
|
|
155
|
+
*/
|
|
156
|
+
const TEMPORARY_FILE = /\.mica-\d+-\d+$/;
|
|
157
|
+
/**
|
|
158
|
+
* Parses `git status --porcelain=v1 -z` into content-root-relative changes.
|
|
159
|
+
* `-z` never changes a path; a rename is two fields and becomes a create and a
|
|
160
|
+
* delete. `.mica/` is excluded: the developer commits the manifest, and the
|
|
161
|
+
* drift check guards model changes.
|
|
162
|
+
*/
|
|
163
|
+
export function parseStatus(raw, prefix) {
|
|
164
|
+
const fields = raw.split("\0").filter((field) => field !== "");
|
|
165
|
+
const changes = [];
|
|
166
|
+
const push = (repoPath, kind, flags = {}) => {
|
|
167
|
+
const path = relativeToContent(repoPath, prefix);
|
|
168
|
+
if (path === null || path === ".mica" || path.startsWith(".mica/"))
|
|
169
|
+
return;
|
|
170
|
+
if (TEMPORARY_FILE.test(path))
|
|
171
|
+
return;
|
|
172
|
+
changes.push({
|
|
173
|
+
path,
|
|
174
|
+
kind,
|
|
175
|
+
untracked: flags.untracked ?? false,
|
|
176
|
+
conflicted: flags.conflicted ?? false,
|
|
177
|
+
});
|
|
178
|
+
};
|
|
179
|
+
for (let at = 0; at < fields.length; at += 1) {
|
|
180
|
+
const field = fields[at];
|
|
181
|
+
const x = field[0] ?? " ";
|
|
182
|
+
const y = field[1] ?? " ";
|
|
183
|
+
const repoPath = field.slice(3);
|
|
184
|
+
if (x === "?" && y === "?") {
|
|
185
|
+
push(repoPath, "create", { untracked: true });
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
// A merge conflict state, whatever the tree holds.
|
|
189
|
+
if (x === "U" || y === "U" || (x === "A" && y === "A") || (x === "D" && y === "D")) {
|
|
190
|
+
push(repoPath, "update", { conflicted: true });
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (x === "R" || x === "C") {
|
|
194
|
+
// The next field is the source path; only a rename deletes it.
|
|
195
|
+
const from = fields[++at];
|
|
196
|
+
push(repoPath, "create");
|
|
197
|
+
if (x === "R")
|
|
198
|
+
push(from, "delete");
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (x === "D" || y === "D") {
|
|
202
|
+
push(repoPath, "delete");
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
push(repoPath, x === "A" ? "create" : "update");
|
|
206
|
+
}
|
|
207
|
+
return changes;
|
|
208
|
+
}
|
|
209
|
+
function relativeToContent(repoPath, prefix) {
|
|
210
|
+
if (prefix === "")
|
|
211
|
+
return repoPath;
|
|
212
|
+
if (repoPath === prefix)
|
|
213
|
+
return null;
|
|
214
|
+
return repoPath.startsWith(`${prefix}/`) ? repoPath.slice(prefix.length + 1) : null;
|
|
215
|
+
}
|
|
216
|
+
/** A content-root-relative path as Git sees it, from the repository root. */
|
|
217
|
+
export function inRepo(prefix, path) {
|
|
218
|
+
return prefix === "" ? path : join(prefix, path).split(sep).join("/");
|
|
219
|
+
}
|
|
220
|
+
function toPosix(path) {
|
|
221
|
+
return sep === "/" ? path : path.split(sep).join("/");
|
|
222
|
+
}
|
|
223
|
+
function chunked(items, size) {
|
|
224
|
+
const batches = [];
|
|
225
|
+
for (let at = 0; at < items.length; at += size)
|
|
226
|
+
batches.push(items.slice(at, at + size));
|
|
227
|
+
return batches;
|
|
228
|
+
}
|
|
229
|
+
//# sourceMappingURL=tree.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tree.js","sourceRoot":"","sources":["../../src/workspace/tree.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Git operations on a working tree whose content root is the draft workspace.\n * The owner policy does the push. Every operation is limited to paths under the\n * content root, never `git add -A` over the repository, so other work in the\n * same checkout is never swept into a commit.\n */\n\nimport { existsSync } from \"node:fs\";\nimport { realpath, rm } from \"node:fs/promises\";\nimport { join, relative, sep } from \"node:path\";\n\nimport type { ContentChange, ContentSource, DirectoryEntry } from \"@micacms/core\";\nimport { within } from \"@micacms/core/node\";\n\nimport type { Author } from \"./files.js\";\nimport { GitError, gitRunner, type GitRunner, type GitRunnerOptions } from \"./git.js\";\n\n/**\n * Makes every path argument literal: `tour[1].json` is not a pattern that also\n * matches `tour1.json`.\n */\nexport const LITERAL = \"--literal-pathspecs\";\n\n/** A content root inside a Git repository. */\nexport interface GitTree {\n /** Runs in the repository root. */\n git: GitRunner;\n /** Content root relative to the repository root; \"\" when they coincide. */\n prefix: string;\n /** Absolute path of the content root. */\n contentRoot: string;\n}\n\n/** A changed file plus what only Git status knows about it. */\nexport interface TreeChange extends ContentChange {\n /** Not yet known to Git — discarding means deleting, not restoring. */\n untracked: boolean;\n /** Mid-merge conflict markers. Unpublishable until resolved with Git tools. */\n conflicted: boolean;\n}\n\n/**\n * Finds the repository the content root sits in, freshly each time: a\n * `git init` after startup should start working, not require a restart.\n */\nexport async function locateTree(\n contentRoot: string,\n options: GitRunnerOptions = {},\n): Promise<GitTree | { reason: string }> {\n const probe = gitRunner(contentRoot, options);\n try {\n const repoRoot = await probe.run([\"rev-parse\", \"--show-toplevel\"]);\n // Git resolves symlinks in the toplevel (`/private/var` for `/var` on\n // macOS); do the same, or the prefix is wrong.\n const resolved = await realpath(contentRoot);\n return {\n git: gitRunner(repoRoot, options),\n prefix: toPosix(relative(repoRoot, resolved)),\n contentRoot,\n };\n } catch (error) {\n return {\n reason:\n error instanceof GitError && error.exitCode === null\n ? error.message\n : \"The content directory is not inside a Git repository.\",\n };\n }\n}\n\n/** The content root's changes, as `git status` sees them. */\nexport async function listTreeChanges(tree: GitTree): Promise<TreeChange[]> {\n // `-uall`: Git otherwise collapses a new, untracked entry directory to one line.\n // `GIT_OPTIONAL_LOCKS=0`: no `index.lock`, so a parallel commit cannot fail on it.\n const raw = await tree.git.run(\n [LITERAL, \"status\", \"--porcelain=v1\", \"-z\", \"-uall\", \"--\", tree.prefix === \"\" ? \".\" : tree.prefix],\n { env: { GIT_OPTIONAL_LOCKS: \"0\" } },\n );\n return parseStatus(raw, tree.prefix);\n}\n\n/**\n * Stages and commits only `paths` (content-root relative) and returns the new\n * HEAD. The pathspec limits both steps, so all other staged or unstaged work\n * stays. Without `author`, Git's configured identity applies.\n */\nexport async function commitPaths(\n tree: GitTree,\n paths: string[],\n message: string,\n author?: Author,\n): Promise<string> {\n const inRepository = await stagePaths(tree, paths);\n const as = author ? [`--author=${author.name} <${author.email}>`] : [];\n await tree.git.run([LITERAL, \"commit\", \"-m\", message, ...as, \"--\", ...inRepository]);\n return tree.git.run([\"rev-parse\", \"HEAD\"]);\n}\n\n/**\n * Commits `paths` again with the message, author and date of `original`, after\n * a rejected push. Returns the new HEAD.\n */\nexport async function recommitPaths(tree: GitTree, paths: string[], original: string): Promise<string> {\n const inRepository = await stagePaths(tree, paths);\n await tree.git.run([LITERAL, \"commit\", \"-C\", original, \"--\", ...inRepository]);\n return tree.git.run([\"rev-parse\", \"HEAD\"]);\n}\n\n/**\n * Stages `paths`, because `git commit -- <paths>` refuses a file Git has never\n * seen. A path gone from the disk uses `rm --cached`, because `git add` refuses\n * a deletion that is already staged (as `reset --soft` leaves it).\n */\nasync function stagePaths(tree: GitTree, paths: string[]): Promise<string[]> {\n const present: string[] = [];\n const gone: string[] = [];\n for (const path of paths) {\n (existsSync(within(tree.contentRoot, path)) ? present : gone).push(inRepo(tree.prefix, path));\n }\n for (const batch of chunked(present, 500)) {\n await tree.git.run([LITERAL, \"add\", \"-A\", \"--\", ...batch]);\n }\n for (const batch of chunked(gone, 500)) {\n await tree.git.run([LITERAL, \"rm\", \"--cached\", \"--ignore-unmatch\", \"--quiet\", \"--\", ...batch]);\n }\n return paths.map((path) => inRepo(tree.prefix, path));\n}\n\n/** Puts one changed file back to its HEAD version, or removes it if HEAD has none. */\nexport async function discardChange(tree: GitTree, change: TreeChange): Promise<void> {\n if (change.kind === \"create\") {\n // Unstage a staged creation first, so the index does not point at a deleted file.\n if (!change.untracked) {\n await tree.git.tryRun([LITERAL, \"rm\", \"--cached\", \"--force\", \"--\", inRepo(tree.prefix, change.path)]);\n }\n await rm(within(tree.contentRoot, change.path), { force: true });\n return;\n }\n await tree.git.run([\n LITERAL,\n \"restore\",\n \"--source=HEAD\",\n \"--staged\",\n \"--worktree\",\n \"--\",\n inRepo(tree.prefix, change.path),\n ]);\n}\n\n/** HEAD's commit id, or null before the first commit. */\nexport function headRevision(tree: GitTree): Promise<string | null> {\n return tree.git.tryRun([\"rev-parse\", \"HEAD\"]);\n}\n\n/** The checked-out branch, or null when there is none to name. */\nexport function currentBranch(tree: GitTree): Promise<string | null> {\n return tree.git.tryRun([\"rev-parse\", \"--abbrev-ref\", \"HEAD\"]);\n}\n\n/** Commits waiting to be pushed, or null when there is no upstream to say. */\nexport async function aheadOfUpstream(tree: GitTree): Promise<number | null> {\n const counted = await tree.git.tryRun([\"rev-list\", \"--count\", \"@{upstream}..HEAD\"]);\n if (counted === null) return null;\n const ahead = Number(counted);\n return Number.isFinite(ahead) ? ahead : null;\n}\n\n/**\n * The content root as `ref` has it, read from Git's object store, because the\n * working tree holds the drafts.\n */\nexport function headSource(tree: GitTree, ref = \"HEAD\"): ContentSource {\n const at = (path: string) => `${ref}:${path === \"\" ? tree.prefix : inRepo(tree.prefix, path)}`;\n\n return {\n origin: ref,\n\n async read(path) {\n // \"Not in HEAD\" and \"no commits yet\" both mean that there is no published version.\n return tree.git.tryRun([\"show\", at(path)]);\n },\n\n async list(path) {\n const raw = await tree.git.tryRun([\"ls-tree\", \"-z\", at(path)]);\n if (raw === null) return [];\n\n const entries: DirectoryEntry[] = [];\n for (const field of raw.split(\"\\0\")) {\n const tab = field.indexOf(\"\\t\");\n if (tab === -1) continue;\n const type = field.slice(0, tab).split(\" \")[1];\n entries.push({ name: field.slice(tab + 1), type: type === \"tree\" ? \"directory\" : \"file\" });\n }\n return entries;\n },\n };\n}\n\n/**\n * A save's temporary file (`*.mica-<pid>-<n>`), seen during a save or after a\n * crash. Never content.\n */\nconst TEMPORARY_FILE = /\\.mica-\\d+-\\d+$/;\n\n/**\n * Parses `git status --porcelain=v1 -z` into content-root-relative changes.\n * `-z` never changes a path; a rename is two fields and becomes a create and a\n * delete. `.mica/` is excluded: the developer commits the manifest, and the\n * drift check guards model changes.\n */\nexport function parseStatus(raw: string, prefix: string): TreeChange[] {\n const fields = raw.split(\"\\0\").filter((field) => field !== \"\");\n const changes: TreeChange[] = [];\n\n const push = (\n repoPath: string,\n kind: TreeChange[\"kind\"],\n flags: { untracked?: boolean; conflicted?: boolean } = {},\n ) => {\n const path = relativeToContent(repoPath, prefix);\n if (path === null || path === \".mica\" || path.startsWith(\".mica/\")) return;\n if (TEMPORARY_FILE.test(path)) return;\n changes.push({\n path,\n kind,\n untracked: flags.untracked ?? false,\n conflicted: flags.conflicted ?? false,\n });\n };\n\n for (let at = 0; at < fields.length; at += 1) {\n const field = fields[at]!;\n const x = field[0] ?? \" \";\n const y = field[1] ?? \" \";\n const repoPath = field.slice(3);\n\n if (x === \"?\" && y === \"?\") {\n push(repoPath, \"create\", { untracked: true });\n continue;\n }\n\n // A merge conflict state, whatever the tree holds.\n if (x === \"U\" || y === \"U\" || (x === \"A\" && y === \"A\") || (x === \"D\" && y === \"D\")) {\n push(repoPath, \"update\", { conflicted: true });\n continue;\n }\n\n if (x === \"R\" || x === \"C\") {\n // The next field is the source path; only a rename deletes it.\n const from = fields[++at]!;\n push(repoPath, \"create\");\n if (x === \"R\") push(from, \"delete\");\n continue;\n }\n\n if (x === \"D\" || y === \"D\") {\n push(repoPath, \"delete\");\n continue;\n }\n\n push(repoPath, x === \"A\" ? \"create\" : \"update\");\n }\n\n return changes;\n}\n\nfunction relativeToContent(repoPath: string, prefix: string): string | null {\n if (prefix === \"\") return repoPath;\n if (repoPath === prefix) return null;\n return repoPath.startsWith(`${prefix}/`) ? repoPath.slice(prefix.length + 1) : null;\n}\n\n/** A content-root-relative path as Git sees it, from the repository root. */\nexport function inRepo(prefix: string, path: string): string {\n return prefix === \"\" ? path : join(prefix, path).split(sep).join(\"/\");\n}\n\nfunction toPosix(path: string): string {\n return sep === \"/\" ? path : path.split(sep).join(\"/\");\n}\n\nfunction chunked<T>(items: T[], size: number): T[][] {\n const batches: T[][] = [];\n for (let at = 0; at < items.length; at += size) batches.push(items.slice(at, at + size));\n return batches;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@micacms/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0-rc.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The Mica single-site runtime: the Hono API, application services and staging orchestration, authentication-neutral. Composed by @micacms/editor locally and @micacms/self-hosted behind OIDC.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -29,8 +29,7 @@
|
|
|
29
29
|
"hono": "^4.13.7",
|
|
30
30
|
"image-size": "^2.0.3",
|
|
31
31
|
"zod": "^4.3.6",
|
|
32
|
-
"@micacms/
|
|
33
|
-
"@micacms/core": "0.4.1"
|
|
32
|
+
"@micacms/core": "0.5.0-rc.2"
|
|
34
33
|
},
|
|
35
34
|
"devDependencies": {
|
|
36
35
|
"@types/node": "^22.20.1",
|
package/dist/db/driver.d.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SQLite driver.
|
|
3
|
-
*
|
|
4
|
-
* Everything above this file talks to `SqlDatabase`, never to `node:sqlite`
|
|
5
|
-
* directly, so the driver can be swapped (better-sqlite3, libsql, a Postgres
|
|
6
|
-
* adapter) without touching the repositories.
|
|
7
|
-
*
|
|
8
|
-
* `node:sqlite` is the default because it ships with Node — no native build
|
|
9
|
-
* step, no prebuilt-binary mismatch inside the container.
|
|
10
|
-
*/
|
|
11
|
-
export type SqlValue = string | number | bigint | null | Uint8Array;
|
|
12
|
-
export type SqlParams = readonly SqlValue[];
|
|
13
|
-
export interface RunResult {
|
|
14
|
-
changes: number;
|
|
15
|
-
lastInsertRowid: number | bigint;
|
|
16
|
-
}
|
|
17
|
-
export interface SqlDatabase {
|
|
18
|
-
all<T = Record<string, unknown>>(sql: string, params?: SqlParams): T[];
|
|
19
|
-
get<T = Record<string, unknown>>(sql: string, params?: SqlParams): T | undefined;
|
|
20
|
-
run(sql: string, params?: SqlParams): RunResult;
|
|
21
|
-
exec(sql: string): void;
|
|
22
|
-
/** Runs `fn` in a transaction, rolling back if it throws. Safe to nest. */
|
|
23
|
-
transaction<T>(fn: () => T): T;
|
|
24
|
-
/** Consistent on-line snapshot to `destination`, safe while writes continue. */
|
|
25
|
-
snapshot(destination: string): void;
|
|
26
|
-
close(): void;
|
|
27
|
-
}
|
|
28
|
-
export interface SqliteOptions {
|
|
29
|
-
/** Milliseconds a blocked writer waits before returning SQLITE_BUSY. */
|
|
30
|
-
busyTimeoutMs?: number;
|
|
31
|
-
/** WAL is the right default for a server; `false` is useful for :memory:. */
|
|
32
|
-
wal?: boolean;
|
|
33
|
-
}
|
|
34
|
-
export declare function openSqlite(filename: string, options?: SqliteOptions): SqlDatabase;
|
|
35
|
-
//# sourceMappingURL=driver.d.ts.map
|
package/dist/db/driver.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"driver.d.ts","sourceRoot":"","sources":["../../src/db/driver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,UAAU,CAAC;AACpE,MAAM,MAAM,SAAS,GAAG,SAAS,QAAQ,EAAE,CAAC;AAE5C,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,MAAM,GAAG,MAAM,CAAC;CAClC;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,GAAG,CAAC,EAAE,CAAC;IACvE,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,GAAG,CAAC,GAAG,SAAS,CAAC;IACjF,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IAChD,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,2EAA2E;IAC3E,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC;IAC/B,gFAAgF;IAChF,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,KAAK,IAAI,IAAI,CAAC;CACf;AAED,MAAM,WAAW,aAAa;IAC5B,wEAAwE;IACxE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,6EAA6E;IAC7E,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AAED,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,aAAkB,GAAG,WAAW,CA2FrF"}
|