my-durable 0.0.22 → 0.0.24
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/apps/dpi-backend/src/git.ts +56 -4
- package/apps/dpi-backend/src/routes.ts +7 -2
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/assets/index-BLV50tg7.js +78 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/assets/index-CpVvdDil.css +1 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/index.html +2 -2
- package/apps/dpi-playground/apps/dpi-playground-fe/src/App.vue +2 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/git-store.ts +101 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/session-git.ts +28 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/GitChip.vue +22 -159
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/GitPanel.vue +278 -137
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TreeRow.vue +6 -0
- package/apps/dpi-sdk/src/client.ts +17 -2
- package/apps/dpi-sdk/src/protocol/rest-routes.ts +1 -0
- package/apps/dpi-tui/src/app.ts +25 -2
- package/apps/dpi-tui/src/views.ts +7 -3
- package/package.json +1 -1
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/assets/index-D5Ehu13_.css +0 -1
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/assets/index-D7bC_-V3.js +0 -80
|
@@ -57,6 +57,19 @@ export interface GitStatus {
|
|
|
57
57
|
subject?: string;
|
|
58
58
|
worktreeOf?: string;
|
|
59
59
|
worktrees?: { path: string; branch: string | null; head: string }[];
|
|
60
|
+
/** Changed files (porcelain XY + path), capped — the IntelliJ-style commit
|
|
61
|
+
* dialog checks exactly these. */
|
|
62
|
+
files?: { path: string; x: string; y: string }[];
|
|
63
|
+
/** Local branches for pickers — nobody should TYPE a name that exists. */
|
|
64
|
+
branches?: { name: string; current?: boolean; upstream?: string }[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** One status for MANY paths — what a session list needs. The per-path cache
|
|
68
|
+
* keeps repeat polls cheap on big repos. */
|
|
69
|
+
export async function gitStatusBatch(paths: string[]): Promise<Record<string, GitStatus>> {
|
|
70
|
+
const unique = [...new Set(paths.filter(Boolean))];
|
|
71
|
+
const entries = await Promise.all(unique.map(async (p) => [p, await gitStatus(p)] as const));
|
|
72
|
+
return Object.fromEntries(entries);
|
|
60
73
|
}
|
|
61
74
|
|
|
62
75
|
/** Structured status for a folder: everything the UIs show, one call. */
|
|
@@ -73,12 +86,13 @@ export async function gitStatus(path: string): Promise<GitStatus> {
|
|
|
73
86
|
out.root = root;
|
|
74
87
|
// Everything after toplevel runs in PARALLEL — status is the long pole
|
|
75
88
|
// (20s+ on big Windows checkouts) and must not wait for four fast calls.
|
|
76
|
-
const [branchP, headP, statusP, gitDirP, wlP] = await Promise.allSettled([
|
|
89
|
+
const [branchP, headP, statusP, gitDirP, wlP, branchesP] = await Promise.allSettled([
|
|
77
90
|
git(path, ["branch", "--show-current"]),
|
|
78
91
|
git(path, ["log", "-1", "--pretty=%h%x09%s"]),
|
|
79
92
|
git(path, ["--no-optional-locks", "status", "--porcelain=v1", "-b"], GIT_SLOW_TIMEOUT),
|
|
80
93
|
git(path, ["rev-parse", "--git-dir"]),
|
|
81
94
|
git(path, ["worktree", "list", "--porcelain"], GIT_SLOW_TIMEOUT),
|
|
95
|
+
git(path, ["for-each-ref", "refs/heads", "--format=%(refname:short)%09%(upstream:short)%09%(if)%(HEAD)%(then)1%(else)0%(end)"]),
|
|
82
96
|
]);
|
|
83
97
|
const branch = branchP.status === "fulfilled" ? branchP.value : "";
|
|
84
98
|
out.branch = branch.trim() || undefined;
|
|
@@ -107,6 +121,21 @@ export async function gitStatus(path: string): Promise<GitStatus> {
|
|
|
107
121
|
}
|
|
108
122
|
// Per line — a ^-anchored regex on the joined blob only ever sees line 1.
|
|
109
123
|
const body = lines.slice(1).filter(Boolean);
|
|
124
|
+
out.files = body.slice(0, 500).map((l) => ({
|
|
125
|
+
path: l.slice(3).replace(/^"|"$/g, ""),
|
|
126
|
+
x: l[0] ?? " ",
|
|
127
|
+
y: l[1] ?? " ",
|
|
128
|
+
}));
|
|
129
|
+
if (branchesP.status === "fulfilled")
|
|
130
|
+
out.branches = branchesP.value
|
|
131
|
+
.split("\n")
|
|
132
|
+
.filter(Boolean)
|
|
133
|
+
.slice(0, 300)
|
|
134
|
+
.map((l) => {
|
|
135
|
+
const [name, upstream, head] = l.split("\t");
|
|
136
|
+
return { name: name ?? "", current: head === "1", upstream: upstream || undefined };
|
|
137
|
+
})
|
|
138
|
+
.filter((b) => b.name);
|
|
110
139
|
out.staged = count(body.join("\n"), /^[MADRC]/gm);
|
|
111
140
|
out.unstaged = count(body.join("\n"), /^ [MD]/gm);
|
|
112
141
|
out.untracked = count(body.join("\n"), /^\?\?/gm);
|
|
@@ -138,6 +167,7 @@ export async function gitStatus(path: string): Promise<GitStatus> {
|
|
|
138
167
|
export type GitOp =
|
|
139
168
|
| "diff"
|
|
140
169
|
| "log"
|
|
170
|
+
| "log-list"
|
|
141
171
|
| "fetch"
|
|
142
172
|
| "pull"
|
|
143
173
|
| "switch"
|
|
@@ -154,14 +184,31 @@ const safeRef = (s: string) => /^[\w./@+-]+$/.test(s) && !s.includes("..");
|
|
|
154
184
|
export async function gitOp(
|
|
155
185
|
path: string,
|
|
156
186
|
op: GitOp,
|
|
157
|
-
opts: { ref?: string; message?: string; target?: string; branch?: string } = {},
|
|
158
|
-
): Promise<{ text: string }> {
|
|
159
|
-
const { ref, message, target, branch } = opts;
|
|
187
|
+
opts: { ref?: string; message?: string; target?: string; branch?: string; files?: string[] } = {},
|
|
188
|
+
): Promise<{ text: string; commits?: { hash: string; refs: string; author: string; date: string; subject: string }[] }> {
|
|
189
|
+
const { ref, message, target, branch, files } = opts;
|
|
160
190
|
switch (op) {
|
|
161
191
|
case "diff":
|
|
162
192
|
return { text: await git(path, ["--no-pager", "diff", ...(ref && safeRef(ref) ? [ref] : [])]) };
|
|
163
193
|
case "log":
|
|
164
194
|
return { text: (await git(path, ["--no-pager", "log", "--oneline", "-n", "30", ...(ref && safeRef(ref) ? [ref] : [])])).trimEnd() };
|
|
195
|
+
case "log-list": {
|
|
196
|
+
// IntelliJ's log window: hash, refs, author, date, subject — structured.
|
|
197
|
+
const raw = await git(
|
|
198
|
+
path,
|
|
199
|
+
["--no-pager", "log", "-n", "50", "--date=short", "--pretty=format:%h%x1f%D%x1f%an%x1f%ad%x1f%s"],
|
|
200
|
+
);
|
|
201
|
+
return {
|
|
202
|
+
text: "",
|
|
203
|
+
commits: raw
|
|
204
|
+
.split("\n")
|
|
205
|
+
.filter(Boolean)
|
|
206
|
+
.map((l) => {
|
|
207
|
+
const [hash, refs, author, date, subject] = l.split("\x1f");
|
|
208
|
+
return { hash: hash ?? "", refs: refs ?? "", author: author ?? "", date: date ?? "", subject: subject ?? "" };
|
|
209
|
+
}),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
165
212
|
case "fetch":
|
|
166
213
|
return { text: (await git(path, ["fetch", "--all", "--prune"])).trim() || "fetched" };
|
|
167
214
|
case "pull":
|
|
@@ -175,6 +222,11 @@ export async function gitOp(
|
|
|
175
222
|
case "commit": {
|
|
176
223
|
const msg = (message ?? "").trim();
|
|
177
224
|
if (!msg) throw Error("commit needs a message");
|
|
225
|
+
// IntelliJ-style: chosen files only, when the caller picked some.
|
|
226
|
+
if (files?.length) {
|
|
227
|
+
for (const f of files) if (f.includes("\0") || f === "") throw Error("bad file path");
|
|
228
|
+
return { text: (await git(path, ["commit", "-m", msg, "--", ...files])).split("\n")[0]?.trim() ?? "committed" };
|
|
229
|
+
}
|
|
178
230
|
return { text: (await git(path, ["commit", "-am", msg])).split("\n")[0]?.trim() ?? "committed" };
|
|
179
231
|
}
|
|
180
232
|
case "worktree-list":
|
|
@@ -10,8 +10,8 @@ import type { DevicePairing } from "./pairing.js";
|
|
|
10
10
|
|
|
11
11
|
type Async = (req: Request, res: Response) => Promise<unknown>;
|
|
12
12
|
const route = (fn: Async) => (req: Request, res: Response, next: NextFunction) => fn(req, res).catch(next);
|
|
13
|
-
import { gitOp, gitStatus, type GitOp } from "./git.ts";
|
|
14
|
-
const GIT_OPS: GitOp[] = ["diff", "log", "fetch", "pull", "switch", "branch", "commit", "worktree-list", "worktree-add", "worktree-remove"];
|
|
13
|
+
import { gitOp, gitStatus, gitStatusBatch, type GitOp } from "./git.ts";
|
|
14
|
+
const GIT_OPS: GitOp[] = ["diff", "log", "log-list", "fetch", "pull", "switch", "branch", "commit", "worktree-list", "worktree-add", "worktree-remove"];
|
|
15
15
|
|
|
16
16
|
interface Locals {
|
|
17
17
|
row: SessionRow;
|
|
@@ -124,6 +124,10 @@ export function v1(
|
|
|
124
124
|
// Git, spoken by the device itself: status for the UIs, ops for the humans.
|
|
125
125
|
// The path must live under a start dir (same rule as every other fs route).
|
|
126
126
|
r.get("/git/status", route(async (req, res) => res.json(await gitStatus(String(req.query.path ?? ".")))));
|
|
127
|
+
r.post("/git/status-batch", route(async (req, res) => {
|
|
128
|
+
const paths = Array.isArray(req.body?.paths) ? req.body.paths.map(String).slice(0, 50) : [];
|
|
129
|
+
res.json(await gitStatusBatch(paths));
|
|
130
|
+
}));
|
|
127
131
|
r.post("/git", route(async (req, res) => {
|
|
128
132
|
const body = req.body ?? {};
|
|
129
133
|
const op = String(body.op ?? "") as GitOp;
|
|
@@ -133,6 +137,7 @@ export function v1(
|
|
|
133
137
|
branch: body.branch,
|
|
134
138
|
message: body.message,
|
|
135
139
|
target: body.target,
|
|
140
|
+
files: Array.isArray(body.files) ? body.files.map(String).slice(0, 500) : undefined,
|
|
136
141
|
});
|
|
137
142
|
res.json(out);
|
|
138
143
|
}));
|