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:
+84
-4
@@ -1241,6 +1241,70 @@ export interface FsTrashItem {
|
||||
/** How a taken name is settled: ask (409 EEXISTS with `names`), skip, keep both, replace. */
|
||||
export type FsConflict = "fail" | "skip" | "keep" | "replace";
|
||||
|
||||
/** A local branch as the Branches panel lists it (`GET …/git/refs`). */
|
||||
export interface GitLocalBranch {
|
||||
name: string;
|
||||
sha: string;
|
||||
current: boolean;
|
||||
upstream: string | null;
|
||||
/** Null without an upstream, or when it is `gone`. */
|
||||
ahead: number | null;
|
||||
behind: number | null;
|
||||
gone: boolean;
|
||||
subject: string;
|
||||
date: string | null;
|
||||
/** Merged into HEAD — what `git branch -d` checks. */
|
||||
merged: boolean;
|
||||
/** Another worktree holding it, which `branch -d` refuses. */
|
||||
worktree: string | null;
|
||||
protected: boolean;
|
||||
}
|
||||
/** A fetched remote-tracking branch (`origin/x`). */
|
||||
export interface GitRemoteBranch {
|
||||
name: string;
|
||||
branch: string;
|
||||
sha: string;
|
||||
subject: string;
|
||||
date: string | null;
|
||||
}
|
||||
/** A tag: its target commit and, when annotated, its message and tagger. */
|
||||
export interface GitTag {
|
||||
name: string;
|
||||
sha: string | null;
|
||||
annotated: boolean;
|
||||
message: string | null;
|
||||
tagger: string | null;
|
||||
date: string | null;
|
||||
}
|
||||
export interface GitRefs {
|
||||
available: boolean;
|
||||
current: string | null;
|
||||
head: string | null;
|
||||
local: GitLocalBranch[];
|
||||
remotes: { remote: string; branches: GitRemoteBranch[] }[];
|
||||
tags: GitTag[];
|
||||
truncated: boolean;
|
||||
}
|
||||
export type GitRefAction =
|
||||
| "branch-create"
|
||||
| "branch-rename"
|
||||
| "branch-upstream"
|
||||
| "branch-delete"
|
||||
| "merge"
|
||||
| "tag-create"
|
||||
| "tag-delete"
|
||||
| "tag-push";
|
||||
export interface GitRefBody {
|
||||
branch?: string;
|
||||
name?: string;
|
||||
from?: string;
|
||||
switch?: boolean;
|
||||
upstream?: string;
|
||||
tag?: string;
|
||||
message?: string;
|
||||
confirm?: boolean;
|
||||
}
|
||||
|
||||
/** The target for `laneId`: an extra folder's or an inner repo, else `session`'s worktree, else the workspace. */
|
||||
export function gitTarget(
|
||||
laneId: number,
|
||||
@@ -4436,6 +4500,19 @@ export const api = {
|
||||
current: string | null;
|
||||
worktrees: LaneWorktree[];
|
||||
}>(gitUrl(id, "/branches")),
|
||||
/** GET /api/lanes/:id/git/refs — every branch and tag, for the Branches panel. */
|
||||
gitRefs: (id: GitTarget) => request<GitRefs>(gitUrl(id, "/refs")),
|
||||
/**
|
||||
* POST /api/lanes/:id/git/:action — a branch & tag write (server/lib/git-refs.js):
|
||||
* `branch-create {branch, from?, switch?}`, `branch-rename {branch, name}`,
|
||||
* `branch-upstream {branch, upstream?}` (none = unset), `tag-create {tag, message?, from?}`,
|
||||
* `tag-delete {tag, confirm: true}`, `tag-push {tag}`, plus `merge`/`branch-delete {branch}`.
|
||||
*/
|
||||
gitRefAction: (id: GitTarget, action: GitRefAction, body: GitRefBody) =>
|
||||
request<{ action: string; ok: true; output: string; branch?: string; tag?: string }>(
|
||||
gitUrl(id, `/${action}`),
|
||||
{ method: "POST", body: JSON.stringify(body) }
|
||||
),
|
||||
/**
|
||||
* GET /api/lanes/:id/git/shelves — the stash entries AgentHub made
|
||||
* (shelves), newest first. Other stashes are never listed.
|
||||
@@ -4620,10 +4697,13 @@ export const api = {
|
||||
background?: boolean;
|
||||
}
|
||||
) =>
|
||||
request<(LaneSourcesView & { path: string }) | { job: CloneJob }>(`/workspaces/${id}/sources/clone`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
request<(LaneSourcesView & { path: string }) | { job: CloneJob }>(
|
||||
`/workspaces/${id}/sources/clone`,
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
}
|
||||
),
|
||||
/** POST /api/lanes/:id/sources/folder — an extra folder the agent may use. */
|
||||
addFolder: (id: number, path: string) =>
|
||||
request<LaneSourcesView>(`/workspaces/${id}/sources/folder`, {
|
||||
|
||||
Reference in New Issue
Block a user