aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-06-29 18:23:14 +0200
committerDanilo M. <danix@danix.xyz>2026-06-29 18:23:14 +0200
commit9359ec0eaedf8b8f8b68a33b96507589e0e2c651 (patch)
tree689eee26c804c95a90e84657d76add8568652679
parentffe0864406cd67f82071becf494f0dd9e701e149 (diff)
downloadgitctl-9359ec0eaedf8b8f8b68a33b96507589e0e2c651.tar.gz
gitctl-9359ec0eaedf8b8f8b68a33b96507589e0e2c651.zip
feat: repo create --private - Phase 1 only, never added to cgit
--private makes the gitolite stanza and bare repo but skips the helper call, 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. Bash completion offers --private; README and CLAUDE.md document the flag and the local /usr/local/bin install. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
-rw-r--r--CLAUDE.md7
-rw-r--r--README.md5
-rw-r--r--completions/gitctl.bash2
-rwxr-xr-xgitctl21
4 files changed, 31 insertions, 4 deletions
diff --git a/CLAUDE.md b/CLAUDE.md
index 7146290..1342973 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -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.
diff --git a/README.md b/README.md
index c8475f3..01b5af7 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/gitctl b/gitctl
index 1140b4c..fe04dae 100755
--- a/gitctl
+++ b/gitctl
@@ -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)")