aboutsummaryrefslogtreecommitdiffstats
path: root/CLAUDE.md
blob: a56836c25a145f09adde3b741ab45384a8b07d41 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# 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.