# 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. ## History is clean - keep it that way The development history (which held real server details and the original prompt) was squashed to a single root commit before the first push, so the current history is clean. The personal dev artifacts (`gitctl_prompt.txt`, `docs/`) remain gitignored. Do not commit personal server details (hostnames, ports, key names, RW rules, real owner strings) to tracked files; the only place a real name/email belongs is the GPLv2 copyright headers.