Files
AgentHub/server/routes/lane-git-history.js
T

92 lines
3.4 KiB
JavaScript

/**
* @file The Changes panel's history reads, mounted on the lanes router
* (`/api/lanes/:id/git/...` and its `/api/workspaces` alias): commit details,
* a commit's patch, a file's history across renames, blame at a revision, a
* tree at a revision, a two-ref comparison, a cherry-pick/revert plan and a
* paused one's state. All of it is server/lib/git-history.js.
*
* Reads, so unguarded like every other `/git/*` read — and each resolves its
* repository exactly like them, through the lanes router's `gitTargetOr404`
* (the lane row, `?session=`, `?source=`, `?repo=`), never a path from the
* request. Error mapping matches `/git/file`: a secret or a foreign folder is
* 403, a bad request 400, anything else (not a repo, no commits) a 200
* `{available: false}` — a state, not a fault.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const history = require("../lib/git-history");
const BAD = new Set([
"EBADPATH",
"EBADREV",
"EBADQUERY",
"EBADSHA",
"EBADACTION",
"ENOFILE",
"ETOOBIG",
"ENOBRANCH",
"EMERGECOMMIT",
]);
const str = (v) => (typeof v === "string" ? v : "");
/**
* @param {import("express").Router} router The lanes router.
* @param {Function} gitTargetOr404 Its repo resolver: `{lane, cwd}` or null
* (the 404 already sent).
*/
function registerGitHistory(router, gitTargetOr404) {
const read = (sub, fn) =>
router.get(`/:id/git/${sub}`, async (req, res) => {
const t = await gitTargetOr404(req, res);
if (!t) return;
try {
res.json({ available: true, ...(await fn(t.cwd, req.query)) });
} catch (err) {
if (err.code === "ESECRET" || err.code === "EFOREIGN") {
return res.status(403).json({ error: { code: err.code, message: err.message } });
}
if (BAD.has(err.code)) {
return res.status(400).json({ error: { code: err.code, message: err.message } });
}
res.json({ available: false });
}
});
/** GET /git/commit?sha= — one commit's message, people, parents, refs. */
read("commit", (cwd, q) => history.commitDetails(cwd, str(q.sha)));
/** GET /git/patch?sha= — the commit as `format-patch` text (Copy patch). */
read("patch", (cwd, q) => history.commitPatch(cwd, str(q.sha)));
/** GET /git/file-history?path=&skip=&limit= — `log --follow`. */
read("file-history", (cwd, q) => history.fileHistory(cwd, str(q.path), q));
/** GET /git/blame-at?path=&rev= — blame at a commit (or `work`). */
read("blame-at", (cwd, q) => history.blameAt(cwd, str(q.path), str(q.rev) || "work"));
/** GET /git/tree-at?rev=&path= — one folder of the tree at a commit. */
read("tree-at", (cwd, q) => history.treeAt(cwd, str(q.rev), str(q.path)));
/** GET /git/compare?a=&b=&mode=three|two[&path=&from=] — two refs. */
read("compare", (cwd, q) =>
history.compareRefs(cwd, {
a: str(q.a),
b: str(q.b),
mode: q.mode === "two" ? "two" : "three",
path: str(q.path) || null,
from: str(q.from) || null,
})
);
/** GET /git/pick-plan?op=cherry-pick|revert&shas=a,b — what it would run. */
read("pick-plan", (cwd, q) =>
history.pickPreview(
cwd,
str(q.op),
str(q.shas)
.split(",")
.map((s) => s.trim())
.filter(Boolean)
)
);
/** GET /git/sequence — the cherry-pick/revert that is paused, if any. */
read("sequence", (cwd) => history.sequenceState(cwd));
}
module.exports = { registerGitHistory };