@micacms/runtime 0.6.0 → 0.7.0
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/app.d.ts.map +1 -1
- package/dist/app.js +5 -0
- package/dist/app.js.map +1 -1
- package/dist/context.d.ts +17 -2
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +62 -18
- package/dist/context.js.map +1 -1
- package/dist/http.d.ts +2 -0
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +5 -0
- package/dist/http.js.map +1 -1
- package/dist/lib/file-type.d.ts +2 -0
- package/dist/lib/file-type.d.ts.map +1 -1
- package/dist/lib/file-type.js +3 -1
- package/dist/lib/file-type.js.map +1 -1
- package/dist/routes/admin-content.d.ts +1 -1
- package/dist/routes/admin-content.d.ts.map +1 -1
- package/dist/routes/admin-content.js +15 -0
- package/dist/routes/admin-content.js.map +1 -1
- package/dist/routes/admin-media.d.ts +5 -3
- package/dist/routes/admin-media.d.ts.map +1 -1
- package/dist/routes/admin-media.js +72 -2
- package/dist/routes/admin-media.js.map +1 -1
- package/dist/routes/admin-staging.d.ts.map +1 -1
- package/dist/routes/admin-staging.js +18 -7
- package/dist/routes/admin-staging.js.map +1 -1
- package/dist/routes/admin-system.d.ts.map +1 -1
- package/dist/routes/admin-system.js +4 -0
- package/dist/routes/admin-system.js.map +1 -1
- package/dist/services/changes.d.ts +19 -4
- package/dist/services/changes.d.ts.map +1 -1
- package/dist/services/changes.js +60 -1
- package/dist/services/changes.js.map +1 -1
- package/dist/services/content-store.d.ts +27 -6
- package/dist/services/content-store.d.ts.map +1 -1
- package/dist/services/content-store.js +231 -18
- package/dist/services/content-store.js.map +1 -1
- package/dist/services/media.d.ts.map +1 -1
- package/dist/services/media.js +4 -1
- package/dist/services/media.js.map +1 -1
- package/dist/services/staging.d.ts.map +1 -1
- package/dist/services/staging.js +26 -14
- package/dist/services/staging.js.map +1 -1
- package/dist/workspace/developer.d.ts +2 -1
- package/dist/workspace/developer.d.ts.map +1 -1
- package/dist/workspace/developer.js +19 -4
- package/dist/workspace/developer.js.map +1 -1
- package/dist/workspace/policy.d.ts +1 -1
- package/dist/workspace/policy.d.ts.map +1 -1
- package/dist/workspace/policy.js.map +1 -1
- package/dist/workspace/tree.d.ts +15 -2
- package/dist/workspace/tree.d.ts.map +1 -1
- package/dist/workspace/tree.js +29 -6
- package/dist/workspace/tree.js.map +1 -1
- package/package.json +2 -2
- package/src/app.ts +5 -0
- package/src/context.ts +69 -20
- package/src/http.ts +6 -0
- package/src/lib/file-type.ts +4 -1
- package/src/routes/admin-content.ts +16 -2
- package/src/routes/admin-media.ts +74 -3
- package/src/routes/admin-staging.ts +22 -7
- package/src/routes/admin-system.ts +4 -0
- package/src/services/changes.ts +84 -5
- package/src/services/content-store.ts +243 -25
- package/src/services/media.ts +7 -1
- package/src/services/staging.ts +30 -14
- package/src/workspace/developer.ts +22 -5
- package/src/workspace/policy.ts +1 -1
- package/src/workspace/tree.ts +35 -6
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
* The policy for the developer's own checkout. Mica never fetches, merges or
|
|
3
3
|
* resets here. Publish pushes with the developer's `git`, credentials and
|
|
4
4
|
* upstream; a new branch without one is pushed to `origin` and tracks it. A
|
|
5
|
-
* failed push keeps the commit.
|
|
5
|
+
* failed push keeps the commit. A push sends only Mica's own commits: other
|
|
6
|
+
* commits that no remote has make Publish refuse.
|
|
6
7
|
*/
|
|
7
|
-
import {
|
|
8
|
+
import { ApiError } from "../lib/errors.js";
|
|
9
|
+
import { unpushedCommits } from "./tree.js";
|
|
8
10
|
export function developerPolicy(options) {
|
|
9
11
|
return {
|
|
10
12
|
owner: "developer",
|
|
@@ -13,11 +15,15 @@ export function developerPolicy(options) {
|
|
|
13
15
|
? { enabled: true }
|
|
14
16
|
: { enabled: false, reason: "Publishing from local mode is disabled (MICA_LOCAL_PUBLISH=false)." };
|
|
15
17
|
},
|
|
16
|
-
async prepare() {
|
|
18
|
+
async prepare(tree) {
|
|
19
|
+
await refuseOtherCommits(tree);
|
|
20
|
+
},
|
|
17
21
|
// The commit keeps the developer's own Git identity.
|
|
18
22
|
commitAuthor: () => undefined,
|
|
19
23
|
async push(tree, revision) {
|
|
20
24
|
try {
|
|
25
|
+
// Checked again: a commit made in a terminal since `prepare` would go along.
|
|
26
|
+
await refuseOtherCommits(tree);
|
|
21
27
|
// Publishing is the request to push, so a new branch gets its upstream here.
|
|
22
28
|
const upstream = await tree.git.tryRun(["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"]);
|
|
23
29
|
const remotes = (await tree.git.tryRun(["remote"]))?.split("\n").filter(Boolean) ?? [];
|
|
@@ -30,13 +36,22 @@ export function developerPolicy(options) {
|
|
|
30
36
|
return { state: "published", revision };
|
|
31
37
|
}
|
|
32
38
|
catch (error) {
|
|
33
|
-
const pushError = error instanceof
|
|
39
|
+
const pushError = error instanceof Error ? error.message : String(error);
|
|
34
40
|
options.logger.warn("Publish committed but the push failed", { revision, pushError });
|
|
35
41
|
return { state: "committed-pending-push", revision, pushError };
|
|
36
42
|
}
|
|
37
43
|
},
|
|
38
44
|
};
|
|
39
45
|
}
|
|
46
|
+
/** A push sends every commit that no remote has; the developer's own commits are theirs to send. */
|
|
47
|
+
async function refuseOtherCommits(tree) {
|
|
48
|
+
const other = (await unpushedCommits(tree))?.other ?? 0;
|
|
49
|
+
if (other === 0)
|
|
50
|
+
return;
|
|
51
|
+
throw new ApiError("conflict", `Publishing would also send ${other} change${other === 1 ? "" : "s"} that ${other === 1 ? "was" : "were"} not made in Mica ` +
|
|
52
|
+
"and that the website does not have yet. Send them with Git first (`git push`), or move them to another branch. " +
|
|
53
|
+
"Then publish again.");
|
|
54
|
+
}
|
|
40
55
|
function noUpstream(remotes) {
|
|
41
56
|
return remotes.length === 0
|
|
42
57
|
? "This repository has no remote. Add one with `git remote add origin <url>`, then publish again."
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"developer.js","sourceRoot":"","sources":["../../src/workspace/developer.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"developer.js","sourceRoot":"","sources":["../../src/workspace/developer.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The policy for the developer's own checkout. Mica never fetches, merges or\n * resets here. Publish pushes with the developer's `git`, credentials and\n * upstream; a new branch without one is pushed to `origin` and tracks it. A\n * failed push keeps the commit. A push sends only Mica's own commits: other\n * commits that no remote has make Publish refuse.\n */\n\nimport type { PublishResult } from \"@micacms/core\";\n\nimport { ApiError } from \"../lib/errors.js\";\nimport type { Logger } from \"../lib/logger.js\";\nimport type { OwnerPolicy, PublishAvailability } from \"./policy.js\";\nimport { unpushedCommits, type GitTree } from \"./tree.js\";\n\nexport interface DeveloperPolicyOptions {\n /** `MICA_LOCAL_PUBLISH`. */\n publishEnabled: boolean;\n logger: Logger;\n}\n\nexport function developerPolicy(options: DeveloperPolicyOptions): OwnerPolicy {\n return {\n owner: \"developer\",\n\n publishAvailability(): PublishAvailability {\n return options.publishEnabled\n ? { enabled: true }\n : { enabled: false, reason: \"Publishing from local mode is disabled (MICA_LOCAL_PUBLISH=false).\" };\n },\n\n async prepare(tree: GitTree) {\n await refuseOtherCommits(tree);\n },\n\n // The commit keeps the developer's own Git identity.\n commitAuthor: () => undefined,\n\n async push(tree: GitTree, revision: string): Promise<PublishResult> {\n try {\n // Checked again: a commit made in a terminal since `prepare` would go along.\n await refuseOtherCommits(tree);\n // Publishing is the request to push, so a new branch gets its upstream here.\n const upstream = await tree.git.tryRun([\"rev-parse\", \"--abbrev-ref\", \"--symbolic-full-name\", \"@{upstream}\"]);\n const remotes = (await tree.git.tryRun([\"remote\"]))?.split(\"\\n\").filter(Boolean) ?? [];\n if (upstream === null && remotes.includes(\"origin\")) await tree.git.run([\"push\", \"-u\", \"origin\", \"HEAD\"]);\n else if (upstream === null) throw new Error(noUpstream(remotes));\n else await tree.git.run([\"push\"]);\n return { state: \"published\", revision };\n } catch (error) {\n const pushError = error instanceof Error ? error.message : String(error);\n options.logger.warn(\"Publish committed but the push failed\", { revision, pushError });\n return { state: \"committed-pending-push\", revision, pushError };\n }\n },\n };\n}\n\n/** A push sends every commit that no remote has; the developer's own commits are theirs to send. */\nasync function refuseOtherCommits(tree: GitTree): Promise<void> {\n const other = (await unpushedCommits(tree))?.other ?? 0;\n if (other === 0) return;\n throw new ApiError(\n \"conflict\",\n `Publishing would also send ${other} change${other === 1 ? \"\" : \"s\"} that ${other === 1 ? \"was\" : \"were\"} not made in Mica ` +\n \"and that the website does not have yet. Send them with Git first (`git push`), or move them to another branch. \" +\n \"Then publish again.\",\n );\n}\n\nfunction noUpstream(remotes: string[]): string {\n return remotes.length === 0\n ? \"This repository has no remote. Add one with `git remote add origin <url>`, then publish again.\"\n : `This branch has no upstream. Push it once with \\`git push -u ${remotes[0]} HEAD\\`, then publish again.`;\n}\n"]}
|
|
@@ -43,7 +43,7 @@ export interface SyncStatus {
|
|
|
43
43
|
message: string;
|
|
44
44
|
since: string;
|
|
45
45
|
} | null;
|
|
46
|
-
/**
|
|
46
|
+
/** A `.gitattributes` changed upstream; a restart applies it. A new content model needs none. */
|
|
47
47
|
restartRequired: boolean;
|
|
48
48
|
/** A publication committed and not yet pushed. Mica retries it. */
|
|
49
49
|
pendingPush: {
|
|
@@ -1 +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,
|
|
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,iGAAiG;IACjG,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"}
|
|
@@ -1 +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 /**
|
|
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 /** A `.gitattributes` changed upstream; a restart applies it. A new content model needs none. */\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"]}
|
package/dist/workspace/tree.d.ts
CHANGED
|
@@ -54,8 +54,21 @@ export declare function discardChange(tree: GitTree, change: TreeChange): Promis
|
|
|
54
54
|
export declare function headRevision(tree: GitTree): Promise<string | null>;
|
|
55
55
|
/** The checked-out branch, or null when there is none to name. */
|
|
56
56
|
export declare function currentBranch(tree: GitTree): Promise<string | null>;
|
|
57
|
-
/**
|
|
58
|
-
export declare
|
|
57
|
+
/** The trailer that marks a commit that Mica made for a publication. */
|
|
58
|
+
export declare const PUBLISHED_WITH_MICA = "Published-with: Mica";
|
|
59
|
+
/** A publication's commit message: the editor's summary, and Mica's trailer. */
|
|
60
|
+
export declare function publicationMessage(summary: string): string;
|
|
61
|
+
/** Commits on HEAD that no remote has, split into Mica's publications and other commits. */
|
|
62
|
+
export interface UnpushedCommits {
|
|
63
|
+
mica: number;
|
|
64
|
+
other: number;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* What a push of HEAD would send: the commits after the upstream branch, or,
|
|
68
|
+
* for a branch without one, the commits that no remote has. Null without a
|
|
69
|
+
* remote, because then nothing can be pushed and everything would count.
|
|
70
|
+
*/
|
|
71
|
+
export declare function unpushedCommits(tree: GitTree): Promise<UnpushedCommits | null>;
|
|
59
72
|
/**
|
|
60
73
|
* The content root as `ref` has it, read from Git's object store, because the
|
|
61
74
|
* working tree holds the drafts.
|
|
@@ -1 +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,
|
|
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,wEAAwE;AACxE,eAAO,MAAM,mBAAmB,yBAAyB,CAAC;AAE1D,gFAAgF;AAChF,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,4FAA4F;AAC5F,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAgBpF;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,SAAS,GAAG,aAAa,CAyBrE;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,aAAa,EAAE,GAAG,EAAE,aAAa,EAAE,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,aAAa,CAqB7G;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"}
|
package/dist/workspace/tree.js
CHANGED
|
@@ -113,13 +113,36 @@ export function headRevision(tree) {
|
|
|
113
113
|
export function currentBranch(tree) {
|
|
114
114
|
return tree.git.tryRun(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
115
115
|
}
|
|
116
|
-
/**
|
|
117
|
-
export
|
|
118
|
-
|
|
119
|
-
|
|
116
|
+
/** The trailer that marks a commit that Mica made for a publication. */
|
|
117
|
+
export const PUBLISHED_WITH_MICA = "Published-with: Mica";
|
|
118
|
+
/** A publication's commit message: the editor's summary, and Mica's trailer. */
|
|
119
|
+
export function publicationMessage(summary) {
|
|
120
|
+
return `${summary}\n\n${PUBLISHED_WITH_MICA}`;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* What a push of HEAD would send: the commits after the upstream branch, or,
|
|
124
|
+
* for a branch without one, the commits that no remote has. Null without a
|
|
125
|
+
* remote, because then nothing can be pushed and everything would count.
|
|
126
|
+
*/
|
|
127
|
+
export async function unpushedCommits(tree) {
|
|
128
|
+
const remotes = await tree.git.tryRun(["remote"]);
|
|
129
|
+
if (!remotes)
|
|
120
130
|
return null;
|
|
121
|
-
|
|
122
|
-
|
|
131
|
+
// A commit can be on another remote branch and still be new to the upstream.
|
|
132
|
+
const upstream = await tree.git.tryRun(["rev-parse", "--verify", "--quiet", "@{upstream}"]);
|
|
133
|
+
const range = upstream ? [`${upstream}..HEAD`] : ["HEAD", "--not", "--remotes"];
|
|
134
|
+
// %x1e ends a commit, so a multi-line message stays one record.
|
|
135
|
+
const log = await tree.git.tryRun(["log", "--max-count=1000", "--format=%B%x1e", ...range]);
|
|
136
|
+
if (log === null)
|
|
137
|
+
return { mica: 0, other: 0 };
|
|
138
|
+
const counted = { mica: 0, other: 0 };
|
|
139
|
+
for (const message of log.split("\x1e")) {
|
|
140
|
+
if (message.trim() === "")
|
|
141
|
+
continue;
|
|
142
|
+
const lines = message.trim().split("\n");
|
|
143
|
+
counted[lines.includes(PUBLISHED_WITH_MICA) ? "mica" : "other"] += 1;
|
|
144
|
+
}
|
|
145
|
+
return counted;
|
|
123
146
|
}
|
|
124
147
|
/**
|
|
125
148
|
* The content root as `ref` has it, read from Git's object store, because the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tree.js","sourceRoot":"","sources":["../../src/workspace/tree.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAC;AAChD,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAGhD,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAG5C,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAyC,MAAM,UAAU,CAAC;AAEtF;;;GAGG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,qBAAqB,CAAC;AAoB7C;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,WAAmB,EACnB,UAA4B,EAAE;IAE9B,MAAM,KAAK,GAAG,SAAS,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;IAC9C,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,iBAAiB,CAAC,CAAC,CAAC;QACnE,sEAAsE;QACtE,+CAA+C;QAC/C,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,WAAW,CAAC,CAAC;QAC7C,OAAO;YACL,GAAG,EAAE,SAAS,CAAC,QAAQ,EAAE,OAAO,CAAC;YACjC,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;YAC7C,WAAW;SACZ,CAAC;IACJ,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO;YACL,MAAM,EACJ,KAAK,YAAY,QAAQ,IAAI,KAAK,CAAC,QAAQ,KAAK,IAAI;gBAClD,CAAC,CAAC,KAAK,CAAC,OAAO;gBACf,CAAC,CAAC,uDAAuD;SAC9D,CAAC;IACJ,CAAC;AACH,CAAC;AAED,6DAA6D;AAC7D,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,IAAa;IACjD,iFAAiF;IACjF,mFAAmF;IACnF,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,GAAG,CAC5B,CAAC,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,MAAM,KAAK,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,EAClG,EAAE,GAAG,EAAE,EAAE,kBAAkB,EAAE,GAAG,EAAE,EAAE,CACrC,CAAC;IACF,OAAO,WAAW,CAAC,GAAG,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;AACvC,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,IAAa,EACb,KAAe,EACf,OAAe,EACf,MAAe;IAEf,MAAM,YAAY,GAAG,MAAM,UAAU,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACnD,MAAM,EAAE,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,YAAY,MAAM,CAAC,IAAI,KAAK,MAAM,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACvE,MAAM,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,EAAE,IAAI,EAAE,GAAG,YAAY,CAAC,CAAC,CAAC;IACrF,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC;AAC7C,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,IAAa,EAAE,KAAe,EAAE,QAAgB;IAClF,MAAM,YAAY,GAAG,MAAM,UAAU,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACnD,MAAM,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,YAAY,CAAC,CAAC,CAAC;IAC/E,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC;AAC7C,CAAC;AAED;;;;GAIG;AACH,KAAK,UAAU,UAAU,CAAC,IAAa,EAAE,KAAe;IACtD,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,MAAM,IAAI,GAAa,EAAE,CAAC;IAC1B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC;IAChG,CAAC;IACD,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,EAAE,CAAC;QAC1C,MAAM,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,KAAK,CAAC,CAAC,CAAC;IAC7D,CAAC;IACD,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC;QACvC,MAAM,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,kBAAkB,EAAE,SAAS,EAAE,IAAI,EAAE,GAAG,KAAK,CAAC,CAAC,CAAC;IACjG,CAAC;IACD,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC;AACxD,CAAC;AAED,sFAAsF;AACtF,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,IAAa,EAAE,MAAkB;IACnE,IAAI,MAAM,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,kFAAkF;QAClF,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;YACtB,MAAM,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACxG,CAAC;QACD,MAAM,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACjE,OAAO;IACT,CAAC;IACD,MAAM,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;QACjB,OAAO;QACP,SAAS;QACT,eAAe;QACf,UAAU;QACV,YAAY;QACZ,IAAI;QACJ,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC;KACjC,CAAC,CAAC;AACL,CAAC;AAED,yDAAyD;AACzD,MAAM,UAAU,YAAY,CAAC,IAAa;IACxC,OAAO,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC;AAChD,CAAC;AAED,kEAAkE;AAClE,MAAM,UAAU,aAAa,CAAC,IAAa;IACzC,OAAO,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,WAAW,EAAE,cAAc,EAAE,MAAM,CAAC,CAAC,CAAC;AAChE,CAAC;AAED,8EAA8E;AAC9E,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,IAAa;IACjD,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,UAAU,EAAE,SAAS,EAAE,mBAAmB,CAAC,CAAC,CAAC;IACpF,IAAI,OAAO,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAClC,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;IAC9B,OAAO,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;AAC/C,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,IAAa,EAAE,GAAG,GAAG,MAAM;IACpD,MAAM,EAAE,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,GAAG,GAAG,IAAI,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,CAAC;IAE/F,OAAO;QACL,MAAM,EAAE,GAAG;QAEX,KAAK,CAAC,IAAI,CAAC,IAAI;YACb,mFAAmF;YACnF,OAAO,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC7C,CAAC;QAED,KAAK,CAAC,IAAI,CAAC,IAAI;YACb,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,SAAS,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YAC/D,IAAI,GAAG,KAAK,IAAI;gBAAE,OAAO,EAAE,CAAC;YAE5B,MAAM,OAAO,GAAqB,EAAE,CAAC;YACrC,KAAK,MAAM,KAAK,IAAI,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;gBACpC,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;gBAChC,IAAI,GAAG,KAAK,CAAC,CAAC;oBAAE,SAAS;gBACzB,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;gBAC/C,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;YAC7F,CAAC;YACD,OAAO,OAAO,CAAC;QACjB,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,IAAmB,EAAE,GAAkB,EAAE,KAAuB;IAC5F,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;IAEhC,OAAO;QACL,MAAM,EAAE,GAAG,IAAI,CAAC,MAAM,4BAA4B;QAElD,IAAI,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAEvE,KAAK,CAAC,IAAI,CAAC,SAAS;YAClB,MAAM,MAAM,GAAG,SAAS,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,SAAS,GAAG,CAAC;YACvD,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;YACrF,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE,CAAC;gBAC5B,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC;oBAAE,SAAS;gBACvC,MAAM,CAAC,IAAI,EAAE,GAAG,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;gBAC/D,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;oBAAE,KAAK,CAAC,GAAG,CAAC,IAAK,EAAE,EAAE,IAAI,EAAE,IAAK,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC;qBACvE,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI;oBAAE,KAAK,CAAC,MAAM,CAAC,IAAK,CAAC,CAAC;;oBACzD,KAAK,CAAC,GAAG,CAAC,IAAK,EAAE,EAAE,IAAI,EAAE,IAAK,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;YACvD,CAAC;YACD,OAAO,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;QAC7B,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,cAAc,GAAG,iBAAiB,CAAC;AAEzC;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,GAAW,EAAE,MAAc;IACrD,MAAM,MAAM,GAAG,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,EAAE,CAAC,CAAC;IAC/D,MAAM,OAAO,GAAiB,EAAE,CAAC;IAEjC,MAAM,IAAI,GAAG,CACX,QAAgB,EAChB,IAAwB,EACxB,QAAuD,EAAE,EACzD,EAAE;QACF,MAAM,IAAI,GAAG,iBAAiB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QACjD,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,OAAO,IAAI,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;YAAE,OAAO;QAC3E,IAAI,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,OAAO;QACtC,OAAO,CAAC,IAAI,CAAC;YACX,IAAI;YACJ,IAAI;YACJ,SAAS,EAAE,KAAK,CAAC,SAAS,IAAI,KAAK;YACnC,UAAU,EAAE,KAAK,CAAC,UAAU,IAAI,KAAK;SACtC,CAAC,CAAC;IACL,CAAC,CAAC;IAEF,KAAK,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE,CAAC;QAC7C,MAAM,KAAK,GAAG,MAAM,CAAC,EAAE,CAAE,CAAC;QAC1B,MAAM,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC;QAC1B,MAAM,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC;QAC1B,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAEhC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;YAC3B,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YAC9C,SAAS;QACX,CAAC;QAED,mDAAmD;QACnD,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG,CAAC,IAAI,CAAC,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG,CAAC,EAAE,CAAC;YACnF,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;YAC/C,SAAS;QACX,CAAC;QAED,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;YAC3B,+DAA+D;YAC/D,MAAM,IAAI,GAAG,MAAM,CAAC,EAAE,EAAE,CAAE,CAAC;YAC3B,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;YACzB,IAAI,CAAC,KAAK,GAAG;gBAAE,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;YACpC,SAAS;QACX,CAAC;QAED,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;YAC3B,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;YACzB,SAAS;QACX,CAAC;QAED,IAAI,CAAC,QAAQ,EAAE,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;IAClD,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,SAAS,iBAAiB,CAAC,QAAgB,EAAE,MAAc;IACzD,IAAI,MAAM,KAAK,EAAE;QAAE,OAAO,QAAQ,CAAC;IACnC,IAAI,QAAQ,KAAK,MAAM;QAAE,OAAO,IAAI,CAAC;IACrC,OAAO,QAAQ,CAAC,UAAU,CAAC,GAAG,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACtF,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,MAAM,CAAC,MAAc,EAAE,IAAY;IACjD,OAAO,MAAM,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACxE,CAAC;AAED,SAAS,OAAO,CAAC,IAAY;IAC3B,OAAO,GAAG,KAAK,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACxD,CAAC;AAED,SAAS,OAAO,CAAI,KAAU,EAAE,IAAY;IAC1C,MAAM,OAAO,GAAU,EAAE,CAAC;IAC1B,KAAK,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,KAAK,CAAC,MAAM,EAAE,EAAE,IAAI,IAAI;QAAE,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC;IACzF,OAAO,OAAO,CAAC;AACjB,CAAC","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 * `base` with `paths` taken from `top`: what a commit of exactly those paths\n * would hold. Publish checks a selection against it, not against drafts that\n * stay behind.\n */\nexport function overlaySource(base: ContentSource, top: ContentSource, paths: Iterable<string>): ContentSource {\n const selected = new Set(paths);\n\n return {\n origin: `${base.origin} with the selected changes`,\n\n read: (path) => (selected.has(path) ? top.read(path) : base.read(path)),\n\n async list(directory) {\n const prefix = directory === \"\" ? \"\" : `${directory}/`;\n const items = new Map((await base.list(directory)).map((item) => [item.name, item]));\n for (const path of selected) {\n if (!path.startsWith(prefix)) continue;\n const [name, ...deeper] = path.slice(prefix.length).split(\"/\");\n if (deeper.length > 0) items.set(name!, { name: name!, type: \"directory\" });\n else if ((await top.read(path)) === null) items.delete(name!);\n else items.set(name!, { name: name!, type: \"file\" });\n }\n return [...items.values()];\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"]}
|
|
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/** The trailer that marks a commit that Mica made for a publication. */\nexport const PUBLISHED_WITH_MICA = \"Published-with: Mica\";\n\n/** A publication's commit message: the editor's summary, and Mica's trailer. */\nexport function publicationMessage(summary: string): string {\n return `${summary}\\n\\n${PUBLISHED_WITH_MICA}`;\n}\n\n/** Commits on HEAD that no remote has, split into Mica's publications and other commits. */\nexport interface UnpushedCommits {\n mica: number;\n other: number;\n}\n\n/**\n * What a push of HEAD would send: the commits after the upstream branch, or,\n * for a branch without one, the commits that no remote has. Null without a\n * remote, because then nothing can be pushed and everything would count.\n */\nexport async function unpushedCommits(tree: GitTree): Promise<UnpushedCommits | null> {\n const remotes = await tree.git.tryRun([\"remote\"]);\n if (!remotes) return null;\n // A commit can be on another remote branch and still be new to the upstream.\n const upstream = await tree.git.tryRun([\"rev-parse\", \"--verify\", \"--quiet\", \"@{upstream}\"]);\n const range = upstream ? [`${upstream}..HEAD`] : [\"HEAD\", \"--not\", \"--remotes\"];\n // %x1e ends a commit, so a multi-line message stays one record.\n const log = await tree.git.tryRun([\"log\", \"--max-count=1000\", \"--format=%B%x1e\", ...range]);\n if (log === null) return { mica: 0, other: 0 };\n const counted = { mica: 0, other: 0 };\n for (const message of log.split(\"\\x1e\")) {\n if (message.trim() === \"\") continue;\n const lines = message.trim().split(\"\\n\");\n counted[lines.includes(PUBLISHED_WITH_MICA) ? \"mica\" : \"other\"] += 1;\n }\n return counted;\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 * `base` with `paths` taken from `top`: what a commit of exactly those paths\n * would hold. Publish checks a selection against it, not against drafts that\n * stay behind.\n */\nexport function overlaySource(base: ContentSource, top: ContentSource, paths: Iterable<string>): ContentSource {\n const selected = new Set(paths);\n\n return {\n origin: `${base.origin} with the selected changes`,\n\n read: (path) => (selected.has(path) ? top.read(path) : base.read(path)),\n\n async list(directory) {\n const prefix = directory === \"\" ? \"\" : `${directory}/`;\n const items = new Map((await base.list(directory)).map((item) => [item.name, item]));\n for (const path of selected) {\n if (!path.startsWith(prefix)) continue;\n const [name, ...deeper] = path.slice(prefix.length).split(\"/\");\n if (deeper.length > 0) items.set(name!, { name: name!, type: \"directory\" });\n else if ((await top.read(path)) === null) items.delete(name!);\n else items.set(name!, { name: name!, type: \"file\" });\n }\n return [...items.values()];\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.7.0",
|
|
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",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"hono": "^4.13.7",
|
|
38
38
|
"image-size": "^2.0.3",
|
|
39
39
|
"zod": "^4.3.6",
|
|
40
|
-
"@micacms/core": "0.
|
|
40
|
+
"@micacms/core": "0.7.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "^22.20.1",
|
package/src/app.ts
CHANGED
|
@@ -95,6 +95,11 @@ export function createApp(ctx: RuntimeContext, options: CreateAppOptions = {}):
|
|
|
95
95
|
|
|
96
96
|
// One gate and no roles: whoever the surface admits is an editor of this site.
|
|
97
97
|
for (const middleware of auth.gate) app.use("/api/admin/*", middleware);
|
|
98
|
+
// A model pushed or written since is used at once; editors cannot restart the server.
|
|
99
|
+
app.use("/api/admin/*", async (c, next) => {
|
|
100
|
+
await c.get("ctx").refreshModel();
|
|
101
|
+
await next();
|
|
102
|
+
});
|
|
98
103
|
|
|
99
104
|
app.route("/api/admin", adminSystemRoutes());
|
|
100
105
|
app.route("/api/admin", adminContentRoutes());
|
package/src/context.ts
CHANGED
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
* runtime stays authentication-neutral.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type
|
|
7
|
+
import { MANIFEST_PATH, type Site } from "@micacms/core";
|
|
8
8
|
|
|
9
9
|
import type { RuntimeConfig } from "./config.js";
|
|
10
10
|
import { createLogger, type Logger } from "./lib/logger.js";
|
|
11
11
|
import { createRateLimiter, type RateLimiter } from "./lib/rate-limit.js";
|
|
12
|
-
import { readOnlyParts } from "./schema.js";
|
|
12
|
+
import { loadSchema, readOnlyParts, SchemaError } from "./schema.js";
|
|
13
13
|
import { createMediaService, type MediaService } from "./services/media.js";
|
|
14
14
|
import type { ContentStagingService } from "./services/changes.js";
|
|
15
15
|
import { createContentStore, type ContentStore } from "./services/content-store.js";
|
|
@@ -23,7 +23,7 @@ import type { GitTree } from "./workspace/tree.js";
|
|
|
23
23
|
export interface SiteContext {
|
|
24
24
|
/** Identifies this site in logs. */
|
|
25
25
|
id: string;
|
|
26
|
-
/** The content model
|
|
26
|
+
/** The content model in use: read at startup, and again when `.mica/schema.json` changes. */
|
|
27
27
|
model: Site;
|
|
28
28
|
/** The working tree's files behind the workspace locks. Only Publish makes a commit. */
|
|
29
29
|
contentRepository: ContentFiles;
|
|
@@ -34,6 +34,8 @@ export interface SiteContext {
|
|
|
34
34
|
schemaRevision: string | null;
|
|
35
35
|
/** Collections and singletons this build cannot write, from `readOnlyParts`, with the reason. */
|
|
36
36
|
readOnly: ReadonlyMap<string, string>;
|
|
37
|
+
/** Why the newer model in the working tree is not in use. The model above stays in use. */
|
|
38
|
+
modelWarning: { message: string; revision: string | null } | null;
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
export const DEFAULT_SITE_ID = "default";
|
|
@@ -74,6 +76,12 @@ export interface RuntimeContext {
|
|
|
74
76
|
};
|
|
75
77
|
operational(): OperationalInfo;
|
|
76
78
|
health: RuntimeHealth;
|
|
79
|
+
/**
|
|
80
|
+
* Uses a new content model when `.mica/schema.json` changed, and keeps the one
|
|
81
|
+
* in use when the new one cannot be read. Checks at most every two seconds,
|
|
82
|
+
* unless `force`.
|
|
83
|
+
*/
|
|
84
|
+
refreshModel(options?: { force?: boolean }): Promise<void>;
|
|
77
85
|
shutdown(): Promise<void>;
|
|
78
86
|
}
|
|
79
87
|
|
|
@@ -94,6 +102,8 @@ export interface CreateRuntimeContextOptions {
|
|
|
94
102
|
locks?: WorkspaceLocks;
|
|
95
103
|
/** See `WorkingTreeStagingDeps.tree`. */
|
|
96
104
|
tree?: GitTree;
|
|
105
|
+
/** Called after a new model is in use, for what the app keeps beside the runtime. */
|
|
106
|
+
onModel?: (model: Site) => void;
|
|
97
107
|
onShutdown?: () => void | Promise<void>;
|
|
98
108
|
}
|
|
99
109
|
|
|
@@ -112,35 +122,73 @@ export function createRuntimeContext(options: CreateRuntimeContextOptions): Runt
|
|
|
112
122
|
// Services write through the lock wrapper; staging takes the locks itself.
|
|
113
123
|
const locks = options.locks ?? createWorkspaceLocks();
|
|
114
124
|
|
|
115
|
-
const readOnly = readOnlyParts(model);
|
|
116
|
-
for (const reason of readOnly.values()) logger.warn("Read-only in this version of Mica", { reason });
|
|
117
|
-
|
|
118
125
|
const site: SiteContext = {
|
|
119
126
|
id: options.siteId ?? DEFAULT_SITE_ID,
|
|
120
127
|
model,
|
|
121
128
|
contentRepository: lockedFiles(options.files, locks.tree),
|
|
122
129
|
schemaRevision,
|
|
123
|
-
readOnly,
|
|
130
|
+
readOnly: new Map(),
|
|
131
|
+
modelWarning: null,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/** The services that read the model. Media does not. */
|
|
135
|
+
const forModel = (current: Site, revision: string | null) => {
|
|
136
|
+
const readOnly = readOnlyParts(current);
|
|
137
|
+
for (const reason of readOnly.values()) logger.warn("Read-only in this version of Mica", { reason });
|
|
138
|
+
site.model = current;
|
|
139
|
+
site.schemaRevision = revision;
|
|
140
|
+
site.readOnly = readOnly;
|
|
141
|
+
return {
|
|
142
|
+
content: createContentStore(current, site.contentRepository, readOnly),
|
|
143
|
+
staging: createWorkingTreeStagingService({
|
|
144
|
+
model: current,
|
|
145
|
+
files: options.files,
|
|
146
|
+
contentRoot: config.contentRoot,
|
|
147
|
+
schemaRevision: revision,
|
|
148
|
+
readOnly,
|
|
149
|
+
publishEnabled: config.MICA_LOCAL_PUBLISH,
|
|
150
|
+
logger,
|
|
151
|
+
locks,
|
|
152
|
+
...(options.policy ? { policy: options.policy } : {}),
|
|
153
|
+
...(options.tree ? { tree: options.tree } : {}),
|
|
154
|
+
}),
|
|
155
|
+
};
|
|
124
156
|
};
|
|
125
157
|
|
|
158
|
+
// Routes read `ctx.services` on each request, so a reload replaces these in place.
|
|
126
159
|
const services = {
|
|
127
|
-
|
|
160
|
+
...forModel(model, schemaRevision),
|
|
128
161
|
media: createMediaService({
|
|
129
162
|
files: site.contentRepository,
|
|
130
163
|
maxBytes: config.MICA_MAX_UPLOAD_BYTES,
|
|
131
164
|
}),
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
let checkedAt = 0;
|
|
168
|
+
let checking: Promise<void> | null = null;
|
|
169
|
+
const refreshModel = (refresh: { force?: boolean } = {}): Promise<void> => {
|
|
170
|
+
// A forced check must read the files as they are now, not join one that started earlier.
|
|
171
|
+
if (checking) return refresh.force ? checking.then(() => refreshModel(refresh)) : checking;
|
|
172
|
+
if (!refresh.force && Date.now() - checkedAt < 2_000) return Promise.resolve();
|
|
173
|
+
checking = (async () => {
|
|
174
|
+
checkedAt = Date.now();
|
|
175
|
+
const revision = await options.files.revision(MANIFEST_PATH);
|
|
176
|
+
if (revision === site.schemaRevision || revision === site.modelWarning?.revision) return;
|
|
177
|
+
try {
|
|
178
|
+
const next = await loadSchema(options.files);
|
|
179
|
+
Object.assign(services, forModel(next, revision));
|
|
180
|
+
site.modelWarning = null;
|
|
181
|
+
logger.info("Using the new content model", { revision });
|
|
182
|
+
options.onModel?.(next);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
if (!(error instanceof SchemaError)) throw error;
|
|
185
|
+
site.modelWarning = { message: error.message, revision };
|
|
186
|
+
logger.warn("The new content model cannot be read; the one in use stays", { message: error.message });
|
|
187
|
+
}
|
|
188
|
+
})().finally(() => {
|
|
189
|
+
checking = null;
|
|
190
|
+
});
|
|
191
|
+
return checking;
|
|
144
192
|
};
|
|
145
193
|
|
|
146
194
|
const limiters = {
|
|
@@ -158,6 +206,7 @@ export function createRuntimeContext(options: CreateRuntimeContextOptions): Runt
|
|
|
158
206
|
options.operational ??
|
|
159
207
|
(() => ({ auth: "unspecified" })),
|
|
160
208
|
health: options.health ?? {},
|
|
209
|
+
refreshModel,
|
|
161
210
|
|
|
162
211
|
async shutdown() {
|
|
163
212
|
limiters.api.stop();
|
package/src/http.ts
CHANGED
|
@@ -44,3 +44,9 @@ export function actorOf(c: Context<Env>): { id: string; email: string; name: str
|
|
|
44
44
|
const identity = c.get("access")?.identity;
|
|
45
45
|
return identity ? { id: identity.subject, email: identity.email, name: identity.name } : null;
|
|
46
46
|
}
|
|
47
|
+
|
|
48
|
+
/** Who did it, for the log: the signed-in editor's email, else their subject. None in the local editor. */
|
|
49
|
+
export function editorOf(c: Context<Env>): string | undefined {
|
|
50
|
+
const actor = actorOf(c);
|
|
51
|
+
return actor ? actor.email || actor.id : undefined;
|
|
52
|
+
}
|
package/src/lib/file-type.ts
CHANGED
|
@@ -92,9 +92,12 @@ export const isImageType = (mimeType: string): boolean => IMAGE_TYPES.has(mimeTy
|
|
|
92
92
|
export function isAccepted(mimeType: string, accept: "image" | "document" | "any"): boolean {
|
|
93
93
|
if (accept === "image") return IMAGE_TYPES.has(mimeType);
|
|
94
94
|
if (accept === "document") return DOCUMENT_TYPES.has(mimeType);
|
|
95
|
-
return IMAGE_TYPES.has(mimeType) || DOCUMENT_TYPES.has(mimeType)
|
|
95
|
+
return IMAGE_TYPES.has(mimeType) || DOCUMENT_TYPES.has(mimeType);
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
/** A video is large for a Git repository, and the media list cannot show one. */
|
|
99
|
+
export const VIDEO_TYPE = "video/mp4";
|
|
100
|
+
|
|
98
101
|
/**
|
|
99
102
|
* SVG can carry scripts, so Mica neither stores nor shows it (#86). It is still
|
|
100
103
|
* detected, so that the refusal can say why.
|
|
@@ -11,7 +11,7 @@ import { Hono } from "hono";
|
|
|
11
11
|
import { z } from "zod";
|
|
12
12
|
|
|
13
13
|
import { ApiError, badRequest, notFound } from "../lib/errors.js";
|
|
14
|
-
import type
|
|
14
|
+
import { editorOf, type Env } from "../http.js";
|
|
15
15
|
|
|
16
16
|
const saveSchema = z.object({
|
|
17
17
|
data: z.record(z.string(), z.unknown()),
|
|
@@ -21,6 +21,10 @@ const saveSchema = z.object({
|
|
|
21
21
|
* exist, so two parallel creates cannot overwrite.
|
|
22
22
|
*/
|
|
23
23
|
revision: z.string().nullable().optional(),
|
|
24
|
+
/** The editor saw that a colleague saved since, and chose to save over it. Logged. */
|
|
25
|
+
keepMine: z.boolean().optional(),
|
|
26
|
+
/** On a URL change: other entries and settings pages that link here follow the new URL. */
|
|
27
|
+
updateLinks: z.boolean().optional(),
|
|
24
28
|
});
|
|
25
29
|
|
|
26
30
|
const reorderSchema = z.object({ slugs: z.array(z.string().min(1)).max(2000) });
|
|
@@ -140,8 +144,15 @@ export function adminContentRoutes(): Hono<Env> {
|
|
|
140
144
|
data: parsed.data.data,
|
|
141
145
|
slug: c.req.param("slug"),
|
|
142
146
|
expectedRevision: parsed.data.revision,
|
|
147
|
+
...(parsed.data.updateLinks ? { updateLinks: true } : {}),
|
|
143
148
|
}),
|
|
144
149
|
);
|
|
150
|
+
if (parsed.data.keepMine) {
|
|
151
|
+
c.get("ctx").logger.info("Kept own version over a newer save", {
|
|
152
|
+
entry: `${c.req.param("collection")}/${c.req.param("slug")}`,
|
|
153
|
+
editor: editorOf(c),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
145
156
|
|
|
146
157
|
return c.json(saved);
|
|
147
158
|
});
|
|
@@ -182,13 +193,16 @@ export function adminContentRoutes(): Hono<Env> {
|
|
|
182
193
|
|
|
183
194
|
app.put("/singletons/:name", async (c) => {
|
|
184
195
|
const { content } = c.get("ctx").services;
|
|
185
|
-
const body = (await jsonBody(c)) as { data?: unknown; revision?: string | null };
|
|
196
|
+
const body = (await jsonBody(c)) as { data?: unknown; revision?: string | null; keepMine?: boolean };
|
|
186
197
|
|
|
187
198
|
const saved = await handled(() =>
|
|
188
199
|
content.saveSingleton(c.req.param("name"), body?.data ?? {}, {
|
|
189
200
|
expectedRevision: body?.revision,
|
|
190
201
|
}),
|
|
191
202
|
);
|
|
203
|
+
if (body?.keepMine === true) {
|
|
204
|
+
c.get("ctx").logger.info("Kept own version over a newer save", { singleton: c.req.param("name"), editor: editorOf(c) });
|
|
205
|
+
}
|
|
192
206
|
|
|
193
207
|
return c.json(saved);
|
|
194
208
|
});
|