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
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* @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 };
|
||||
Reference in New Issue
Block a user