aboutsummaryrefslogtreecommitdiffstats
path: root/CLAUDE.md
diff options
context:
space:
mode:
Diffstat (limited to 'CLAUDE.md')
-rw-r--r--CLAUDE.md91
1 files changed, 91 insertions, 0 deletions
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..267bac1
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,91 @@
+# gitctl
+
+Thin-client CLI plus a server helper (runs as the git user) to manage repos on
+a personal gitolite3 + cgit server without raw ssh commands. Packaged later as
+a SlackBuild.
+
+## Architecture
+
+Two single-file scripts, no shared module:
+
+- `gitctl` — client, runs on the laptop. Dumb caller: loads TOML config, edits
+ the local gitolite-admin clone and pushes, shells out to the helper over SSH.
+- `gitctl-helper` — server helper, runs as the git user over a `command=`-restricted
+ SSH key. Holds all parsing-heavy logic. Dispatches on `SSH_ORIGINAL_COMMAND`.
+
+The restricted key forces `gitctl-helper` regardless of the command the client
+sends; the verb + args arrive in `SSH_ORIGINAL_COMMAND`, are `shlex.split` then
+fed to argparse. **No shell string interpolation, ever.** The client mirrors this
+with `shlex.quote`; the round-trip is pinned in both self-tests.
+
+Two distinct SSH aliases, enforced different at config load:
+- `push_ssh_alias` (e.g. `git_push`) — normal unrestricted git pushes
+- `helper_ssh_alias` (e.g. `git_helper`) — restricted helper key, also `User git`
+
+The helper runs as the **git** user, not root (the target server has
+`PermitRootLogin no`). git owns the bare repos' `description` files already; for
+`/etc/cgitrc` you `chown git:gitolite3 /etc/cgitrc` once during setup. Because
+git cannot create temp inodes in `/etc`, cgitrc is written **in place** (not via
+atomic rename), with a pre-write backup to `BACKUP_DIR`
+(`/var/lib/gitolite3/cgitrc-backups`).
+
+The helper key does NOT go in git's normal `~/.ssh/authorized_keys` — gitolite
+owns and regenerates that file, and an existing gitolite key would match first
+and run `gitolite-shell` (`FATAL: unknown git/gitolite command`). It lives in a
+separate `AuthorizedKeysFile` (`~git/.ssh/gitctl_keys`) wired via a
+`Match User git` stanza in sshd_config. Client-side, both ssh hosts use
+`IdentitiesOnly yes` so a multi-key agent does not trip the server's
+`MaxAuthTries` or offer a gitolite key first.
+
+## Hard rules (do not break)
+
+- **Python 3.11+ stdlib only.** No third-party deps (SlackBuild packaging).
+ Both scripts must stay single-file.
+- **cgit `repo.desc=` has exactly one writer: the existing `sync-cgit-descs.py`.**
+ Never write `repo.desc=` directly. Descriptions flow:
+ `set-desc` -> bare repo's `description` file -> run sync.
+- **cgitrc section blocks are flush-left and positional.** `section=` is positional,
+ so a new repo block must be inserted at the end of the section's span:
+ before the next `section=` line OR the next banner comment
+ (`# ---------- Name ------------#`), whichever comes first — a banner belongs
+ to the section it precedes (`insert_repo_block`, `BANNER_RE`). Ending the span
+ only at `section=` drops the block under the following section's banner.
+- **`description` file is owned `git:gitolite3`** (system user git, group gitolite3,
+ no gitolite3 user). `set-desc` writes in place (truncate-write) to preserve owner,
+ with `os.chown` back as insurance.
+- Every operation is idempotent and safe to re-run.
+- No em dashes in user-facing text.
+- Work on `master`, no feature branches.
+
+## Testing
+
+No framework. Each script has an assert-based `--self-test`:
+
+```
+python3 gitctl --self-test
+python3 gitctl-helper --self-test
+```
+
+The helper self-test prints expected `error:` lines to stderr (it exercises
+rejection paths) then `self-test OK`. Run both after any change.
+
+## Server constants (top of gitctl-helper)
+
+`REPO_BASE`, `CGITRC`, `SYNC_SCRIPT`, `BACKUP_DIR`, `DEFAULT_OWNER` — must match
+the actual server. Confirmed against the real `/etc/cgitrc` and
+`sync-cgit-descs.py`. Verified end to end on the live server (read, section add,
+two-phase repo create, desc, sync, idempotency, backups).
+
+## Docs / design
+
+- User/install docs: `README.md`
+- Design doc and implementation plan live under `docs/` locally but are
+ gitignored (they hold personal server details); not part of the published repo.
+
+## Before the first push to a public remote
+
+Untracking the personal artifacts only scrubbed future commits. The early
+history (commits before the genericize commit) still contains real server
+details and the original prompt. **Squash to a single clean commit before
+pushing anywhere public** (e.g. `git checkout --orphan` a fresh root from the
+current tree, or `git filter-repo`). Do not push the existing history.