feat(lanes,git): publish a repo with no remote, and read a repo with no commits
What broke: a lane on a fresh `git init` repo had no working git surface.
`statusCounts` ran `rev-parse --short HEAD` unguarded, so `gitFacts` threw
and `GET /api/lanes/:id/git` answered `{available:false}` — the Changes
panel showed no header, and `commit`/`switch` 500'd through `isDirty`.
Add lane in Worktree mode accepted such a repo and left a `failed` row
behind, and a repo with commits but no remote had a permanently disabled
Push with no way to give it one short of a terminal.
What changed:
- worktree.js: an unborn HEAD is a normal state. `statusCounts` reports
`head: null`; `gitFacts` reads the branch via `symbolic-ref`, an empty
subject, and a new `unborn` field.
- git-ops.js: an 11th write, `publish` — `remote add origin -- <url>` then
`push -u origin HEAD`, only with a commit (ENOCOMMITS) and NO remote at
all (EREMOTEEXISTS, so it never re-points one), the url allowlisted by
`validateRemoteUrl` (https without userinfo, ssh://, scp-like; refuses
local paths, file://, ext::, fd::, git://, http://, leading `-`:
EBADURL), and `origin` removed again when the push fails.
- POST /api/lanes/worktree answers 400 EEMPTYREPO for a repo with no
commits instead of creating a row that can only fail.
- LaneGitBar: unborn repos show "no commits yet" and a commit-first hint;
no remote offers Publish… with an inline URL box; a remote but no
upstream relabels Push as "Publish branch". AddLaneModal disables Add
on an empty repo and offers "Use as a Repo lane".
- The two tests that used an unborn repo to force a provisioning failure
now use a `feat` ref clash, since that repo is refused up front.
- CLAUDE.md rule reworded to "the boring eleven" with publish's guards;
docs/API.md, docs/LANES.md, ARCHITECTURE.md, server/README.md, openapi.
This commit is contained in:
@@ -2533,12 +2533,13 @@ export const api = {
|
||||
* POST /api/lanes/:id/git/:action — one git write on the lane's worktree.
|
||||
* @param id The lane id.
|
||||
* @param action `fetch`, `pull` (always --ff-only), `push`, `stage`,
|
||||
* `unstage`, `discard`, `commit`, `switch`, `branch-create` or
|
||||
* `branch-delete`.
|
||||
* `unstage`, `discard`, `commit`, `switch`, `branch-create`,
|
||||
* `branch-delete` or `publish`.
|
||||
* @param body `{message, only}` for commit; `{branch, create}` for switch;
|
||||
* `{branch, base}` for branch-create; `{paths}` for stage/unstage;
|
||||
* `{paths, confirm: true}` for discard — which is irreversible, so the
|
||||
* server refuses it without that flag.
|
||||
* server refuses it without that flag; `{url}` for publish (a repo with
|
||||
* no remote yet: adds `origin` and pushes HEAD with an upstream).
|
||||
* @returns `{action, ok, output}` — `output` is git's own text, shown as-is.
|
||||
*/
|
||||
gitAction: (
|
||||
@@ -2556,11 +2557,13 @@ export const api = {
|
||||
| "discard-hunk"
|
||||
| "branch-create"
|
||||
| "branch-delete"
|
||||
| "publish"
|
||||
| "merge"
|
||||
| "resolve"
|
||||
| "merge-commit"
|
||||
| "merge-abort",
|
||||
body: {
|
||||
url?: string;
|
||||
message?: string;
|
||||
only?: "staged";
|
||||
branch?: string;
|
||||
|
||||
@@ -2584,9 +2584,11 @@ export type LaneGitFacts =
|
||||
available: true;
|
||||
/** Branch name, or the literal `HEAD` for a detached checkout. */
|
||||
branch: string;
|
||||
/** Short commit hash of HEAD. */
|
||||
head: string;
|
||||
/** Subject line of the HEAD commit. */
|
||||
/** Short commit hash of HEAD; null in a repo with no commit yet. */
|
||||
head: string | null;
|
||||
/** True when the repo has no commit yet (a fresh `git init`). */
|
||||
unborn?: boolean;
|
||||
/** Subject line of the HEAD commit; "" when unborn. */
|
||||
subject: string;
|
||||
/** Tracked files with uncommitted changes. */
|
||||
dirty: number;
|
||||
|
||||
Reference in New Issue
Block a user