aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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)")