Skip to content

feat: add 'zen repo add' and first-use repo registration - #26

Open
imkarrer wants to merge 2 commits into
mgreau:mainfrom
imkarrer:feat/repo-add
Open

feat: add 'zen repo add' and first-use repo registration#26
imkarrer wants to merge 2 commits into
mgreau:mainfrom
imkarrer:feat/repo-add

Conversation

@imkarrer

@imkarrer imkarrer commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Registering a repo with zen currently means the interactive zen setup walkthrough or hand-editing ~/.zen/config.yaml. This adds zen repo add — run from inside a clone, it infers the whole config entry — and makes zen work new / zen review offer to register the surrounding clone on first use.

What to review: the remote-URL inference in internal/gitrepo/detect.go, and the YAML-preserving config edit in AddRepo (internal/config/config.go).

Design and behavior
  • Everything is inferred from the clone. The short name is the clone's directory name (zen requires the main clone at base_path/<short>, so this is the only correct choice — no --name override), base_path is the parent directory, and full_name comes from the upstream remote when one exists, falling back to origin, so forks resolve to the repo the PRs live on.
  • First-use registration captures intent. Cloning a repo doesn't mean you'll contribute to it, so nothing happens at clone time. The moment you run zen work new or zen review inside an unregistered clone is the moment intent is observable — zen offers to register it there, prompting only when stdin is a TTY so scripted invocations never block. A git clone shell wrapper for clone-time registration is documented in docs/configuration.md.
  • Config edits preserve the user's file. AddRepo edits the YAML document via yaml.Node surgery rather than a struct round-trip, so comments and 2-space indentation survive. The write goes through a temp file + rename with the config's original permissions, so an interrupted write can't truncate the file and a 0600 config stays 0600. Re-adding an identical repo is a no-op; a short name or full name already configured with different values is an error, never a silent overwrite. Base paths under $HOME are written in ~/ form. The daemon already hot-reloads config on every poll tick, so no restart is needed.
  • Guards. zen repo add refuses to run from a linked worktree (its directory name and parent would produce a bogus entry). Remote parsing handles scp-style, https://, ssh://, and git:// URLs.
How it was verified
  • New tests: remote-URL parsing table, origin/upstream preference, detection from a subdirectory, linked-worktree, no-remote and unparseable-remote errors (internal/gitrepo/detect_test.go); add, idempotent re-add in both ~/absolute forms, both conflict shapes, missing and empty repos: sections, comment/~ preservation, and file-mode preservation for AddRepo (internal/config/config_test.go).
  • Coverage: internal/gitrepo at 100% of statements; AddRepo and its helpers covered on every reachable path (remaining uncovered lines are filesystem/encoder fault branches).
  • Full suite, go vet, and gofmt pass. The cmd/ glue (prompt + hooks) follows this repo's pattern of keeping cmd/ untested and logic in internal/.
  • Smoke-tested the built binary against a fake $HOME: fresh add from a fork with both remotes, idempotent second run, and a hand-commented config file round-tripping with comments, inline comments, and indentation intact.

@imkarrer
imkarrer marked this pull request as ready for review September 2, 2026 16:35
Register a clone with zen in one shot: the short name comes from the
clone's directory name, base_path from its parent directory, and
full_name from the upstream remote (falling back to origin) so forks
resolve to the repo the PRs live on. Adding the same repo twice is a
no-op, and conflicting entries error instead of silently overwriting.

zen work new and zen review now offer to register the surrounding clone
on first use, prompting only on an interactive terminal. Config edits go
through yaml.Node surgery so user comments and 2-space indentation in
~/.zen/config.yaml survive.
AddRepo previously truncated ~/.zen/config.yaml in place with a
hardcoded 0644 mode, which could widen a user's 0600 config and leave a
corrupt file behind an interrupted write. It now writes a temp file with
the original permissions and renames it over the config.

Also covers the remaining reachable paths: an empty 'repos:' key, an
unparseable remote URL through Detect, a scheme-only remote URL, and
identical re-adds with absolute base paths. internal/gitrepo is at 100%
statement coverage.
@mgreau

mgreau commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Nice!

@mgreau mgreau left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The YAML-preserving config edit and explicit duplicate checks are useful, and the full Go test suite and go vet passed locally. I reproduced one first-use registration bug that should be fixed before merging; details are inline.

The planned merge order is #23, #24, corrected #20, rebased #13, then this PR. Rebasing onto #20 requires retaining its ZEN_HOME-aware config directory while adding AddRepo. Keeping #13 before this PR ensures repositories inferred from upstream also fetch PR refs from the correct remote.

Comment thread cmd/helpers.go
fmt.Printf("%s is not registered with zen yet.\n", info.FullName)
fmt.Printf("Register it as %q (worktrees in %s)? [Y/n]: ", info.Short, ui.ShortenHome(info.BasePath, home))
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Treat EOF as cancellation and use a real TTY check. stdinIsTerminal currently tests ModeCharDevice, which is also true for /dev/null. Then scanner.Scan() returns false at EOF, but its result is ignored and the empty answer is accepted as yes. I reproduced the built CLI modifying config.yaml when running zen work new example test --no-terminal </dev/null from an unregistered clone, without any answer. Please check whether stdin is actually a terminal and return false when Scan fails, rather than interpreting EOF as an empty submitted response. Cover /dev/null and EOF at an interactive prompt.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants