From dd69a6b1173c1c4930b50a24803e10cf23b0b9af Mon Sep 17 00:00:00 2001 From: brutsalvadi Date: Thu, 7 May 2026 22:28:48 +0200 Subject: [PATCH] Add git commit with attribution on every page/story save and delete --- edit/server.py | 53 ++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/edit/server.py b/edit/server.py index e3826cb..c606156 100644 --- a/edit/server.py +++ b/edit/server.py @@ -43,6 +43,41 @@ _SAFE_SLUG = re.compile(r"^[a-zA-Z0-9_][a-zA-Z0-9_\-/]*$") _ALLOWED_IMAGE_TYPES = {"image/jpeg", "image/png", "image/webp", "image/gif"} _MAX_IMAGE_BYTES = 10 * 1024 * 1024 # 10 MB +# Git attribution — on VPS the work tree has no .git dir; bare repo is separate. +# Set GIT_DIR to the bare repo path in the systemd service; GIT_WORK_TREE is _ROOT. +_GIT_DIR = os.environ.get("GIT_DIR") +_git_lock = asyncio.Lock() + + +async def _git_commit(file_path: Path, handle: str, verb: str) -> None: + rel = str(file_path.relative_to(_ROOT)) + env = os.environ.copy() + if _GIT_DIR: + env["GIT_DIR"] = _GIT_DIR + env["GIT_WORK_TREE"] = str(_ROOT) + env.setdefault("GIT_COMMITTER_NAME", "bincio-wiki") + env.setdefault("GIT_COMMITTER_EMAIL", "wiki@bincio.wiki") + try: + async with _git_lock: + add = await asyncio.create_subprocess_exec( + "git", "add", rel, + cwd=str(_ROOT), env=env, + stdout=asyncio.subprocess.DEVNULL, + stderr=asyncio.subprocess.DEVNULL, + ) + await add.wait() + commit = await asyncio.create_subprocess_exec( + "git", "commit", + "-m", f"{handle}: {verb} {file_path.stem}", + "--author", f"{handle} <{handle}@bincio.wiki>", + cwd=str(_ROOT), env=env, + stdout=asyncio.subprocess.DEVNULL, + stderr=asyncio.subprocess.DEVNULL, + ) + await commit.wait() + except Exception as e: + print(f"[git] commit failed for {rel}: {e}", flush=True) + def _unique_name(directory: Path, filename: str) -> str: stem, suffix = Path(filename).stem, Path(filename).suffix @@ -260,11 +295,16 @@ async def get_page(slug: str, user: User = Depends(require_auth)) -> JSONRespons @app.post("/pages/{slug:path}") async def save_page(slug: str, body: PageBody, user: User = Depends(require_auth)) -> JSONResponse: - return _save(slug, body, pages_dir) + result = _save(slug, body, pages_dir) + await _git_commit(_slug_to_path(slug, pages_dir), user.handle, "edited") + return result @app.delete("/pages/{slug:path}") async def delete_page(slug: str, user: User = Depends(require_auth)) -> JSONResponse: - return _delete(slug, pages_dir) + path = _slug_to_path(slug, pages_dir) + result = _delete(slug, pages_dir) + await _git_commit(path, user.handle, "deleted") + return result # ── Story endpoints ─────────────────────────────────────────────────────────── @@ -279,11 +319,16 @@ async def get_story(slug: str, user: User = Depends(require_auth)) -> JSONRespon @app.post("/stories/{slug:path}") async def save_story(slug: str, body: PageBody, user: User = Depends(require_auth)) -> JSONResponse: - return _save(slug, body, stories_dir) + result = _save(slug, body, stories_dir) + await _git_commit(_slug_to_path(slug, stories_dir), user.handle, "edited") + return result @app.delete("/stories/{slug:path}") async def delete_story(slug: str, user: User = Depends(require_auth)) -> JSONResponse: - return _delete(slug, stories_dir) + path = _slug_to_path(slug, stories_dir) + result = _delete(slug, stories_dir) + await _git_commit(path, user.handle, "deleted") + return result # ── Asset upload ─────────────────────────────────────────────────────────────