Files
AgentHub/server/openapi-extra/git-refs.js
T
nntrivi2001 2cc492bb88 feat(git): Branches & tags panel — rename, branch from any ref, upstream, tags
GET /git/refs lists local branches (upstream, ahead/behind, last commit,
merged, held by a worktree, protected), remote branches grouped by remote
and tags (target, annotated message/tagger/date). New writes in
server/lib/git-refs.js, dispatched from git-ops.js: branch-rename,
branch-upstream (set/unset), tag-create (light/annotated), tag-delete
(confirm), tag-push (one tag to origin, no --tags, no force), and
branch-create with a looked-up from + switch on a clean tree. Every ref is
looked up, never passed through; repo-wide writes are repo-root only
(ESUBDIR) and refused during any operation (EMERGING).

Client: git/BranchManager + TagManager dialog opened from the git bar's
tags icon, en+vi (gitRefs namespace), keyboard navigable.

# Conflicts:
#	CLAUDE.md
#	client/src/components/lanes/LaneGitBar.tsx
#	docs/API.md
#	server/__tests__/lanes-api.test.js
#	server/routes/lanes.js
2026-11-02 12:00:55 +07:00

51 lines
3.9 KiB
JavaScript

/**
* @file OpenAPI fragment for the git bar's Branches panel (server/lib/git-refs.js):
* the `GET /api/lanes/{id}/git/refs` read, plus `extend()`, which adds the
* branch & tag writes to the existing `POST /api/lanes/{id}/git/{action}`
* entry in place (its enum and description) rather than replacing that entry.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const { REF_ACTIONS } = require("../lib/git-refs");
const REF_ACTIONS_DOC =
" Branch & tag tools (server/lib/git-refs.js; every ref named in the body is LOOKED UP in refs/heads, refs/remotes or refs/tags — or a hex sha verified as a commit — and git gets the full refname, never the request's text; every write here except branch-create runs only from a repo-root lane (400 ESUBDIR) and never while a merge, cherry-pick, revert or rebase is open (409 EMERGING)): branch-create also takes {from} (a local/remote branch, a tag, or a commit sha; 400 EBADREF, 400 EAMBIGUOUS when a branch and a tag share the name — spell out refs/tags/x) and {switch:true} (clean tree only, 400 EDIRTY); branch-rename {branch, name} (`git branch -m`; refuses main/master/development/develop 400 EPROTECTED, an existing name 400 EBRANCHEXISTS, a name `check-ref-format --branch` rejects 400 EBADBRANCH); branch-upstream {branch, upstream} sets it to a fetched remote branch (`origin/x`, 400 ENOBRANCH otherwise) and {branch} with no upstream unsets it (400 ENOUPSTREAM when none); tag-create {tag, message?, from?} (annotated when a message is given; on HEAD or a looked-up start point; 400 EBADTAG / ETAGEXISTS / ENOCOMMITS); tag-delete {tag, confirm:true} (irreversible: 400 ECONFIRM without it, 400 ENOTAG); tag-push {tag, credentials?} pushes ONE tag `refs/tags/<tag>:refs/tags/<tag>` to origin (else the only remote, 400 ENOREMOTE) — no --tags, no force, so a remote tag at another commit is a 409 in git's words.";
const paths = {
"/api/lanes/{id}/git/refs": {
get: {
tags: ["Lanes"],
summary: "Every branch and tag of the lane's repo, for the Branches panel",
description:
"`local`: each local branch with its upstream, ahead/behind against it (null without one, `gone` when the upstream ref is gone), last commit subject and date, `merged` (into HEAD — what `branch -d` checks), `worktree` (another worktree holding it), `protected` and `current`. `remotes`: the fetched remote-tracking branches grouped by remote (symbolic HEADs left out). `tags`: name, target commit `sha`, and for an annotated tag its `message`, `tagger` and `date`. At most 1000 of each kind (`truncated`). The repo comes from the lane row (or `?session` / `?source` / `?repo`), never a path from the query; a folder that is not a repo answers available:false with HTTP 200. Read-only, no same-origin guard.",
operationId: "listLaneGitRefs",
parameters: [
{ name: "id", in: "path", required: true, schema: { type: "integer" } },
{ name: "session", in: "query", required: false, schema: { type: "string" } },
{ name: "source", in: "query", required: false, schema: { type: "integer" } },
{ name: "repo", in: "query", required: false, schema: { type: "string" } },
],
responses: {
200: {
description:
"{available, current, head, local[], remotes[{remote, branches[]}], tags[], truncated}.",
},
404: { description: "Lane not found, ENOSOURCE, ENOREPO or ENOWORKTREE." },
},
},
},
};
/** Add the branch & tag writes to the combined spec's git action entry. */
function extend(out) {
const post = out.paths["/api/lanes/{id}/git/{action}"]?.post;
if (!post) return out;
const action = (post.parameters || []).find((p) => p.name === "action");
const list = action?.schema?.enum;
if (Array.isArray(list)) for (const a of REF_ACTIONS) if (!list.includes(a)) list.push(a);
if (!post.description.includes("git-refs.js")) post.description += REF_ACTIONS_DOC;
return out;
}
module.exports = { tags: [], schemas: {}, paths, extend };