feat: git backbone — wip safety commits, per-page publish with pre-flight, discard, state badges #25

Closed
dominik.polakovics wants to merge 5 commits from afk/10 into main

Closes #10

What this adds

The git backbone from D2/D3/D14/D16 on top of #3's save path:

  • Save → wip safety commit. Every successful save also snapshots the entire working tree to the local ref `refs/deckle/wip` via a temporary index — the editor's index, HEAD, branch, and working tree are untouched, the ref is never pushed, and an unchanged tree is skipped rather than erroring. Kill the harness at any point and restart: nothing saved is lost.
  • Per-page publish. Publishing page A stages exactly A's bundle (leaf bundles: whole directory; branch bundles: `_index.md` + direct non-md resources; plain pages: the file), commits it as `deckle: publish `, and pushes — page B's pending edits stay uncommitted throughout. "Publish all" ships every pending content change in one commit. Empty publishes report as no-ops; a no-op publish also pushes any stranded commit from an earlier failed push, so retrying self-heals without duplicating commits.
  • Pre-flight build. Before anything is committed, hugo builds the exact to-be-published tree (HEAD + only the bundle being published, materialized into a scratch directory) — another page's half-finished draft can neither break nor sneak into the publish, and the running preview is untouched. A failing build refuses the publish with hugo's output returned verbatim (422).
  • pull --rebase with path policy. Before every push: conflicts under `content/` resolve to the tool's version, everything else to the remote's. A developer pushing theme changes mid-session merges cleanly and the publish proceeds; uncommitted drafts survive the rebase (stash round-trip resolved under the same policy).
  • Per-page discard restores a bundle to its last-published state; a never-published page is deleted, with the UI confirm calling that out explicitly.
  • Draft-link warning. Publishing a page that links (markdown links or ref/relref) to a still-draft page returns a 409 the UI turns into a confirm-and-retry.
  • State badges. `GET /api/pages` derives draft/modified/published per page from the uncommitted diff against HEAD; the SPA shows badges in a new pages list and a publish bar, refreshed after save/publish/discard.
  • Graceful degradation. A site dir that isn't a usable git repo root (e.g. `testdata/site`) still edits fine; the git surface returns an actionable reason (503 / `git.available: false`) instead of crashing.

New API

  • `GET /api/pages` → `{"git": {"available", "error"}, "pages": [{"page", "state"}]}`
  • `POST /api/page/publish` `{"page", "confirmDraftLinks"}` → `{"page", "noop"}` | 409 `draft-links` | 422 `preflight` | 503 `git`
  • `POST /api/publish-all` `{}` → `{"noop"}`
  • `POST /api/page/discard` `{"page"}` → `{"page", "removed"}`

Structure

  • `internal/gitops` — git primitives (temp-index wip snapshots, partial commits, tree materialization, policy rebase, restore), tested against real temp repos with a bare origin and a concurrent "developer" clone
  • `internal/bundle` — bundle resolution, page listing, link extraction
  • `internal/publish` — orchestration (states, publish, publish-all, discard, wip), tested with fake and real hugo
  • `internal/server` + `cmd/harness` — endpoints, save hook, degraded-mode wiring
  • `web/` — pages list with badges, publish bar, confirm flows (28 tests)

Verification

  • `go vet ./...`, `go test ./...` — all packages green (gitops/publish suites run against real git repos; publish includes a real-hugo end-to-end test)
  • `npm test` (28 passing), `tsc --noEmit`, `vite build` clean
  • Manual end-to-end smoke against a real repo + bare origin with the built binary: save→wip ref; kill -9 + restart loses nothing; selective publish (B stays local); pre-flight refusal on a broken page pushes nothing; discard (both revert and delete); draft-link 409 + confirmed override; publish-all; noop repeat; concurrent dev theme push rebased cleanly under the path policy; degraded mode against a non-repo site dir

🤖 Generated with Claude Code

https://claude.ai/code/session_01PWermJL5sDiWKNfArKJ9Xc

Closes #10 ## What this adds The git backbone from D2/D3/D14/D16 on top of #3's save path: - **Save → wip safety commit.** Every successful save also snapshots the entire working tree to the local ref \`refs/deckle/wip\` via a temporary index — the editor's index, HEAD, branch, and working tree are untouched, the ref is never pushed, and an unchanged tree is skipped rather than erroring. Kill the harness at any point and restart: nothing saved is lost. - **Per-page publish.** Publishing page A stages exactly A's bundle (leaf bundles: whole directory; branch bundles: \`_index.md\` + direct non-md resources; plain pages: the file), commits it as \`deckle: publish <page>\`, and pushes — page B's pending edits stay uncommitted throughout. "Publish all" ships every pending content change in one commit. Empty publishes report as no-ops; a no-op publish also pushes any stranded commit from an earlier failed push, so retrying self-heals without duplicating commits. - **Pre-flight build.** Before anything is committed, hugo builds the exact to-be-published tree (HEAD + only the bundle being published, materialized into a scratch directory) — another page's half-finished draft can neither break nor sneak into the publish, and the running preview is untouched. A failing build refuses the publish with hugo's output returned verbatim (422). - **pull --rebase with path policy.** Before every push: conflicts under \`content/\` resolve to the tool's version, everything else to the remote's. A developer pushing theme changes mid-session merges cleanly and the publish proceeds; uncommitted drafts survive the rebase (stash round-trip resolved under the same policy). - **Per-page discard** restores a bundle to its last-published state; a never-published page is deleted, with the UI confirm calling that out explicitly. - **Draft-link warning.** Publishing a page that links (markdown links or ref/relref) to a still-draft page returns a 409 the UI turns into a confirm-and-retry. - **State badges.** \`GET /api/pages\` derives draft/modified/published per page from the uncommitted diff against HEAD; the SPA shows badges in a new pages list and a publish bar, refreshed after save/publish/discard. - **Graceful degradation.** A site dir that isn't a usable git repo root (e.g. \`testdata/site\`) still edits fine; the git surface returns an actionable reason (503 / \`git.available: false\`) instead of crashing. ## New API - \`GET /api/pages\` → \`{"git": {"available", "error"}, "pages": [{"page", "state"}]}\` - \`POST /api/page/publish\` \`{"page", "confirmDraftLinks"}\` → \`{"page", "noop"}\` | 409 \`draft-links\` | 422 \`preflight\` | 503 \`git\` - \`POST /api/publish-all\` \`{}\` → \`{"noop"}\` - \`POST /api/page/discard\` \`{"page"}\` → \`{"page", "removed"}\` ## Structure - \`internal/gitops\` — git primitives (temp-index wip snapshots, partial commits, tree materialization, policy rebase, restore), tested against real temp repos with a bare origin and a concurrent "developer" clone - \`internal/bundle\` — bundle resolution, page listing, link extraction - \`internal/publish\` — orchestration (states, publish, publish-all, discard, wip), tested with fake and real hugo - \`internal/server\` + \`cmd/harness\` — endpoints, save hook, degraded-mode wiring - \`web/\` — pages list with badges, publish bar, confirm flows (28 tests) ## Verification - \`go vet ./...\`, \`go test ./...\` — all packages green (gitops/publish suites run against real git repos; publish includes a real-hugo end-to-end test) - \`npm test\` (28 passing), \`tsc --noEmit\`, \`vite build\` clean - Manual end-to-end smoke against a real repo + bare origin with the built binary: save→wip ref; kill -9 + restart loses nothing; selective publish (B stays local); pre-flight refusal on a broken page pushes nothing; discard (both revert and delete); draft-link 409 + confirmed override; publish-all; noop repeat; concurrent dev theme push rebased cleanly under the path policy; degraded mode against a non-repo site dir 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01PWermJL5sDiWKNfArKJ9Xc
Shell out to git for the editing harness's version-control needs: crash-safe
working-tree snapshots to refs/deckle/wip via a temporary index (never
touching the user's index, HEAD, or branch), per-pathspec partial commits,
tree materialization (HEAD + working-tree overlay) for publish pre-flights,
restore-from-HEAD for discards, and a fetch/rebase/push cycle that resolves
conflicts by path policy (local wins under given pathspecs, remote wins
elsewhere) while preserving uncommitted changes across the rebase.

Tested against real temporary repositories with a bare origin and a second
clone simulating concurrent developer pushes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWermJL5sDiWKNfArKJ9Xc
Map a content-relative page to the repository paths forming its
publish/discard unit (leaf bundles take their whole directory, branch
bundles take _index.md plus direct non-markdown resources, plain pages
just their file), enumerate all pages under a content root, and extract
markdown/ref/relref link targets so publish can warn about links to
still-draft pages.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWermJL5sDiWKNfArKJ9Xc
Publisher composes gitops and bundle into the editor's operations: derive
draft/modified/published state per page from the uncommitted diff against
HEAD, publish one page's bundle (or everything) via pre-flight hugo build
of exactly the to-be-published tree in a scratch directory, partial commit,
pull --rebase (content/ wins locally, remote wins elsewhere), and push,
discard a bundle back to its last-published state, and snapshot saves to
the wip ref. Structured refusals (draft-links, preflight, git) carry what
the UI needs; a publish that finds nothing to commit still pushes stranded
commits so retrying a failed push self-heals without duplicates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWermJL5sDiWKNfArKJ9Xc
New endpoints: GET /api/pages (per-page draft/modified/published state plus
git availability), POST /api/page/publish (with draft-link confirmation),
POST /api/publish-all, and POST /api/page/discard, mapping structured
publish refusals to 409/422/503 bodies the SPA can act on. Every
successful save now also snapshots the working tree to refs/deckle/wip
(best-effort; the save response never fails because of it). A site
directory that is not a usable git repository degrades gracefully: the
harness starts, editing works, and the git surface reports the actionable
reason instead of pretending to work.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWermJL5sDiWKNfArKJ9Xc
feat(web): pages list with state badges and publish/discard UI
All checks were successful
ci / build-and-test (pull_request) Successful in 1m1s
8f135b8f13
Replace the free-text page input with a pages list showing per-page
Draft/Modified/Published badges from GET /api/pages, and add a publish bar
for the open page: publish (with a confirm-and-retry flow when the page
links to still-draft pages), discard (confirm first; deletion called out
for never-published pages), and publish-all. Pre-flight build failures
render hugo's output verbatim; when git is unavailable the actions are
disabled with the reason shown. Badges refresh after save, publish, and
discard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWermJL5sDiWKNfArKJ9Xc
All checks were successful
ci / build-and-test (pull_request) Successful in 1m1s

Pull request closed

Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/deckle!25
No description provided.