@jxsuite/protocol 6.2.1 → 6.2.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/protocol",
3
- "version": "6.2.1",
3
+ "version": "6.2.3",
4
4
  "description": "The Studio Backend Protocol — wire types and the canonical route table every Jx Studio backend implements",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -14,6 +14,7 @@
14
14
  "type": "module",
15
15
  "exports": {
16
16
  ".": "./src/index.ts",
17
+ "./git-scope": "./src/git-scope.ts",
17
18
  "./problem": "./src/problem.ts",
18
19
  "./problems": "./src/problems.ts",
19
20
  "./routes": "./src/routes.ts",
@@ -27,6 +28,6 @@
27
28
  "test:coverage": "bun test --isolate --coverage"
28
29
  },
29
30
  "dependencies": {
30
- "@jxsuite/schema": "^5.1.0"
31
+ "@jxsuite/schema": "^5.3.0"
31
32
  }
32
33
  }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * What a project nested inside a larger repository sees of it.
3
+ *
4
+ * Git answers in two coordinate systems. `status` and `diff --name-only` print paths from the
5
+ * REPOSITORY ROOT whatever the cwd is (porcelain ignores `status.relativePaths`), while `add`,
6
+ * `restore`, `checkout` and the `readFile` Studio pairs them with read paths from the PROJECT ROOT.
7
+ * Handed straight across, a site at `Sites/acme/` asked git to stage
8
+ * `Sites/acme/Sites/acme/pages/index.md`.
9
+ *
10
+ * So every path a backend gives Studio is project-relative, and a project's status and commit cover
11
+ * that project (`desktop.md` §5.1, `studio.md` §22.8). Both backends run these same rules so they
12
+ * cannot disagree about one working tree. At a repository's root the prefix is empty and each of
13
+ * them is the identity.
14
+ *
15
+ * @module
16
+ * @docs extending/embedding/platform-adapter
17
+ */
18
+
19
+ /** Runs one git command in the project root, resolving stdout and rejecting on a non-zero exit. */
20
+ export type GitRunner = (args: string[]) => Promise<string>;
21
+
22
+ /** `git rev-parse --show-prefix`: the project's folder within its repository, `""` at the root. */
23
+ export async function readGitPrefix(run: GitRunner): Promise<string> {
24
+ const out = await run(["rev-parse", "--show-prefix"]);
25
+ // A prefix is empty or ends in `/`, so this can only remove the newline git adds.
26
+ return out.trimEnd();
27
+ }
28
+
29
+ /**
30
+ * A root-relative path git printed, as the project-relative path every other verb reads.
31
+ *
32
+ * A path outside the project stays as git printed it: it has no project-relative spelling the file
33
+ * routes would accept, and a `conflicts` list still has to name it.
34
+ *
35
+ * @param {string} prefix - From {@link readGitPrefix}
36
+ * @param {string} path - Relative to the repository root
37
+ * @returns {string}
38
+ */
39
+ export function projectRelativePath(prefix: string, path: string): string {
40
+ return path.startsWith(prefix) ? path.slice(prefix.length) : path;
41
+ }
42
+
43
+ /**
44
+ * The `git commit` that publishes THIS project's staged changes and nothing else.
45
+ *
46
+ * Review changes lists only the project, and a commit made from a subfolder still takes the whole
47
+ * index. In a repository of several sites, where each one's changes are staged the first time its
48
+ * Review changes shows them, a plain commit would carry another site's changes under this site's
49
+ * summary. So those stay staged, for the project they belong to:
50
+ *
51
+ * - **At the root**, everything is the project: the index, or every tracked change when nothing is
52
+ * staged (the route's "staged, else all dirty").
53
+ * - **During a merge** the commit is the merge, which git refuses to split, and which is the
54
+ * repository's rather than the project's.
55
+ * - **Nothing of the project's staged**: its own tracked changes, never `-a`, which reaches past it.
56
+ * - **Only the project's staged**: the index, exactly.
57
+ * - **Others staged too**: `--only` the project's paths. Those paths are committed from the working
58
+ * tree rather than the index, so this is reserved for the case that needs it.
59
+ *
60
+ * `--no-renames` because a staged rename's old path has to be committed too, or `--only` would keep
61
+ * it in the tree and turn the rename into a copy; `--no-relative` because `diff.relative` would
62
+ * otherwise hide exactly the paths this is looking for.
63
+ *
64
+ * @param {GitRunner} run - Runs git in the project root
65
+ * @param {string} message - The commit message
66
+ * @returns {Promise<string[]>} The argv to hand git
67
+ */
68
+ export async function projectCommitArgs(run: GitRunner, message: string): Promise<string[]> {
69
+ const [prefix, stagedOut, merging] = await Promise.all([
70
+ readGitPrefix(run),
71
+ run(["diff", "--cached", "--name-only", "--no-renames", "--no-relative", "-z"]),
72
+ run(["rev-parse", "--quiet", "--verify", "MERGE_HEAD"]).then(
73
+ () => true,
74
+ () => false,
75
+ ),
76
+ ]);
77
+ const commit = ["commit", "-m", message];
78
+ const staged = stagedOut.split("\0").filter(Boolean);
79
+ if (prefix === "") {
80
+ return staged.length > 0 ? commit : [...commit, "-a"];
81
+ }
82
+ if (merging) {
83
+ return commit;
84
+ }
85
+ const own = staged
86
+ .filter((path) => path.startsWith(prefix))
87
+ .map((path) => path.slice(prefix.length));
88
+ if (own.length === 0) {
89
+ return [...commit, "--", "."];
90
+ }
91
+ return own.length === staged.length ? commit : [...commit, "--only", "--", ...own];
92
+ }
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  * @license MIT
6
6
  */
7
7
 
8
+ export * from "./git-scope";
8
9
  export * from "./problem";
9
10
  export * from "./problems";
10
11
  export * from "./routes";
package/src/routes.ts CHANGED
@@ -371,12 +371,20 @@ export const STUDIO_ROUTES = {
371
371
  ),
372
372
 
373
373
  // ─── Git ──────────────────────────────────────────────────────────────────
374
- gitStatus: route("GET", "/__studio/git/status", "Working-tree status (GitStatusResult)"),
374
+ gitStatus: route(
375
+ "GET",
376
+ "/__studio/git/status",
377
+ "The project's working-tree status (GitStatusResult), project-relative",
378
+ ),
375
379
  gitBranches: route("GET", "/__studio/git/branches", "Branch list (GitBranchesResult)"),
376
380
  gitLog: route("GET", "/__studio/git/log", "Recent commits (GitLogEntry[])"),
377
381
  gitStage: route("POST", "/__studio/git/stage", "Stage files"),
378
382
  gitUnstage: route("POST", "/__studio/git/unstage", "Unstage files"),
379
- gitCommit: route("POST", "/__studio/git/commit", "Commit staged (else all dirty) files"),
383
+ gitCommit: route(
384
+ "POST",
385
+ "/__studio/git/commit",
386
+ "Commit the project's staged (else all its dirty) files",
387
+ ),
380
388
  gitPush: route("POST", "/__studio/git/push", "Push (cloud: sync check — commits land on push)"),
381
389
  gitPull: route("POST", "/__studio/git/pull", "Pull/fast-forward; 409 {conflicts} on overlap"),
382
390
  gitFetch: route("POST", "/__studio/git/fetch", "Refresh remote tracking state"),
package/src/types.ts CHANGED
@@ -206,6 +206,10 @@ export interface GitFileStatus {
206
206
  * code.
207
207
  */
208
208
  status: string;
209
+ /**
210
+ * Relative to the PROJECT root, never the repository's: the path every other git verb and file
211
+ * route reads. A project nested in a larger repository lists only its own changes.
212
+ */
209
213
  path: string;
210
214
  /** Whether this change is in the index. Absent reads as unstaged. */
211
215
  staged?: boolean;