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
93
94
95
96
97
98
99
100
101
102
103
104
105
|
# gitctl TODO
Planned features. Each note captures the non-obvious bits so the work does not
re-discover them. Keep the hard rules from CLAUDE.md (stdlib only, single-file
scripts, single-writer repo.desc, idempotent, no em dashes, work on master).
## 1. delete repo -- DONE
`gitctl repo delete <name>` is implemented. It removes a repo from all three
places it lives, the inverse of `repo create`:
- gitolite stanza -> client `remove_stanza` (inverse of `add_stanza`), commit +
push (needs the SSH key / card, like create). PUSHED FIRST so gitolite stops
serving the repo before its bytes move.
- cgit block -> helper verb `delete-repo`, `remove_repo_block` (inverse of
`insert_repo_block`); deletes the repo.url/path/owner block + its spacer
blanks, leaves the section header. Backed up by write_cgitrc_lines, sync after.
- bare repo -> `trash_repo` MOVES it to `TRASH_DIR` (`/var/lib/gitolite3/trash`,
outside REPO_BASE, timestamped name) instead of rm, with a `.trashinfo`
breadcrumb. shutil.move, with a realpath check that the source is one level
under REPO_BASE (defense in depth on top of validate_url).
The helper exposes ONE verb (`delete-repo`) doing cgit-block removal + move +
.trashinfo. Always confirms (the global `-y` is deliberately NOT read by
cmd_delete). Idempotent: a piece already gone is skipped, not an error. Partial
failure (push ok, helper fails, or commit ok but push fails) is resumable by
re-running, same as create. Self-tests: remove_repo_block and remove_stanza both
pinned by an insert/add-then-remove round-trip plus middle/last/missing cases;
trash_repo and do_delete_repo exercised on temp dirs.
FUTURE (not built, YAGNI): a `gc-trash --older-than <days>` prune verb for cron.
The timestamped TRASH_DIR layout already supports it.
## 2. list repos -- DONE
`gitctl repo list` is implemented. It lists EVERY bare repo on disk (the full
set, public and private), not just the cgit-exposed ones, because cgit
auto-discovery is off and private repos are not in cgitrc. Source of truth is
`REPO_BASE/*.git`, not cgitrc; cgitrc membership only tags a repo PUB vs PRIV
and supplies its section. Description comes from each repo's `description` file
(truncated 60). Client renders a table, green public / dim private on a TTY.
Helper verb `list-repos`; primitive `list_repos()` + `repo_sections_from()`
(the latter dedupes the old `find_repo_section`). Self-tests cover both the
cgitrc parse and a temp-FS scan.
Still open from the original plan: extend bash completion to complete repo
NAMES (for `desc` / `delete` / `add-remote`) now that a list verb exists. The
`repo list` action itself is already in completion.
## 3. `-y` / `--yes` flag -- DONE
`-y` / `--yes` is implemented as a GLOBAL flag (on the top-level parser), so it
goes before the subcommand: `gitctl -y repo create ...`. Today the only y/N it
skips is the phase-1 push confirmation in `repo create`; the diff is still
printed and an "auto-confirmed" note shown. It does NOT bypass SSH key auth
(password / smartcard touch), so creation is never fully unattended. Section
auto-create was already silent, so behavior stays consistent.
Hard wall for delete (item 1): `-y` must NEVER be honored by a destructive
delete. The flag help says so, this note records it, and the global -y comment
in the client points at item 1. When delete is built, it reads its own
confirmation and ignores `a.yes`.
Bash completion offers `-y`/`--yes` at the top level and transparently skips a
leading `-y` when resolving group/action.
## Cross-cutting
Done as part of items 1-3: both SKILL.md files updated (delete + `-y` limit),
self-test coverage for every new helper verb, completion actions for `list` and
`delete`, README and CLAUDE.md brought current.
## Remaining
1. Repo-name bash completion -- DONE. `desc`, `delete`, and `add-remote`
complete repo NAMES at the name position from `gitctl repo list` (first
field, header dropped), one ssh call, only at that position so a stray Tab
never hits the network. `-y` before the subcommand is handled.
2. Stray sync-script echo -- DONE. The `No changes.` / `Wrote N change(s).`
lines come from the server sync script, passed through over ssh. Rather than
hide them, `call_helper` now prefixes every passthrough line with `server: `
(`_server_prefixed`), so they read clearly as remote output instead of
interleaved local noise.
3. FUTURE / YAGNI: `gc-trash --older-than <days>` helper verb to prune
`TRASH_DIR` entries past a retention window, runnable from cron. The
timestamped trash layout already supports it. It would be the ONLY real
`rm -rf` in the tool, so it needs hard guards (realpath-under-TRASH_DIR
check, no symlink follow, validated timestamp parse) and a dry-run default.
Do not build until trash actually accumulates and space needs reclaiming.
4. BUG: a crashed `repo create` left the working tree poisoned -- DONE. Phase 1
used to write the stanza into the working `conf/gitolite.conf` BEFORE the y/N
and commit; a run that died at the prompt (e.g. no TTY, EOFError) left the
edit uncommitted, and because `stanza_exists` read the working file, the
re-run reported "stanza already present", SKIPPED Phase 1 entirely (never
committed, never pushed), and timed out waiting for a bare repo gitolite was
never told to build. Hit live while creating the private knowledge_base repo.
Fix: Phase 1 is now driven by git STATE, not text presence. It ensures the
stanza is in the working file (idempotent), commits if the working tree
differs from HEAD, and pushes if HEAD is ahead of upstream. This recovers
both a leftover uncommitted edit and a committed-but-unpushed stanza, and is
a no-op when already in sync; confirm happens before the commit, abort
reverts to HEAD. A self-test pins the three git states with a throwaway repo
plus bare upstream.
|