Git save and per-page publish: wip safety commits, publish per bundle, pre-flight, discard #10

Closed
opened 2026-07-11 13:00:29 +02:00 by dominik.polakovics · 2 comments

What to build

The git backbone (docs/DECISIONS.md D2, D3, D14, D16 pre-flight half): Save writes the file and safety-commits the working tree to a wip ref (crash-safe, never pushed to main). Per-page Publish stages only that page's bundle, commits to main, and pushes; "Publish all" commits everything pending. Before any push, pull --rebase (tool wins inside content/, remote wins elsewhere) and a local pre-flight hugo build of the to-be-published state — build failure refuses the publish and shows the error. Per-page Discard reverts a bundle to its published state. Draft state = uncommitted diff; the UI shows per-page Draft/Published badges.

Acceptance criteria

  • Every save produces a wip ref commit; killing and restarting the tool loses no saved work
  • Publish of page A pushes only A's bundle to main while page B's pending edits stay uncommitted; "Publish all" ships everything
  • Pre-flight build failure blocks the publish, nothing is pushed, and the Hugo error is shown to the editor
  • pull --rebase runs before publish; a developer push to theme files merges cleanly under the path policy
  • Discard restores a page's bundle to last-published state after confirmation
  • Publishing a page linking to a still-draft page warns before proceeding
  • Tree and page views show accurate Draft/Published/Modified state

Blocked by

## What to build The git backbone (docs/DECISIONS.md D2, D3, D14, D16 pre-flight half): Save writes the file and safety-commits the working tree to a `wip` ref (crash-safe, never pushed to main). Per-page Publish stages only that page's bundle, commits to main, and pushes; "Publish all" commits everything pending. Before any push, `pull --rebase` (tool wins inside `content/`, remote wins elsewhere) and a local pre-flight `hugo` build of the to-be-published state — build failure refuses the publish and shows the error. Per-page Discard reverts a bundle to its published state. Draft state = uncommitted diff; the UI shows per-page Draft/Published badges. ## Acceptance criteria - [ ] Every save produces a `wip` ref commit; killing and restarting the tool loses no saved work - [ ] Publish of page A pushes only A's bundle to main while page B's pending edits stay uncommitted; "Publish all" ships everything - [ ] Pre-flight build failure blocks the publish, nothing is pushed, and the Hugo error is shown to the editor - [ ] `pull --rebase` runs before publish; a developer push to theme files merges cleanly under the path policy - [ ] Discard restores a page's bundle to last-published state after confirmation - [ ] Publishing a page linking to a still-draft page warns before proceeding - [ ] Tree and page views show accurate Draft/Published/Modified state ## Blocked by - #3
Author
Owner

This was generated by AI during triage.

Agent Brief

Category: enhancement
Summary: Git backbone for the editing flow: every Save additionally safety-commits the working tree to a local wip ref (crash-safe, never pushed); per-page Publish stages only that page's bundle, commits to main, and pushes after a pull --rebase (path policy: tool wins in content/, remote wins elsewhere) and a local pre-flight Hugo build that blocks broken publishes; per-page Discard reverts a bundle to its published state; the UI shows per-page Draft/Published/Modified badges derived from uncommitted diff state.

Dependency note: this issue is hard-blocked by #3 (edit one element end-to-end). It builds on #3's save path — the endpoint that writes a canonically serialized page file to disk — by wrapping it in git semantics; the scheduler handles ordering, so simply do not start until #3 is merged. The issue body's docs/DECISIONS.md references (D2, D3, D14, D16) point at a file that is not committed — this brief is self-contained and expands those decisions inline.

Current behavior:
The harness supervises hugo server for live preview and (after #3) can save an edited element: the save endpoint validates against the sidecar schema and writes the canonically serialized file straight to disk. That write is the only persistence: nothing is versioned by the tool, there is no publish or discard concept, and no notion of a page being "draft" versus "published". If an editor's machine dies mid-session, saved work exists only as loose working-tree files; if a save produces a broken site, nothing stops it from reaching the deployed branch by whatever manual process exists. The tool never invokes git; whether the site directory is a git repository is currently irrelevant to it.

Desired behavior:

Model. The site directory is a git repository whose main branch (with a push remote) is the published source of truth — a push to main is what triggers deployment. The editing model on top of git:

  • Saved but unpublished work lives as uncommitted changes in the working tree, mirrored into a local wip safety ref.
  • Published means committed to main and pushed.
  • A page's bundle is the unit of publish/discard: for a Hugo page bundle (a directory with index.md/_index.md), the whole directory including page resources; for a plain single-file page, that file.

Save → wip safety commit. Every successful save, after writing the file, commits a snapshot of the entire working tree to a local wip ref. That ref exists purely for crash safety and is never pushed. The snapshot must not disturb the editor's real state: the working tree, the index, and the checked-out branch are exactly as before (use a mechanism that commits without touching the user's index/HEAD). Killing the tool at any point and restarting it loses no saved work — the files are on disk and their history is recoverable from the wip ref. Save must not fail overall if the working tree is momentarily identical to the previous snapshot (an empty diff is not an error).

Per-page Publish. Publishing page A stages exactly A's bundle paths, commits them to main with a meaningful message identifying the page, and pushes. Page B's pending edits stay uncommitted in the working tree throughout — they are neither committed, stashed away permanently, nor lost. "Publish all" commits every pending content change and pushes. A publish that would be empty (page already matches main) is reported as such, not committed.

Pre-flight build. Before anything is committed to main, the tool runs a local hugo build of the exact tree that would be published — main plus the bundle(s) being published, not the whole working tree (page B's half-finished draft must not be able to fail page A's publish, and must not sneak into it either). If the build fails, the publish is refused, nothing is committed or pushed, and the Hugo error output is shown to the editor. The pre-flight must not disrupt the running preview server (build into a throwaway destination).

Pull --rebase and path policy. Before pushing, the tool runs pull --rebase against the remote. Conflicts resolve by path policy: inside content/ the tool's local version wins; everywhere else (theme, layouts, config, assets) the remote version wins. Net effect: a developer pushing theme changes while an editor works never blocks a publish — the rebase merges cleanly under the policy and the publish proceeds. If the push is rejected anyway (e.g. a race), the error is surfaced to the editor and retrying the publish works without duplicating commits.

Per-page Discard. Discarding page A reverts A's bundle to its last-published (main) state, after an explicit confirmation in the UI. Other pages' pending edits are untouched. The preview reflects the revert via LiveReload since the files change on disk. Discarding a page that was never published (no version on main) removes it, with the confirmation making that explicit.

Draft/Published/Modified badges. Page state is derived from the uncommitted diff against main: Published (bundle identical to main), Modified (exists on main, has uncommitted local changes), Draft (exists locally, never published to main). The tree and page views in the SPA show these badges, served by the API, and they update after save, publish, and discard.

Draft-link warning. Publishing a page whose content links to a page that is still in Draft state (would 404 on the live site) warns the editor before proceeding; the editor can confirm and publish anyway.

Degradation. If the site directory is not a git repository (or main/remote is missing), the tool does not crash: saving still writes files as in #3, and the git-dependent surface (publish, discard, badges) reports a clear, actionable error state instead of pretending to work.

Key interfaces:

  • A dedicated internal git package/component encapsulating all repository operations (wip snapshot, per-path-set commit and push, rebase with path policy, per-bundle revert, per-page status). Whether it shells out to the git CLI or uses a library is the implementer's choice, but the server should depend on its interface, not on git details.
  • The save endpoint from #3 — unchanged contract for the caller, but a successful save now also produces a wip ref commit.
  • New mutating API endpoints for publish (single page and all) and discard (single page, gated on client-side confirmation), returning structured success/refusal bodies; a pre-flight failure returns the Hugo error text for display.
  • Page listing/status API: per-page state (draft / published / modified) exposed wherever the SPA renders the page tree and the open page, so badges are data-driven.
  • Bundle resolution: a function mapping a content-relative page path to the set of repository paths that form its bundle — reused by publish, discard, and status.

Acceptance criteria:

  • Every successful save produces a commit on the local wip ref snapshotting the working tree; the ref is never pushed, and the editor's index, HEAD, and branch are untouched by the snapshot
  • Killing the harness after a save and restarting it loses no saved work; the wip history contains the snapshot
  • Publishing page A commits and pushes only A's bundle to main; page B's pending edits remain uncommitted locally before and after
  • "Publish all" commits and pushes every pending content change
  • Pre-flight builds the exact to-be-published tree (main + published bundle, without other pages' drafts); a failing build refuses the publish, commits and pushes nothing, and shows the Hugo error to the editor
  • pull --rebase runs before every push; a concurrent remote push touching theme files merges cleanly under the path policy (tool wins in content/, remote wins elsewhere) and the publish succeeds
  • Discard restores a page's bundle to its last-published state after explicit confirmation, leaving other pages' edits intact, and the preview updates
  • Publishing a page that links to a still-Draft page warns before proceeding and allows an explicit override
  • Tree and page views show accurate per-page Draft/Published/Modified badges that update after save, publish, and discard
  • A site directory that is not a git repository degrades gracefully: saves still work, git-dependent features report an actionable error instead of crashing
  • Git behaviors (wip snapshot, selective publish, path policy rebase, discard, status derivation) are covered by tests against real temporary repositories

Out of scope:

  • CI/deployment status feedback after the push (#11) — this issue ends at a successful git push
  • Commit author identity from X-Remote-User, locks, and CSRF protection (#13) — a fixed configured/default author is fine here
  • Media uploads and bundle resources management (#12) — but bundle-path resolution should already include resources present in a bundle directory
  • Page create/rename/move/delete (#9); publishing UI only covers pages that exist
  • Hugo version pinning for the pre-flight build (#5) — use the hugo on PATH as the preview already does
  • Scaffolding new sites or CI workflows (#17)
> *This was generated by AI during triage.* ## Agent Brief **Category:** enhancement **Summary:** Git backbone for the editing flow: every Save additionally safety-commits the working tree to a local `wip` ref (crash-safe, never pushed); per-page Publish stages only that page's bundle, commits to main, and pushes after a `pull --rebase` (path policy: tool wins in `content/`, remote wins elsewhere) and a local pre-flight Hugo build that blocks broken publishes; per-page Discard reverts a bundle to its published state; the UI shows per-page Draft/Published/Modified badges derived from uncommitted diff state. **Dependency note:** this issue is **hard-blocked by #3** (edit one element end-to-end). It builds on #3's save path — the endpoint that writes a canonically serialized page file to disk — by wrapping it in git semantics; the scheduler handles ordering, so simply do not start until #3 is merged. The issue body's `docs/DECISIONS.md` references (D2, D3, D14, D16) point at a file that is not committed — this brief is self-contained and expands those decisions inline. **Current behavior:** The harness supervises `hugo server` for live preview and (after #3) can save an edited element: the save endpoint validates against the sidecar schema and writes the canonically serialized file straight to disk. That write is the only persistence: nothing is versioned by the tool, there is no publish or discard concept, and no notion of a page being "draft" versus "published". If an editor's machine dies mid-session, saved work exists only as loose working-tree files; if a save produces a broken site, nothing stops it from reaching the deployed branch by whatever manual process exists. The tool never invokes git; whether the site directory is a git repository is currently irrelevant to it. **Desired behavior:** *Model.* The site directory is a git repository whose `main` branch (with a push remote) is the published source of truth — a push to main is what triggers deployment. The editing model on top of git: - **Saved but unpublished** work lives as uncommitted changes in the working tree, mirrored into a local `wip` safety ref. - **Published** means committed to main and pushed. - A page's **bundle** is the unit of publish/discard: for a Hugo page bundle (a directory with `index.md`/`_index.md`), the whole directory including page resources; for a plain single-file page, that file. *Save → wip safety commit.* Every successful save, after writing the file, commits a snapshot of the entire working tree to a local `wip` ref. That ref exists purely for crash safety and is never pushed. The snapshot must not disturb the editor's real state: the working tree, the index, and the checked-out branch are exactly as before (use a mechanism that commits without touching the user's index/HEAD). Killing the tool at any point and restarting it loses no saved work — the files are on disk and their history is recoverable from the `wip` ref. Save must not fail overall if the working tree is momentarily identical to the previous snapshot (an empty diff is not an error). *Per-page Publish.* Publishing page A stages exactly A's bundle paths, commits them to main with a meaningful message identifying the page, and pushes. Page B's pending edits stay uncommitted in the working tree throughout — they are neither committed, stashed away permanently, nor lost. "Publish all" commits every pending content change and pushes. A publish that would be empty (page already matches main) is reported as such, not committed. *Pre-flight build.* Before anything is committed to main, the tool runs a local `hugo` build of the exact tree that would be published — main plus the bundle(s) being published, **not** the whole working tree (page B's half-finished draft must not be able to fail page A's publish, and must not sneak into it either). If the build fails, the publish is refused, nothing is committed or pushed, and the Hugo error output is shown to the editor. The pre-flight must not disrupt the running preview server (build into a throwaway destination). *Pull --rebase and path policy.* Before pushing, the tool runs `pull --rebase` against the remote. Conflicts resolve by path policy: inside `content/` the tool's local version wins; everywhere else (theme, layouts, config, assets) the remote version wins. Net effect: a developer pushing theme changes while an editor works never blocks a publish — the rebase merges cleanly under the policy and the publish proceeds. If the push is rejected anyway (e.g. a race), the error is surfaced to the editor and retrying the publish works without duplicating commits. *Per-page Discard.* Discarding page A reverts A's bundle to its last-published (main) state, after an explicit confirmation in the UI. Other pages' pending edits are untouched. The preview reflects the revert via LiveReload since the files change on disk. Discarding a page that was never published (no version on main) removes it, with the confirmation making that explicit. *Draft/Published/Modified badges.* Page state is derived from the uncommitted diff against main: **Published** (bundle identical to main), **Modified** (exists on main, has uncommitted local changes), **Draft** (exists locally, never published to main). The tree and page views in the SPA show these badges, served by the API, and they update after save, publish, and discard. *Draft-link warning.* Publishing a page whose content links to a page that is still in Draft state (would 404 on the live site) warns the editor before proceeding; the editor can confirm and publish anyway. *Degradation.* If the site directory is not a git repository (or main/remote is missing), the tool does not crash: saving still writes files as in #3, and the git-dependent surface (publish, discard, badges) reports a clear, actionable error state instead of pretending to work. **Key interfaces:** - A dedicated internal git package/component encapsulating all repository operations (wip snapshot, per-path-set commit and push, rebase with path policy, per-bundle revert, per-page status). Whether it shells out to the `git` CLI or uses a library is the implementer's choice, but the server should depend on its interface, not on git details. - The save endpoint from #3 — unchanged contract for the caller, but a successful save now also produces a `wip` ref commit. - New mutating API endpoints for publish (single page and all) and discard (single page, gated on client-side confirmation), returning structured success/refusal bodies; a pre-flight failure returns the Hugo error text for display. - Page listing/status API: per-page state (`draft` / `published` / `modified`) exposed wherever the SPA renders the page tree and the open page, so badges are data-driven. - Bundle resolution: a function mapping a content-relative page path to the set of repository paths that form its bundle — reused by publish, discard, and status. **Acceptance criteria:** - [ ] Every successful save produces a commit on the local `wip` ref snapshotting the working tree; the ref is never pushed, and the editor's index, HEAD, and branch are untouched by the snapshot - [ ] Killing the harness after a save and restarting it loses no saved work; the wip history contains the snapshot - [ ] Publishing page A commits and pushes only A's bundle to main; page B's pending edits remain uncommitted locally before and after - [ ] "Publish all" commits and pushes every pending content change - [ ] Pre-flight builds the exact to-be-published tree (main + published bundle, without other pages' drafts); a failing build refuses the publish, commits and pushes nothing, and shows the Hugo error to the editor - [ ] `pull --rebase` runs before every push; a concurrent remote push touching theme files merges cleanly under the path policy (tool wins in `content/`, remote wins elsewhere) and the publish succeeds - [ ] Discard restores a page's bundle to its last-published state after explicit confirmation, leaving other pages' edits intact, and the preview updates - [ ] Publishing a page that links to a still-Draft page warns before proceeding and allows an explicit override - [ ] Tree and page views show accurate per-page Draft/Published/Modified badges that update after save, publish, and discard - [ ] A site directory that is not a git repository degrades gracefully: saves still work, git-dependent features report an actionable error instead of crashing - [ ] Git behaviors (wip snapshot, selective publish, path policy rebase, discard, status derivation) are covered by tests against real temporary repositories **Out of scope:** - CI/deployment status feedback after the push (#11) — this issue ends at a successful `git push` - Commit author identity from `X-Remote-User`, locks, and CSRF protection (#13) — a fixed configured/default author is fine here - Media uploads and bundle resources management (#12) — but bundle-path resolution should already include resources present in a bundle directory - Page create/rename/move/delete (#9); publishing UI only covers pages that exist - Hugo version pinning for the pre-flight build (#5) — use the `hugo` on PATH as the preview already does - Scaffolding new sites or CI workflows (#17)
Author
Owner

Closing as superseded: the maintainer has decided on a clean-slate reset of the project (issue #29, PR #30). The v1 implementation this issue builds on is being removed and the plan will be re-drawn from scratch; this spec is retired rather than actioned.

Closing as superseded: the maintainer has decided on a clean-slate reset of the project (issue #29, PR #30). The v1 implementation this issue builds on is being removed and the plan will be re-drawn from scratch; this spec is retired rather than actioned.
Sign in to join this conversation.
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#10
No description provided.