diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 2db9264..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,184 +0,0 @@ -# AGENTS.md - -## CLEAN CODE ENFORCEMENT RULES - -Source: "Clean Code" by Robert C. Martin — distilled for AI agents - -You are a senior software engineer who has **memorized and strictly follows** _Clean Code_. -Before writing or modifying any code, **you must**: - -1. **Cite 1–3 specific rules** from this file that apply. -2. **Apply them exactly** — no exceptions unless explicitly approved. -3. **Never violate a rule** to save lines or time. - ---- - -### 1. NAMING (Chapter 2) - -- **Use intention-revealing names** - Bad: `d`, `data`, `temp` - Good: `elapsedDaysSincePurchase`, `userSessionTimeoutSeconds` - -- **Avoid disinformation** - Never: `accountList` (if not a `List`) - Never: `theUser` (no article) - -- **Make meaningful distinctions** - Bad: `copy1`, `a1`, `a2` - Good: `sourceFilePath`, `destinationFilePath` - -- **Use pronounceable, searchable names** - Prefer: `generationTimestamp` - Avoid: `genymdhms` - -- **Class names**: Noun or noun phrase (`Customer`, `WikiPage`) -- **Method names**: Verb or verb phrase (`deletePage()`, `save()`) - ---- - -### 2. FUNCTIONS (Chapter 3) - -- **Do ONE thing** (Single Responsibility Principle) - If you can extract another function with a name that is not merely a restatement, **you must**. - -- **Small**: - - **< 20 lines** (including comments) - - **< 4 levels of nesting** - - **< 3 arguments** (0–2 preferred) - -- **One level of abstraction per function** - High-level steps → call lower-level functions - Never mix `saveToDatabase()` with `validateEmailFormat()` - -- **Stepdown Rule**: Code should read like a newspaper — high to low abstraction - -- **Use descriptive names** - A long name is better than a short name + comment - -- **Command Query Separation**: - - Functions should either **do something** or **answer something**, not both - Bad: `setUserActive(id, true)` → returns old value - Good: `activateUser(id)` + `wasUserActive(id)` - ---- - -### 3. COMMENTS (Chapter 4) - -- **Good comments**: - - Legal (license) - - Clarify intent (`// retry 3 times to avoid transient DB failure`) - - Warning of consequences - - TODO with ticket link - -- **Bad comments**: - - Restate code - - Mislead - - Mandated (e.g., `// Default constructor`) - - Journal (`// Changed by John on 2025-11-01`) - -- **Strongly prefer code over comments** - Extract to function with good name instead of comment - ---- - -### 4. FORMATTING (Chapter 5) - -- **Vertical**: - - One blank line between logical sections - - Related lines grouped (newspaper style) - -- **Horizontal**: - - **< 100 characters per line** - - Indent consistently (4 spaces or 2 — match project) - -- **Team rules**: - Follow `.editorconfig` and `ruff` config - ---- - -### 5. OBJECTS & DATA STRUCTURES (Chapter 6) - -- **Prefer data abstraction** - Hide implementation: `coordinates.getX()` not `coordinates.x` - -- **Law of Demeter**: - A method `f` of object `C` should only call methods of: - - `C` - - Objects created by `f` - - Objects passed as arguments - - Objects held in instance variables - ---- - -### 6. ERROR HANDLING (Chapter 7) - -- **Use exceptions, not return codes** - Bad: `if (deletePage(page) == E_OK)` - Good: `try { deletePage(page) } catch (...)` - -- **Don’t return null** - Throw exception or return special case object - -- **Don’t pass null** - Validate inputs early - -- **Wrap third-party APIs** in try/catch if needed - ---- - -### 7. BOUNDARIES (Chapter 8) - -- **Third-party code**: - Wrap in adapter (`OurDatabase`, not raw `PrismaClient`) - -- **Learning tests**: - Write tests to explore new APIs — keep them - ---- - -### 8. UNIT TESTS (Chapter 9) - -- **F.I.R.S.T. Principles**: - - **Fast** - - **Independent** - - **Repeatable** - - **Self-validating** - - **Timely** - -- **One assert per test** (when possible) - -- **Test names**: `shouldReturnTrueWhenUserIsActive` - ---- - -### 9. CLASSES (Chapter 10) - -- **Small**: First few methods → small class -- **Single Responsibility**: One reason to change - -- **Cohesion**: Instance variables used by most methods - -- **Organize for change**: Open/Closed where possible - ---- - -### 10. SYSTEMS & EMERGENCE (Chapters 11–13) - -- **Eliminate duplication** (DRY) - - Same logic → extract - - Similar logic → template method or strategy - -- **Expressiveness**: Code should read like well-written prose - -- **Minimal classes and methods** - ---- - -### ENFORCEMENT INSTRUCTIONS (FOR AI AGENTS) - -**Before generating code**: - -1. List 2–3 rules from this file that apply. -2. Confirm: "I will follow these rules exactly." -3. Generate code. -4. After code: List which rules were applied and how. diff --git a/PROMPT.md b/PROMPT.md deleted file mode 100644 index d367460..0000000 --- a/PROMPT.md +++ /dev/null @@ -1,46 +0,0 @@ -# Overview - -Create a command line tool, named git-wt, that will use 'git worktree' under the hood, to manage Git worktrees. -It will have several subcommands: - -- create [-u|--upstream ] - - upstream should default to whatever resolved sym-ref is in .git/refs/remotes/origin/HEAD (hard-coded to origin/HEAD), e.g., origin/main - - path should create a sibling directory of the workdir using a suffix that based on the normalized that replaces / with .; for example, if the main worktree is at ~/myrepo then if were nn/feat-1 it would create ~/myrepo.nn.feat-1 - - if the branch or directory already exist create should fail - - will run `git worktree add -b ` - - the branch named should have its upstream set to - - if the normalized is an invalid directory name then `git worktree add` will fail, we need not validate the name further -- list - - can use `git worktree list --porcelain` but should just show "pretty" names for the worktrees and the relative path to its workdir - - it should only show any Git worktrees that appear to have been created by git-wt based on them being siblings of the "main worktree" and using the . name normalization suffix - - use a libgloss table -- prune [-p|--prompt] - - can use the remove subcommand on any worktrees, from the list subcommand, that have had their branch merged to upstream branch and have clean workdirs (use the same logic as in the remove command) - - unmerged/unclean worktrees should be silently skipped unless the --prompt option is provide in which case the user can decide, per worktree, whether to force removal - - merged/clean worktree should be removed without confirmation - - use github.com/charmbracelet/huh for the prompt, use a multi-select with the merged/clean worktree already selected, then remove can be called with --force since the user confirmed -- remove [-f|--force] - - can use `git worktree remove` but resolves the argument from the - - if the worktree's workdir is not removed after running `git worktree remove` it should warn the user - - `git worktree remove` might delete the branch but if not use `git branch -d` unless force was specified then use `git branch -D` - - after remove runs both the worktree and its branch should be removed, the upstream_branch should not be touched - - should fail if their branch has note been merged to its own upstream branch (use git merge-base --is-ancestor) or the workdir is not clean unless the --force option is provided to override this check -- fang should automatically inject the completion subcommand - -# Technical Implementation Details - -- Initialize a go module named github.com/nnutter/git-wt at the repo root, using Go 1.26.2 -- All code should have test coverage. Tests should create a Git repo in order to actually be able to run Git commands. Focus on end-to-end tests that actually run git-wt commands inside the test Git repo over unit tests but create unit tests if you create helper functions that are moderately complex. -- Prefer using github.com/go-git/go-git/v6 over executing Git commands. I think that that package calls workdirs worktrees which is a misnomer so you will need to shell out to the git command to interact with Git worktrees. -- Use github.com/spf13/cobra to create the command -- Use github.com/charmbracelet/lipgloss/v2 for styling output (STDOUT and STDERR) -- log/slog can be used for logging but logging should really only be used for debugging message -- STDERR should be used for any status messages, using lipgloss -- STDOUT should only be used for output that could be piped to another command (perhaps no STDOUT) -- Create a struct to represent the subcommand flags and arguments - - The struct should have a method Execute([]string) error to implement the RunE on a cobra.Command - - A New\*() constructor should construct the cobra.Command, bind flags, etc. and return the cobra.Command to be bound to the root command -- Root command can just be a package var, in gitwt.go -- Each subcommand should be in its own file, e.g., gitwt_create.go -- The root command and all subcommands should be in an internal/gitwt package -- A main.go can execute the root command using charm.land/fang/v2 diff --git a/internal/gitwt/gitwt.go b/internal/gitwt/gitwt.go index c52fdcf..4a91b46 100644 --- a/internal/gitwt/gitwt.go +++ b/internal/gitwt/gitwt.go @@ -11,12 +11,14 @@ func NewRootCommand() *cobra.Command { SilenceErrors: true, SilenceUsage: true, } + rootCommand.CompletionOptions.HiddenDefaultCmd = true rootCommand.AddCommand(NewCreateCommand()) rootCommand.AddCommand(NewListCommand()) rootCommand.AddCommand(NewMigrateCommand()) rootCommand.AddCommand(NewPruneCommand()) rootCommand.AddCommand(NewRemoveCommand()) + rootCommand.AddCommand(NewShellCommand()) return rootCommand } diff --git a/internal/gitwt/gitwt_shell.go b/internal/gitwt/gitwt_shell.go new file mode 100644 index 0000000..9621e8f --- /dev/null +++ b/internal/gitwt/gitwt_shell.go @@ -0,0 +1,27 @@ +package gitwt + +import ( + "github.com/spf13/cobra" +) + +type shellCommandOptions struct{} + +func NewShellCommand() *cobra.Command { + options := &shellCommandOptions{} + + command := &cobra.Command{ + Use: `shell`, + Short: `Generate shell integration for worktrees`, + Args: cobra.NoArgs, + RunE: options.Execute, + } + command.CompletionOptions.HiddenDefaultCmd = true + + command.AddCommand(NewZshCommand()) + + return command +} + +func (x *shellCommandOptions) Execute(command *cobra.Command, args []string) error { + return command.Help() +} diff --git a/internal/gitwt/gitwt_shell_zsh.go b/internal/gitwt/gitwt_shell_zsh.go new file mode 100644 index 0000000..784165e --- /dev/null +++ b/internal/gitwt/gitwt_shell_zsh.go @@ -0,0 +1,150 @@ +package gitwt + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/spf13/cobra" +) + +type zshCommandOptions struct { + name string + out string + force bool +} + +func NewZshCommand() *cobra.Command { + options := &zshCommandOptions{} + + command := &cobra.Command{ + Use: `zsh`, + Short: `Generate zsh function and completion for worktree switching`, + Args: cobra.NoArgs, + RunE: options.Execute, + } + + command.Flags().StringVarP(&options.name, `name`, `n`, `gt`, `name of generated zsh function`) + command.Flags().StringVarP(&options.out, `out`, `o`, xdgDataHome()+`/zsh/site-functions`, `output directory for generated files`) + command.Flags().BoolVarP(&options.force, `force`, `f`, false, `overwrite existing generated files`) + + return command +} + +func xdgDataHome() string { + if xdg := os.Getenv(`XDG_DATA_HOME`); xdg != `` { + return xdg + } + return filepath.Join(os.Getenv(`HOME`), `.local`, `share`) +} + +func (x *zshCommandOptions) Execute(command *cobra.Command, args []string) error { + outDir := x.out + + if err := os.MkdirAll(outDir, 0o755); err != nil { + return fmt.Errorf(`create output directory: %w`, err) + } + + functionPath := filepath.Join(outDir, x.name) + completionPath := filepath.Join(outDir, `_`+x.name) + + if !x.force { + if _, err := os.Stat(functionPath); err == nil { + return fmt.Errorf(`function file %q already exists; use --force to overwrite`, functionPath) + } + if _, err := os.Stat(completionPath); err == nil { + return fmt.Errorf(`completion file %q already exists; use --force to overwrite`, completionPath) + } + } + + if err := x.writeFunctionFile(functionPath); err != nil { + return err + } + + if err := x.writeCompletionFile(completionPath); err != nil { + return err + } + + fmt.Fprintf(command.ErrOrStderr(), "Wrote %s\n", functionPath) + fmt.Fprintf(command.ErrOrStderr(), "Wrote %s\n", completionPath) + fmt.Fprintf(command.ErrOrStderr(), "Ensure %s is on fpath. Restart zsh or run compinit if completion does not appear immediately.\n", outDir) + + return nil +} + +func (x *zshCommandOptions) writeFunctionFile(target string) error { + content := `# Generated by git-wt shell zsh +# DO NOT EDIT. Regenerate with git-wt shell zsh + +` + x.name + `() { + if [[ -z "$1" ]]; then + echo "Usage: ` + x.name + ` " + return 1 + fi + + local main_dir=$(git worktree list --porcelain | head -n1 | sed "s/^worktree //") + local parent_dir=$(dirname $main_dir) + local repo_name=$(basename $main_dir) + + local arg=$1 + + # Strip repository prefix from arg if used + if [[ $arg == $repo_name.* ]]; then + arg=${arg#$repo_name.} + fi + + case "$1" in + main) + if [[ $(pwd) == $main_dir ]]; then + echo "Already in main worktree" + return 0 + fi + if ! [[ -d $main_dir ]]; then + echo "Main worktree not found" >&2 + return 1 + fi + cd $main_dir + ;; + *) + local target_dir=$parent_dir/${repo_name}.${arg//\//.} + if [[ $(pwd) == $target_dir ]]; then + echo "Already in $arg" + return 0 + fi + if ! [[ -d $target_dir ]]; then + echo "Worktree $arg not found at $target_dir" >&2 + return 1 + fi + cd $target_dir + ;; + esac +} +` + + return os.WriteFile(target, []byte(content), 0o644) +} + +func (x *zshCommandOptions) writeCompletionFile(target string) error { + content := `#compdef ` + x.name + ` + +# Generated by git-wt shell zsh +# DO NOT EDIT. Regenerate with git-wt shell zsh + +_` + x.name + `() { + if ! git rev-parse --is-inside-work-tree 1>/dev/null 2>/dev/null; then + return 1 + fi + + local main_dir + main_dir=$(git worktree list --porcelain | head -n1 | sed "s/^worktree //") + + local -a worktrees + worktrees=(main) + worktrees+=($(git worktree list --porcelain 2>/dev/null | grep '^worktree ' | tail -n +2 | sed 's|^worktree '"$main_dir"'\.||')) + + _describe 'worktrees' worktrees +} +` + + return os.WriteFile(target, []byte(content), 0o644) +} diff --git a/main.go b/main.go index 76f81fa..d57e65e 100644 --- a/main.go +++ b/main.go @@ -9,7 +9,6 @@ import ( ) func main() { - gitwt.Command.CompletionOptions.HiddenDefaultCmd = true if err := fang.Execute(context.Background(), gitwt.Command); err != nil { os.Exit(1) }