clavarium 0.2.0-next.1 → 0.2.0-next.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/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/server/api.mjs +4 -0
- package/server/gitpush.mjs +63 -0
- package/server/main.mjs +4 -0
- package/web/app.js +544 -238
- package/web/gitgraph.js +116 -0
- package/web/index.html +135 -134
- package/web/links.js +6 -2
- package/web/style.css +197 -4
- package/web/vendor/diff2html/LICENSE +49 -0
- package/web/vendor/diff2html/diff2html-ui-slim.min.js +1 -0
- package/web/vendor/diff2html/diff2html.min.css +1 -0
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clavarium",
|
|
3
|
-
"version": "0.2.0-next.
|
|
3
|
+
"version": "0.2.0-next.3",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "clavarium",
|
|
9
|
-
"version": "0.2.0-next.
|
|
9
|
+
"version": "0.2.0-next.3",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/package.json
CHANGED
package/server/api.mjs
CHANGED
|
@@ -37,6 +37,8 @@
|
|
|
37
37
|
* POST /api/updates/apply { version }: update to the version that is out (in the background)
|
|
38
38
|
* POST /api/updates/back { version }: go back to a release that is kept (in the background)
|
|
39
39
|
* GET /api/git/<name>/outgoing?target=<branch>&branch=<branch> what would go from the agent's branch, to that branch of the remote
|
|
40
|
+
* GET /api/git/<name>/graph the branch diagram: branches and their last commits
|
|
41
|
+
* GET /api/git/<name>/commit?id=<sha> one commit, as a review shows it
|
|
40
42
|
* POST /api/git/<name>/push {head, target?, sure?}: exactly that reviewed commit, to exactly that branch
|
|
41
43
|
* GET /api/git/<name>/keep origin kept up to date, checks, automatic sending, log
|
|
42
44
|
* POST /api/git/<name>/keep {refreshMinutes?, alert?, checkCommand?, checkSeconds?, runHook?, requireForSend?, fsck?}
|
|
@@ -215,6 +217,8 @@ export async function handleApi(request, response, conversations, headers, resou
|
|
|
215
217
|
if (answered && answerer) return send(200, answerer.log(decodeURIComponent(answered[1]))), true;
|
|
216
218
|
/* A repository: where its commits go, and what would go now. */
|
|
217
219
|
const repo = /^\/api\/git\/([a-z0-9-]{1,32})(\/outgoing|\/keep)?$/.exec(pathname);
|
|
220
|
+
const drawn = /^\/api\/git\/([a-z0-9-]{1,32})\/(graph|commit)$/.exec(pathname);
|
|
221
|
+
if (drawn && gitReview) return send(200, drawn[2] === 'graph' ? await gitReview.graph(drawn[1]) : await gitReview.commit(drawn[1], query.get('id'))), true;
|
|
218
222
|
if (repo && repo[2] === '/keep' && gitKeeper) return send(200, gitKeeper.about(repo[1])), true;
|
|
219
223
|
if (repo && gitReview) return send(200, repo[2] ? await gitReview.outgoing(repo[1], { target: query.get('target') ?? undefined, branch: query.get('branch') ?? undefined }) : gitReview.settings(repo[1])), true;
|
|
220
224
|
const watched = /^\/api\/conversations\/([^/]+)\/signin$/.exec(pathname);
|
package/server/gitpush.mjs
CHANGED
|
@@ -40,6 +40,9 @@ export const UNCHANGED_MAX = 50;
|
|
|
40
40
|
/* A commit message is shown up to this many characters, and the cut is said. */
|
|
41
41
|
export const MESSAGE_MAX = 4000;
|
|
42
42
|
export const REVIEWS_KEPT = 20;
|
|
43
|
+
/* The branch diagram: so many commits, so many of the remote's branches. */
|
|
44
|
+
export const GRAPH_COMMITS = 40;
|
|
45
|
+
export const GRAPH_BRANCHES = 20;
|
|
43
46
|
/* How long a review waits for the reference, and a send or a refresh. */
|
|
44
47
|
export const REVIEW_WAIT_MS = 10_000;
|
|
45
48
|
export const SEND_WAIT_MS = 60_000;
|
|
@@ -408,6 +411,66 @@ export class GitReview {
|
|
|
408
411
|
} finally { got.remove(); }
|
|
409
412
|
}
|
|
410
413
|
|
|
414
|
+
/* The workspace of one read, with the remote's history lent to it when
|
|
415
|
+
there is one, for `work(ws, remoteTips)`; deleted after. */
|
|
416
|
+
async #withRead(r, work) {
|
|
417
|
+
const got = await this.#read(r);
|
|
418
|
+
try {
|
|
419
|
+
let release = null; let remoteTips = new Map();
|
|
420
|
+
if (this.#remoteOf(r) && this.#resources.hasOrigin(r.name)) {
|
|
421
|
+
release = await this.#runner.lock(this.#reference(r.name), REVIEW_WAIT_MS);
|
|
422
|
+
this.#runner.borrow(got.path, this.#reference(r.name));
|
|
423
|
+
remoteTips = await this.#tipsIn(this.#reference(r.name));
|
|
424
|
+
}
|
|
425
|
+
try { return await work(got, remoteTips); } finally { release?.(); }
|
|
426
|
+
} finally { got.remove(); }
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/* THE BRANCH DIAGRAM: the agent's branches, the remote's, and their
|
|
430
|
+
last commits, newest first in topological order: { branches: [{name,
|
|
431
|
+
id, side: 'agent'|'remote'}], commits: [{id, parents, subject, date,
|
|
432
|
+
refs: [{name, side}]}] }. What the agent wrote is escaped as everywhere. */
|
|
433
|
+
async graph(name) {
|
|
434
|
+
const r = this.#repository(name);
|
|
435
|
+
try {
|
|
436
|
+
return await this.#withRead(r, async (got, remoteTips) => {
|
|
437
|
+
const branches = got.branches.map((b) => ({ name: escaped(b.name), id: b.id, side: 'agent' }));
|
|
438
|
+
for (const [b, id] of [...remoteTips].sort(([a], [c]) => a.localeCompare(c)).slice(0, GRAPH_BRANCHES)) branches.push({ name: escaped(b), id, side: 'remote' });
|
|
439
|
+
const tips = [...new Set(branches.map((b) => b.id))];
|
|
440
|
+
if (tips.length === 0) return { branches, commits: [] };
|
|
441
|
+
const log = await this.#runner.in(got.path, ['log', '--topo-order', `--max-count=${GRAPH_COMMITS}`, '-z', '--format=%H%x00%P%x00%aI%x00%s', '--stdin', '--end-of-options'], { input: `${tips.join('\n')}\n`, deadlineMs: 60_000, maxBytes: 4 * 1024 * 1024 });
|
|
442
|
+
const fields = log.stdout.toString('utf8').split('\0');
|
|
443
|
+
const commits = [];
|
|
444
|
+
for (let i = 0; i + 3 < fields.length; i += 4) {
|
|
445
|
+
const id = fields[i].replace(/^\n/, '');
|
|
446
|
+
if (!isId(id)) break;
|
|
447
|
+
commits.push({ id, parents: fields[i + 1].split(' ').filter(isId), date: fields[i + 2], subject: escaped(fields[i + 3]).slice(0, 300),
|
|
448
|
+
refs: branches.filter((b) => b.id === id).map((b) => ({ name: b.name, side: b.side })) });
|
|
449
|
+
}
|
|
450
|
+
return { branches, commits };
|
|
451
|
+
});
|
|
452
|
+
} catch (error) {
|
|
453
|
+
if (error instanceof GitFailure && error.kind === 'empty') return { branches: [], commits: [] };
|
|
454
|
+
throw error;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/* ONE COMMIT, as a review shows it: against its first parent (or
|
|
459
|
+
nothing, for the first commit). Never sent from here. */
|
|
460
|
+
async commit(name, id) {
|
|
461
|
+
const r = this.#repository(name);
|
|
462
|
+
const asked = String(id ?? '');
|
|
463
|
+
if (!isId(asked)) throw new Refusal(400, 'A commit is named by its full identifier.');
|
|
464
|
+
return this.#withRead(r, async (got) => {
|
|
465
|
+
const p = await this.#runner.in(got.path, ['rev-list', '--parents', '--max-count=1', '--end-of-options', asked], { deadlineMs: 60_000 });
|
|
466
|
+
const ids = p.stdout.toString('latin1').trim().split(' ');
|
|
467
|
+
if (p.code !== 0 || ids[0] !== asked) throw new Refusal(404, 'This commit is not in the repository.');
|
|
468
|
+
const parents = ids.slice(1).filter(isId);
|
|
469
|
+
const shown = await this.#compare(got.path, { head: asked, base: parents[0] ?? EMPTY_TREE, range: [asked], negatives: parents });
|
|
470
|
+
return { ...shown, id: asked, parents, head: asked, state: 'commit', message: parents.length > 1 ? 'A merge: its changes against its first parent.' : '' };
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
411
474
|
/* What a review shows of head against base, in a workspace: the commits
|
|
412
475
|
(with their whole messages), the true count, the files, the changes,
|
|
413
476
|
and what the changes alone could hide. `range` and `negatives` say
|
package/server/main.mjs
CHANGED
|
@@ -70,6 +70,10 @@ export const FILES = {
|
|
|
70
70
|
'/voice-core.js': ['web/voice-core.js', 'text/javascript; charset=utf-8'],
|
|
71
71
|
'/icons.js': ['web/icons.js', 'text/javascript; charset=utf-8'],
|
|
72
72
|
'/links.js': ['web/links.js', 'text/javascript; charset=utf-8'],
|
|
73
|
+
'/gitgraph.js': ['web/gitgraph.js', 'text/javascript; charset=utf-8'],
|
|
74
|
+
/* The review's diff, drawn by diff2html (vendored, pinned: web/vendor/diff2html/LICENSE). */
|
|
75
|
+
'/vendor/diff2html.js': ['web/vendor/diff2html/diff2html-ui-slim.min.js', 'text/javascript; charset=utf-8'],
|
|
76
|
+
'/vendor/diff2html.css': ['web/vendor/diff2html/diff2html.min.css', 'text/css; charset=utf-8'],
|
|
73
77
|
'/vendor/xterm.js': [packageFile('@xterm/xterm', 'lib/xterm.js'), 'text/javascript; charset=utf-8'],
|
|
74
78
|
'/vendor/xterm.css': [packageFile('@xterm/xterm', 'css/xterm.css'), 'text/css; charset=utf-8'],
|
|
75
79
|
'/vendor/fit.js': [packageFile('@xterm/addon-fit', 'lib/addon-fit.js'), 'text/javascript; charset=utf-8'],
|