diff options
| -rw-r--r-- | CLAUDE.md | 7 | ||||
| -rw-r--r-- | README.md | 5 | ||||
| -rw-r--r-- | completions/gitctl.bash | 2 | ||||
| -rwxr-xr-x | gitctl | 21 |
4 files changed, 31 insertions, 4 deletions
@@ -10,6 +10,8 @@ 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. + Installed locally at `/usr/local/bin/gitctl` (the user copies it there by + hand; not done by this repo). - `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`. @@ -57,6 +59,11 @@ separate `AuthorizedKeysFile` (`~git/.ssh/gitctl_keys`) wired via a - **`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. +- **`repo create --private` runs Phase 1 only.** It makes the gitolite stanza + and bare repo but never calls the helper, so the repo is never written to + cgitrc and stays off the public cgit index (shows `PRIV` in `repo list`). + `--section` is required unless `--private` (enforced in `cmd_create`, not + argparse, since the rule is conditional). - Every operation is idempotent and safe to re-run. - No em dashes in user-facing text. - Work on `master`, no feature branches. @@ -173,6 +173,7 @@ ssh git_push info # gitolite access check gitctl sections list gitctl sections add "Generic Projects" gitctl repo create publisher --section SlackBuilds --desc "..." +gitctl repo create secrets --private gitctl repo list gitctl repo desc publisher "a new description" gitctl repo add-remote publisher --remote-name origin @@ -193,6 +194,10 @@ for the SSH key, so it is not fully unattended. - `repo create` shows the gitolite.conf diff and asks before committing. On decline it reverts the conf edit, leaving a clean tree. +- `--private` creates a Phase 1 only repo: the gitolite stanza and bare repo + are made, but the repo is never added to cgitrc, so it stays off the public + cgit index. `--section` is not required (and is ignored) with `--private`; + the repo shows as `PRIV` in `repo list`. - `-y` / `--yes` (a global flag, so it goes BEFORE the subcommand: `gitctl -y repo create ...`) skips that y/N confirmation. The diff is still printed. It does NOT make creation unattended: the phase-1 push still needs diff --git a/completions/gitctl.bash b/completions/gitctl.bash index 321a917..497d0a8 100644 --- a/completions/gitctl.bash +++ b/completions/gitctl.bash @@ -58,7 +58,7 @@ _gitctl() { # flags, by subcommand if [[ $cur == -* ]]; then case "$group $action" in - "repo create") COMPREPLY=($(compgen -W "--section --desc --owner" -- "$cur")) ;; + "repo create") COMPREPLY=($(compgen -W "--section --private --desc --owner" -- "$cur")) ;; "repo add-remote") COMPREPLY=($(compgen -W "--remote-name" -- "$cur")) ;; esac return @@ -177,6 +177,10 @@ def _self_test(): # repo delete takes a name; -y parses but cmd_delete ignores it (always confirms) da = pr.parse_args(["-y", "repo", "delete", "doomed"]) assert da.action == "delete" and da.name == "doomed" and da.yes is True + # --private: defaults off, --section no longer hard-required at the parser level + assert pr.parse_args(["repo", "create", "r", "--section", "S"]).private is False + pa = pr.parse_args(["repo", "create", "r", "--private"]) + assert pa.private is True and pa.section is None print("self-test OK") @@ -232,6 +236,8 @@ def _confirm(prompt): def cmd_create(cfg, a): + if not a.private and not a.section: + sys.exit("error: --section is required unless --private is given") owner = a.owner or cfg["default_owner"] conf_path = os.path.join(cfg["admin_clone_path"], "conf", "gitolite.conf") @@ -266,7 +272,11 @@ def cmd_create(cfg, a): else: sys.exit(f"error: timed out waiting for {a.name}.git on the server") - # PHASE 2: server-side via helper + # PHASE 2: server-side via helper. Skipped for private repos: they are + # never written to cgitrc, so they stay off the public cgit index. + if a.private: + print(f"done: {a.name} created (private, not in cgit)") + return 0 sections = call_helper(cfg, ["list-sections"], check=False).stdout.split("\n") if a.section not in [s.strip() for s in sections if s.strip()]: call_helper(cfg, ["add-section", "--name", a.section]) @@ -361,10 +371,15 @@ def build_parser(): "pushing (declining reverts the edit), then polls until gitolite " "has built the bare repo. Phase 2 ensures the cgit section exists, " "files the repo under it, and optionally sets its description. " + "With --private, Phase 2 is skipped: the repo is never added to " + "cgitrc, so it stays off the public cgit index. " "Every step is idempotent and safe to re-run.") rc.add_argument("name", help="repo name; used for the gitolite stanza, the bare repo, and cgit") - rc.add_argument("--section", required=True, - help="cgit section to file the repo under; created automatically if missing") + rc.add_argument("--section", + help="cgit section to file the repo under; created automatically if " + "missing. Required unless --private.") + rc.add_argument("--private", action="store_true", + help="create a private repo: Phase 1 only, never added to cgit") rc.add_argument("--desc", help="optional initial cgit description for the repo") rc.add_argument("--owner", help="cgit repo.owner string (default: default_owner from config)") |
