From a85a2eeb6d683e8bb2b0e1989cd5d6673266a18c Mon Sep 17 00:00:00 2001 From: brutsalvadi Date: Fri, 8 May 2026 08:57:49 +0200 Subject: [PATCH] Fix sidecar: advance refs/heads/main after each commit post-receive uses `git checkout -f ` which detaches HEAD in the bare repo. Without this fix, sidecar commits advance the detached HEAD but not refs/heads/main and are orphaned on the next push. Also commits CLAUDE.md docs update and the retry-loop sync-vps.sh. --- CLAUDE.md | 30 +++++++++++++++++++++++++++++- edit/server.py | 10 ++++++++++ scripts/sync-vps.sh | 15 ++++++++++++++- 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b2c2f2b..aca900c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,8 +12,11 @@ pointing to `../pages` and `../blog` (outside the submodule). No symlinks. ``` bincio_wiki/ pages/ wiki content (*.md files the community edits) - _docs/ software documentation (shown as separate section) + _docs/ documentation shown as a separate section blog/ blog/stories content (*.md files) + config/ bincio-specific configuration (outside site/ submodule) + sections.json wiki sections and subsections (imported by Astro pages + editor) + i.bonsai.md wikibonsai semantic tree (loaded by the index collection) assets/ user-uploaded images (gitignored; rsync'd to VPS separately) site/ Astro 6 app (git submodule → brutsalvadi/astro-bloomz) edit/ FastAPI edit server (Python, port 8001) @@ -114,6 +117,31 @@ Debian 12 VPS. nginx serves the built site from `/var/www/bincio/wiki/`. FastAPI proxied from `localhost:8001` via nginx `location /api/ { proxy_pass }`. See `docs/vps.md` for full nginx config and deployment steps. +## Edit concurrency — future work + +Current state: last-write-wins. No conflict detection. + +Planned approach (not yet implemented): + +1. **Git commits with attribution** — on every save, `git add ` + + `git commit -m "handle: edited page-name" --author="handle "`. + Requires an `asyncio.Lock` in `server.py` to serialize git operations. + +2. **Optimistic locking via base-hash check** — when the client loads a page + for editing, the server returns the current git commit hash alongside the + content. On save, the client sends that hash back. If `HEAD` has moved for + that file since then, the save is rejected with a 409 and a "pagina + modificata, ricarica prima di salvare" message. No lock to release, no + keepalive needed — the user can abandon the editor freely. + +3. **3-way merge (ideal)** — instead of rejecting on conflict, use + `git merge-file` with base = content at `baseHash`, ours = current HEAD, + theirs = user's new content. Different lines → automatic merge, one commit. + Same lines → 409 conflict error. Builds on step 2; adds it when needed. + +No pessimistic locking needed. Optimistic locking with 3-way merge handles +concurrent edits gracefully without timers or "are you still there?" prompts. + ## Git conventions - No `Co-Authored-By: Claude` trailers in commits diff --git a/edit/server.py b/edit/server.py index 09629e3..5fadb32 100644 --- a/edit/server.py +++ b/edit/server.py @@ -133,6 +133,16 @@ async def _git_commit(file_path: Path, handle: str, verb: str) -> None: stderr=asyncio.subprocess.DEVNULL, ) await commit.wait() + # post-receive uses `git checkout -f ` which detaches HEAD, so + # commits would advance the detached HEAD but not refs/heads/main and + # would be lost on the next push. Explicitly keep main up to date. + update_ref = await asyncio.create_subprocess_exec( + "git", "update-ref", "refs/heads/main", "HEAD", + cwd=str(_ROOT), env=env, + stdout=asyncio.subprocess.DEVNULL, + stderr=asyncio.subprocess.DEVNULL, + ) + await update_ref.wait() except Exception as e: print(f"[git] commit failed for {rel}: {e}", flush=True) diff --git a/scripts/sync-vps.sh b/scripts/sync-vps.sh index f29f237..284675d 100755 --- a/scripts/sync-vps.sh +++ b/scripts/sync-vps.sh @@ -1,5 +1,8 @@ #!/usr/bin/env bash # Pull web-edit commits from VPS, then push everything back. +# Retries the container push (up to 3x) if a VPS web-edit landed between +# our pull and push — receive.denyNonFastForwards is enabled on the bare repo +# so those races are rejected rather than silently dropped. # Usage: bash scripts/sync-vps.sh set -e cd "$(dirname "$0")/.." @@ -11,6 +14,16 @@ echo "==> Pushing site submodule to VPS..." (cd site && git push vps main) echo "==> Pushing container repo to VPS..." -git push vps main +for attempt in 1 2 3; do + if git push vps main; then + break + elif [ $attempt -lt 3 ]; then + echo " Push rejected — VPS has new commits, pulling and retrying (attempt $attempt)..." + git pull --rebase --autostash vps main + else + echo " Push failed after 3 attempts — run sync-vps.sh again manually." + exit 1 + fi +done echo "==> Done."