feat(git): history graph with search, commit details, file history, blame/browse at a revision, compare refs, and multi-commit cherry-pick/revert with preview, pause banner and undo
This commit is contained in:
+97
-6
@@ -886,10 +886,67 @@ that file.
|
||||
else the repo's default branch (what `origin/HEAD` names, else `main`/`master`)
|
||||
— `400 ENOBASE` when neither exists — and `400 ENOFILECHANGE` for a `path` with no changes in that range.
|
||||
|
||||
#### History: commit details, patch, file history, blame, browse, compare
|
||||
|
||||
```http
|
||||
GET /api/lanes/:id/git/commit?sha=<rev>
|
||||
GET /api/lanes/:id/git/patch?sha=<rev>
|
||||
GET /api/lanes/:id/git/file-history?path=<path>[&skip=<n>][&limit=<n>]
|
||||
GET /api/lanes/:id/git/blame-at?path=<path>[&rev=work|<rev>]
|
||||
GET /api/lanes/:id/git/tree-at?rev=<rev>[&path=<folder>]
|
||||
GET /api/lanes/:id/git/compare?a=<rev>&b=<rev>[&mode=three|two][&path=<file>[&from=<old name>]]
|
||||
GET /api/lanes/:id/git/pick-plan?op=cherry-pick|revert&shas=<sha>,<sha>
|
||||
GET /api/lanes/:id/git/sequence
|
||||
```
|
||||
|
||||
The History tab's reads (`server/lib/git-history.js`, mounted by
|
||||
`server/routes/lane-git-history.js`). Read-only and unguarded like every
|
||||
`/git` read, each resolving its repo exactly like them (the lane row, or
|
||||
`?session=` / `?source=` / `?repo=`); `{available:false}` with HTTP **200**
|
||||
for a folder git cannot read. A revision is a sha or a ref name (no leading
|
||||
`-`, no `..`; `~n`/`^n` suffixes allowed) and is resolved to a commit before
|
||||
git sees it, else `400 EBADREV`. A path is lane-relative with no `..`
|
||||
(`400 EBADPATH`). `403 ESECRET` / `EFOREIGN` as for `/git/file`.
|
||||
|
||||
- **`/git/commit`** → `{sha, short, subject, message, author, email, date,
|
||||
committer, committerEmail, committedAt, parents, badges, isHead, branches,
|
||||
tags, containsTruncated}` — the details pane; `branches`/`tags` contain the
|
||||
commit (50 each). Its files are `/git/changes?range=commit&sha=`.
|
||||
- **`/git/patch`** → `{sha, patch, omitted, truncated}` — `git format-patch -1
|
||||
--stdout --relative` over the commit's files inside the workspace (Copy
|
||||
patch). A secret file's diff is NEVER in it: it is named in `omitted`.
|
||||
2 MB cap. A merge commit is `400 EMERGECOMMIT`.
|
||||
- **`/git/file-history`** → `{path, commits:[{sha, short, subject, author,
|
||||
date, parents, status, path, from?}], hasMore}` — `git log --follow`: each
|
||||
commit with the name the file had THERE (`from` = a rename's old name), so
|
||||
a version's diff is `/git/changes?range=commit&sha=<sha>&path=<its path>`.
|
||||
- **`/git/blame-at`** → `{path, rev, commits: {<sha>: {short, author, date,
|
||||
summary, previous?}}, lines: [{sha|null, text}]}` — blame at a commit (or
|
||||
`work`), with each line's text; `previous` is the commit to blame next
|
||||
("before this change"). 1 MB cap (`400 ETOOBIG`), `400 ENOFILE`.
|
||||
- **`/git/tree-at`** → `{rev, path, entries:[{path, name, dir, submodule,
|
||||
size}], truncated}` — one folder at a commit (Browse files), folders first,
|
||||
5000 entries; a secret file is left out of the list, and its content stays
|
||||
refused by `/git/file?rev=`.
|
||||
- **`/git/compare`** → `{a, b, mode, onlyA, onlyB, commitsTruncated, files,
|
||||
filesTruncated}` — the commits only in `a` / only in `b`
|
||||
(`--left-right a...b`, 300 each) and the files that differ: `mode=three`
|
||||
(default) is `a...b`, what `b` adds since they diverged; `mode=two` is
|
||||
`a b`, the trees as they stand. With `path` (and a rename's `from`):
|
||||
`{a, b, mode, file}` with that file's hunks (200 KB cap); a secret file is
|
||||
`{secret: true, hunks: []}`.
|
||||
- **`/git/pick-plan`** → `{op, commits, branch, clean, repoRoot, busy}` — what
|
||||
`cherry-pick`/`revert` with `shas` would run, in the order it would run it
|
||||
(a pick oldest first, a revert newest first; ancestry, then date), for the
|
||||
confirmation dialog. `400 EBADSHA` / `EMERGECOMMIT`.
|
||||
- **`/git/sequence`** → `{active:false}` or `{active:true, operation, current,
|
||||
remaining, done, startedFrom, conflicts}` — the cherry-pick/revert that is
|
||||
paused (the banner's "paused at X (2 of 5)"); a merge is not reported here.
|
||||
|
||||
#### Read a workspace's commits, a file, or the file tree
|
||||
|
||||
```http
|
||||
GET /api/lanes/:id/git/log?limit=30[&file=<path>][&ref=<branch>]
|
||||
GET /api/lanes/:id/git/log?limit=30[&skip=<n>][&all=1][&ref=<branch>][&q=<text>][&author=<name>][&path=<path>]
|
||||
GET /api/lanes/:id/git/blame?path=<path>
|
||||
GET /api/lanes/:id/git/file?path=<path>[&rev=work|HEAD|<sha>]
|
||||
PUT /api/lanes/:id/git/file {path, content, baseSha}
|
||||
@@ -907,15 +964,25 @@ answer `{available:false}` with HTTP **200** for a cwd git cannot read —
|
||||
except the file explorer's `/git/tree`, `/git/tree-all` and `/git/ls`, which
|
||||
read the disk instead for a folder that is not a git repo (below).
|
||||
|
||||
- **`/git/log`** → `{available, commits:[{sha, short, subject, author, date,
|
||||
refs, parents}]}`, newest first in `--date-order` (no parent before its
|
||||
children; `parents` = parent shas, first parent first), `limit` clamped to 200. `file=` narrows it to the
|
||||
- **`/git/log`** → `{available, commits:[{sha, short, subject, author, email,
|
||||
date, refs, parents, badges}], hasMore, filtered}`, newest first in
|
||||
`--date-order` (no parent before its children; `parents` = parent shas,
|
||||
first parent first) — the History tab's commit graph. `badges` are the refs
|
||||
pointing at the commit, typed for drawing: `{kind: "local"|"remote"|"tag"|"head",
|
||||
name, current?}` (`current` = the branch HEAD is on, `head` = a detached
|
||||
HEAD; `refs/remotes/*/HEAD` left out). `limit` is clamped to 500 (default
|
||||
30) and `skip` pages it; `hasMore` says another page exists. `all=1` reads
|
||||
every branch, remote branch and tag (`--exclude=refs/stash --all`). Search:
|
||||
`q=` matches the message case-insensitively and LITERALLY (never a regex),
|
||||
and a hex `q` also puts the commit with that SHA prefix first; `author=`
|
||||
matches name or e-mail, literally; `path=` (alias `file=`) narrows it to the
|
||||
commits touching that path — relative, no `..` segment, else
|
||||
`400 EBADPATH` — with each `parents` REWRITTEN to the file's own previous
|
||||
commit (`--parents`), so the file's graph is one joined-up line. `ref=` reads another branch's history instead of HEAD's
|
||||
(where the panel's Cherry-pick finds its commits): a local or
|
||||
remote-tracking branch that exists, else `400 ENOBRANCH`. Reading one commit's diff is
|
||||
`/git/changes?range=commit&sha=`.
|
||||
remote-tracking branch that exists, else `400 ENOBRANCH`. `q`/`author` over
|
||||
200 characters are `400 EBADQUERY`; a walk is killed after 20 s. Reading
|
||||
one commit's diff is `/git/changes?range=commit&sha=`.
|
||||
- **`/git/blame`** → `{available, path, commits: {<sha>: {short, author,
|
||||
date, summary}}, lines: [<sha>|null]}` — `git blame --porcelain` of the
|
||||
working-tree file (Annotate); `null` = a line not committed yet. Same
|
||||
@@ -1198,6 +1265,11 @@ cannot collide on `index.lock`.
|
||||
| `merge-abort` | `git merge --abort` (or `cherry-pick --abort` / `revert --abort`, whichever is open) | `{"confirm": true}` |
|
||||
| `cherry-pick` | `git cherry-pick <sha>` — a non-merge commit, onto a clean repo-root workspace | `{"sha": "9b3e74a"}` |
|
||||
| `revert` | `git revert --no-edit <sha>` — same guards as `cherry-pick` | `{"sha": "9b3e74a"}` |
|
||||
| `cherry-pick` / `revert` with `shas` | ONE git sequence of 1–50 non-merge commits, in `/git/pick-plan`'s order (`server/lib/git-pick.js`) | `{"shas": ["9b3e74a", "c1d2e3f"]}` |
|
||||
| `pick-continue` | `git cherry-pick --continue` / `git revert --continue` once nothing is unmerged | `{}` |
|
||||
| `pick-skip` | `--skip` the commit the pick/revert stopped at (drops its resolutions) | `{"confirm": true}` |
|
||||
| `pick-abort` | `--abort`: back to where the pick/revert started | `{"confirm": true}` |
|
||||
| `undo-pick` | `git reset --soft <from>` after a pick/revert — its changes stay staged | `{"from": "<sha>", "to": "<sha>", "confirm": true}` |
|
||||
| `undo-commit` | `git reset --soft HEAD~1` — only an UNPUSHED, non-merge, non-root HEAD that is still `sha`; its changes stay staged | `{"confirm": true, "sha": "<HEAD the user saw>"}` |
|
||||
| `amend` | `git commit --amend -m <message>`, or `--no-edit` with no message — only an UNPUSHED HEAD that is still `sha`; takes the index as it stands, or with `paths` exactly those files (`--only`; an empty list rewords and needs a message) | `{"message": "...", "sha": "<HEAD>", "paths": ["..."]}` |
|
||||
| `shelve` | `git stash push --include-untracked -m "agenthub-shelf: <branch> · <ISO time>[ · <note>]" [-- <paths>]` — everything, or exactly the ticked changed `paths` | `{"note": "...", "paths": ["..."]}` |
|
||||
@@ -1327,6 +1399,25 @@ cannot collide on `index.lock`.
|
||||
failing signer) is undone the same way and answers git's own error. A
|
||||
rebase stopped in a terminal makes every history action `409 EMERGING`;
|
||||
`merge-commit`/`merge-abort` then continue/abort that rebase.
|
||||
- **Several commits at once** (`shas` instead of `sha`, `server/lib/git-pick.js`)
|
||||
run as ONE git sequence — `git cherry-pick A B C`, `git revert --no-edit C B A`
|
||||
— in the order `GET /git/pick-plan` shows (a pick oldest first, a revert
|
||||
newest first), under the same guards (1–50 commits, none a merge, a clean
|
||||
repo-root tree, nothing already open — `409 EMERGING`). The answer is
|
||||
`{from, to, commits, conflicts, paused, applied}`: `paused: true` with
|
||||
`conflicts[]` stopped at a conflict, with `empty: true` at a commit whose
|
||||
changes are already here. A paused one shows on `GET /git/sequence` and is
|
||||
finished by `pick-continue` (`409 ECONFLICTS` while anything is unmerged,
|
||||
`409 EEMPTY` when the resolution changed nothing — skip it then),
|
||||
`pick-skip` or `pick-abort` (both `confirm: true`, `400 ECONFIRM`; abort
|
||||
puts the branch back at `from`), `409 ENOMERGE` when nothing is open.
|
||||
`undo-pick {from, to, confirm: true}` takes a finished pick/revert back with
|
||||
`reset --soft <from>` (the changes stay staged): only while HEAD is still
|
||||
`to` (`409 ESTALE`), `from` is its ancestor 1–50 commits back with no merge
|
||||
between (`400 EBADSHA` / `EMERGECOMMIT`), and none of those commits is on a
|
||||
remote-tracking branch (`409 EPUSHED`). The single-`sha` body is unchanged.
|
||||
While a rebase is open every one of these is `409 EMERGING`, and
|
||||
`GET /git/sequence` does not report a rebase as a paused pick.
|
||||
- **Undo commit and amend** rewrite the last commit, so both are
|
||||
`409 EPUSHED` once any remote-tracking branch contains HEAD, `409 ESTALE`
|
||||
when HEAD is no longer the `sha` the request names (`400 EBADSHA` without
|
||||
|
||||
Reference in New Issue
Block a user