From 2c833face265ebb5820b868e882cffb6640e9d74 Mon Sep 17 00:00:00 2001 From: Hans Elizaga Date: Fri, 3 Oct 2025 16:30:01 -0700 Subject: [PATCH 01/19] Add contributing guidelines, license, and restructure project files - Introduced CONTRIBUTING.md to outline contribution processes and coding standards. - Added LICENSE file with MIT License details. - Restructured README.md for clarity and expanded content on features and installation. - Removed gtr.sh and run_services.example.sh, replacing them with a more modular structure. - Added various adapter scripts for AI tools and editors to enhance functionality. --- CONTRIBUTING.md | 257 ++++++++++++ LICENSE | 21 + README.md | 566 +++++++++++++++++++++++-- adapters/ai/aider.sh | 28 ++ adapters/ai/claudecode.sh | 27 ++ adapters/ai/codex.sh | 29 ++ adapters/ai/continue.sh | 28 ++ adapters/ai/cursor.sh | 33 ++ adapters/editor/cursor.sh | 20 + adapters/editor/vscode.sh | 20 + adapters/editor/zed.sh | 20 + bin/gtr | 675 ++++++++++++++++++++++++++++++ completions/_gtr | 77 ++++ completions/gtr.bash | 56 +++ completions/gtr.fish | 58 +++ gtr.sh | 460 -------------------- lib/config.sh | 134 ++++++ lib/copy.sh | 107 +++++ lib/core.sh | 233 +++++++++++ lib/hooks.sh | 80 ++++ lib/platform.sh | 133 ++++++ lib/ui.sh | 70 ++++ run_services.example.sh | 15 - templates/gtr.config.example | 72 ++++ templates/run_services.example.sh | 71 ++++ templates/setup-example.sh | 36 ++ 26 files changed, 2814 insertions(+), 512 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 adapters/ai/aider.sh create mode 100644 adapters/ai/claudecode.sh create mode 100644 adapters/ai/codex.sh create mode 100644 adapters/ai/continue.sh create mode 100644 adapters/ai/cursor.sh create mode 100644 adapters/editor/cursor.sh create mode 100644 adapters/editor/vscode.sh create mode 100644 adapters/editor/zed.sh create mode 100755 bin/gtr create mode 100644 completions/_gtr create mode 100644 completions/gtr.bash create mode 100644 completions/gtr.fish delete mode 100644 gtr.sh create mode 100644 lib/config.sh create mode 100644 lib/copy.sh create mode 100644 lib/core.sh create mode 100644 lib/hooks.sh create mode 100644 lib/platform.sh create mode 100644 lib/ui.sh delete mode 100644 run_services.example.sh create mode 100644 templates/gtr.config.example create mode 100755 templates/run_services.example.sh create mode 100755 templates/setup-example.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..996cc5c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,257 @@ +# Contributing to gtr + +Thank you for considering contributing to `gtr`! This document provides guidelines and instructions for contributing. + +## How to Contribute + +### Reporting Issues + +Before creating an issue, please: + +1. **Search existing issues** to avoid duplicates +2. **Provide a clear description** of the problem +3. **Include your environment details**: + - OS and version (macOS, Linux distro, Windows Git Bash) + - Git version + - Shell (bash, zsh, fish) +4. **Steps to reproduce** the issue +5. **Expected vs actual behavior** + +### Suggesting Features + +We welcome feature suggestions! Please: + +1. **Check existing issues** for similar requests +2. **Describe the use case** - why is this needed? +3. **Propose a solution** if you have one in mind +4. **Consider backwards compatibility** and cross-platform support + +## Development + +### Architecture Overview + +``` +git-worktree-runner/ +├── bin/gtr # Main executable dispatcher +├── lib/ # Core functionality +│ ├── core.sh # Git worktree operations +│ ├── config.sh # Configuration (git-config wrapper) +│ ├── platform.sh # OS-specific utilities +│ ├── ui.sh # User interface (logging, prompts) +│ ├── copy.sh # File copying logic +│ └── hooks.sh # Hook execution +├── adapters/ # Pluggable integrations +│ ├── editor/ # Editor adapters (cursor, vscode, zed) +│ └── ai/ # AI tool adapters (aider) +├── completions/ # Shell completions (bash, zsh, fish) +└── templates/ # Example configs and scripts +``` + +### Coding Standards + +#### Shell Script Best Practices + +- **POSIX compliance**: Write POSIX-compatible shell code (use `#!/bin/sh`) +- **Set strict mode**: Use `set -e` to exit on errors +- **Quote variables**: Always quote variables: `"$var"` +- **Use local variables**: Declare function-local vars with `local` +- **Error handling**: Check return codes and provide clear error messages +- **No bashisms**: Avoid bash-specific features unless absolutely necessary + +#### Code Style + +- **Function names**: Use `snake_case` for functions +- **Variable names**: Use `snake_case` for variables +- **Constants**: Use `UPPER_CASE` for constants/env vars +- **Indentation**: 2 spaces (no tabs) +- **Line length**: Keep lines under 100 characters when possible +- **Comments**: Add comments for complex logic + +#### Example: + +```sh +#!/bin/sh +# Brief description of what this file does + +# Function description +do_something() { + local input="$1" + local result + + if [ -z "$input" ]; then + log_error "Input required" + return 1 + fi + + result=$(some_command "$input") + printf "%s" "$result" +} +``` + +### Adding New Features + +#### Adding an Editor Adapter + +1. Create `adapters/editor/yourname.sh`: + +```sh +#!/bin/sh +# YourEditor adapter + +editor_can_open() { + command -v yourcommand >/dev/null 2>&1 +} + +editor_open() { + local path="$1" + + if ! editor_can_open; then + log_error "YourEditor not found. Install from https://..." + return 1 + fi + + yourcommand "$path" +} +``` + +2. Update README.md with setup instructions +3. Update completions to include new editor +4. Test on macOS, Linux, and Windows if possible + +#### Adding an AI Tool Adapter + +1. Create `adapters/ai/yourtool.sh`: + +```sh +#!/bin/sh +# YourTool AI adapter + +ai_can_start() { + command -v yourtool >/dev/null 2>&1 +} + +ai_start() { + local path="$1" + shift + + if ! ai_can_start; then + log_error "YourTool not found. Install with: ..." + return 1 + fi + + (cd "$path" && yourtool "$@") +} +``` + +2. Update README.md +3. Update completions +4. Add example usage + +#### Adding Core Features + +For changes to core functionality (`lib/*.sh`): + +1. **Discuss first**: Open an issue to discuss the change +2. **Maintain compatibility**: Avoid breaking existing configs +3. **Add tests**: Provide test cases or manual testing instructions +4. **Update docs**: Update README.md and help text +5. **Consider edge cases**: Think about error conditions + +### Testing + +Currently, testing is manual. Please test your changes on: + +1. **macOS** (if available) +2. **Linux** (Ubuntu, Fedora, or Arch recommended) +3. **Windows Git Bash** (if available) + +#### Manual Testing Checklist + +- [ ] Create worktree with auto ID +- [ ] Create worktree with specific ID +- [ ] Create from remote branch +- [ ] Create from local branch +- [ ] Create new branch +- [ ] Open in editor (if testing adapters) +- [ ] Run AI tool (if testing adapters) +- [ ] Remove worktree +- [ ] List worktrees +- [ ] Test configuration commands +- [ ] Test completions (tab completion works) + +### Pull Request Process + +1. **Fork the repository** +2. **Create a feature branch**: `git checkout -b feature/my-feature` +3. **Make your changes** +4. **Test thoroughly** (see checklist above) +5. **Update documentation** (README.md, help text, etc.) +6. **Commit with clear messages**: + - Use present tense: "Add feature" not "Added feature" + - Be descriptive: "Add VS Code adapter" not "Add adapter" +7. **Push to your fork** +8. **Open a Pull Request** with: + - Clear description of changes + - Link to related issues + - Testing performed + - Screenshots/examples if applicable + +### Commit Message Format + +``` +: + + + + +``` + +**Types:** +- `feat`: New feature +- `fix`: Bug fix +- `docs`: Documentation changes +- `refactor`: Code refactoring (no functional changes) +- `test`: Adding or updating tests +- `chore`: Maintenance tasks + +**Examples:** +``` +feat: add JetBrains IDE adapter + +Add support for opening worktrees in IntelliJ, PyCharm, and other +JetBrains IDEs via the 'idea' command. + +Closes #42 +``` + +``` +fix: handle spaces in worktree paths + +Properly quote paths in all commands to support directories with spaces. +``` + +## Design Principles + +When contributing, please keep these principles in mind: + +1. **Cross-platform first** - Code should work on macOS, Linux, and Windows +2. **No external dependencies** - Avoid requiring tools beyond git and basic shell +3. **Config over code** - Prefer configuration over hardcoding behavior +4. **Fail safely** - Validate inputs and provide clear error messages +5. **Stay modular** - Keep functions small and focused +6. **User-friendly** - Prioritize good UX and clear documentation + +## Community + +- **Be respectful** and constructive +- **Help others** who are learning +- **Share knowledge** and best practices +- **Have fun!** This is a community project + +## Questions? + +- Open an issue for questions +- Check existing issues and docs first +- Be patient - maintainers are volunteers + +Thank you for contributing! 🎉 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..09b05f4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Anthropic PBC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 1ce346a..ef871d3 100644 --- a/README.md +++ b/README.md @@ -1,56 +1,548 @@ -# gtr — Git worktree runner for parallel Claude Code dev +# gtr - Git Worktree Runner -gtr creates and manages per-branch git worktrees (mono-2, mono-3, …) and automates setup: -- Detects/creates branches (remote/local/new from `main`) -- Opens GitHub Desktop to the new worktree (macOS) -- Copies `.env.local` and `CLAUDE.md` files (preserving paths) and `run_services.sh` -- Installs deps with pnpm and runs `turbo build` -- Shortcuts: cd, remove, launch Claude or Cursor, list worktrees +> A portable, cross-platform CLI for managing git worktrees with ease -Related guide: https://docs.anthropic.com/en/docs/claude-code/common-workflows#run-parallel-claude-code-sessions-with-git-worktrees +`gtr` makes it simple to create, manage, and work with [git worktrees](https://git-scm.com/docs/git-worktree), enabling you to work on multiple branches simultaneously without stashing or switching contexts. + +## Why Git Worktrees? + +Git worktrees let you check out multiple branches at once in separate directories. This is invaluable when you need to: + +- Review a PR while working on a feature +- Run tests on `main` while developing +- Quickly switch between branches without stashing +- Compare implementations side-by-side +- Run multiple development servers simultaneously + +## Why not just `git worktree`? + +While `git worktree` is powerful, it requires remembering paths and manually setting up each worktree. `gtr` adds: + +| Task | With `git worktree` | With `gtr` | +|------|---------------------|------------| +| Create worktree | `git worktree add ../repo-feature feature` | `gtr create --branch feature --auto` | +| Open in editor | `cd ../repo-feature && cursor .` | `gtr open 2 --editor cursor` | +| Start AI tool | `cd ../repo-feature && aider` | `gtr ai 2 --tool aider` | +| Copy config files | Manual copy/paste | Auto-copy via `gtr.copy.include` | +| Run build steps | Manual `npm install && npm run build` | Auto-run via `gtr.hook.postCreate` | +| List worktrees | `git worktree list` (shows paths) | `gtr list` (shows IDs + status) | +| Switch to worktree | `cd ../repo-feature` | `cd "$(gtr cd 2)"` | +| Clean up | `git worktree remove ../repo-feature` | `gtr rm 2` | + +**TL;DR:** `gtr` wraps `git worktree` with quality-of-life features for modern development workflows (AI tools, editors, automation). + +## Features + +- 🚀 **Simple commands** - Create and manage worktrees with intuitive CLI +- 🔧 **Configurable** - Git-config based settings, no YAML/TOML parsers needed +- 🎨 **Editor integration** - Open worktrees in Cursor, VS Code, or Zed +- 🤖 **AI tool support** - Launch Aider or other AI coding tools +- 📋 **Smart file copying** - Selectively copy configs/env files to new worktrees +- 🪝 **Hooks system** - Run custom commands after create/remove +- 🌍 **Cross-platform** - Works on macOS, Linux, and Windows (Git Bash) +- 🎯 **Shell completions** - Tab completion for Bash, Zsh, and Fish + +## Installation + +### Quick Install (macOS/Linux) -## Install (zsh on macOS) ```bash -curl -fsSL https://raw.githubusercontent.com//git-worktree-runner/main/gtr.sh -o ~/.gtr.sh -chmod +x ~/.gtr.sh -echo 'source ~/.gtr.sh' >> ~/.zshrc -exec zsh +# Clone the repository +git clone https://github.com/anthropics/git-worktree-runner.git +cd git-worktree-runner + +# Add to PATH (choose one) +# Option 1: Symlink to /usr/local/bin +sudo ln -s "$(pwd)/bin/gtr" /usr/local/bin/gtr + +# Option 2: Add to your shell profile +echo 'export PATH="$PATH:'$(pwd)'/bin"' >> ~/.zshrc # or ~/.bashrc +source ~/.zshrc ``` -## Usage +### Shell Completions (Optional) + +**Bash:** ```bash -# Create next available worktree from a branch -gtr create my-feature -# Or specify ID and branch -gtr create 3 ui-fixes +echo 'source /path/to/git-worktree-runner/completions/gtr.bash' >> ~/.bashrc +``` -# Jump into a worktree -gtr cd 2 +**Zsh:** +```bash +echo 'source /path/to/git-worktree-runner/completions/_gtr' >> ~/.zshrc +``` + +**Fish:** +```bash +ln -s /path/to/git-worktree-runner/completions/gtr.fish ~/.config/fish/completions/ +``` -# Start tools -gtr claude 2 -gtr cursor 2 -gtr desktop 2 +## Quick Start -# List / remove +```bash +# Create a worktree with auto-assigned ID +gtr create --branch my-feature --auto + +# Create worktree with specific ID +gtr create --id 3 --branch ui-fixes + +# List all worktrees gtr list + +# Open worktree in editor (if configured) +gtr open 2 --editor cursor + +# Start AI tool in worktree +gtr ai 2 --tool aider + +# Remove worktree +gtr rm 2 + +# Change to worktree directory +gtr cd 2 +``` + +## Commands + +### `gtr create` + +Create a new git worktree. + +```bash +gtr create [options] [branch-name] + +Options: + --branch Branch name + --id Worktree ID (default: auto-assigned) + --auto Auto-assign next available ID + --from Create branch from ref (default: main/master) + --track Track mode: auto|remote|local|none + --open [editor] Open in editor after creation + --ai [tool] Start AI tool after creation + --no-copy Skip file copying + --yes Non-interactive mode +``` + +**Examples:** +```bash +# Auto-assign ID, prompt for branch +gtr create --auto + +# Specific ID and branch +gtr create --id 2 --branch feature-x + +# Create from specific ref +gtr create --branch hotfix --from v1.2.3 + +# Create and open in Cursor +gtr create --branch ui --auto --open cursor + +# Create and start Aider +gtr create --branch refactor --auto --ai aider +``` + +### `gtr open` + +Open a worktree in an editor or file browser. + +```bash +gtr open [--editor ] + +Options: + --editor Editor: cursor, vscode, zed +``` + +**Examples:** +```bash +# Open in default editor +gtr open 2 + +# Open in specific editor +gtr open 2 --editor cursor +``` + +### `gtr ai` + +Start an AI coding tool in a worktree. + +```bash +gtr ai [--tool ] [-- args...] + +Options: + --tool AI tool: aider + -- Pass remaining args to tool +``` + +**Examples:** +```bash +# Start default AI tool +gtr ai 2 + +# Start Aider with specific model +gtr ai 2 --tool aider -- --model gpt-4o +``` + +### `gtr rm` + +Remove worktree(s). + +```bash +gtr rm [...] [options] + +Options: + --delete-branch Also delete the branch + --yes Non-interactive mode +``` + +**Examples:** +```bash +# Remove single worktree gtr rm 2 + +# Remove and delete branch +gtr rm 2 --delete-branch + +# Remove multiple worktrees +gtr rm 2 3 4 --yes +``` + +### `gtr list` + +List all git worktrees. + +```bash +gtr list +``` + +### `gtr cd` + +Change to worktree directory (prints info for shell integration). + +```bash +gtr cd +``` + +### `gtr config` + +Manage gtr configuration via git config. + +```bash +gtr config get [--global] +gtr config set [--global] +gtr config unset [--global] +``` + +**Examples:** +```bash +# Set default editor locally +gtr config set gtr.editor.default cursor + +# Set global worktree prefix +gtr config set gtr.worktrees.prefix "wt-" --global + +# Get current value +gtr config get gtr.editor.default +``` + +## Configuration + +All configuration is stored via `git config`, making it easy to manage per-repository or globally. + +### Worktree Settings + +```bash +# Base directory (default: -worktrees) +gtr.worktrees.dir = /path/to/worktrees + +# Name prefix (default: wt-) +gtr.worktrees.prefix = dev- + +# Starting ID (default: 2) +gtr.worktrees.startId = 1 + +# Default branch (default: auto-detect) +gtr.defaultBranch = main +``` + +### Editor Settings + +```bash +# Default editor: cursor, vscode, zed, or none +gtr.editor.default = cursor +``` + +**Setup editors:** +- **Cursor**: Install from [cursor.com](https://cursor.com), enable shell command +- **VS Code**: Install from [code.visualstudio.com](https://code.visualstudio.com), enable `code` command +- **Zed**: Install from [zed.dev](https://zed.dev), `zed` command available automatically + +### AI Tool Settings + +```bash +# Default AI tool: none (or aider, claudecode, codex, cursor, continue) +gtr.ai.default = none +``` + +**Supported AI Tools:** + +| Tool | Install | Use Case | Command Example | +|------|---------|----------|-----------------| +| **[Aider](https://aider.chat)** | `pip install aider-chat` | Pair programming, edit files with AI | `gtr ai 2 --tool aider` | +| **[Claude Code](https://claude.com/claude-code)** | Install from claude.com | Terminal-native coding agent | `gtr ai 2 --tool claudecode` | +| **[Codex CLI](https://github.com/openai/codex)** | `npm install -g @openai/codex` | OpenAI coding assistant | `gtr ai 2 --tool codex -- "add tests"` | +| **[Cursor](https://cursor.com)** | Install from cursor.com | AI-powered editor with CLI agent | `gtr ai 2 --tool cursor` | +| **[Continue](https://continue.dev)** | See [docs](https://docs.continue.dev/cli/install) | Open-source coding agent | `gtr ai 2 --tool continue` | + +**Examples:** +```bash +# Set default AI tool globally +gtr config set gtr.ai.default aider --global + +# Use specific tools per worktree +gtr ai 2 --tool claudecode -- --plan "refactor auth" +gtr ai 3 --tool aider -- --model gpt-4o +gtr ai 4 --tool continue -- --headless +``` + +### File Copying + +Copy files to new worktrees using glob patterns: + +```bash +# Add patterns to copy (multi-valued) +git config --add gtr.copy.include "**/.env.example" +git config --add gtr.copy.include "**/CLAUDE.md" +git config --add gtr.copy.include "*.config.js" + +# Exclude patterns (multi-valued) +git config --add gtr.copy.exclude "**/.env" +git config --add gtr.copy.exclude "**/secrets.*" +``` + +**⚠️ Security Note:** Be careful not to copy sensitive files. Use `.env.example` instead of `.env`. + +### Hooks + +Run custom commands after worktree operations: + +```bash +# Post-create hooks (multi-valued, run in order) +git config --add gtr.hook.postCreate "npm install" +git config --add gtr.hook.postCreate "npm run build" + +# Post-remove hooks +git config --add gtr.hook.postRemove "echo 'Cleaned up!'" +``` + +**Environment variables available in hooks:** +- `REPO_ROOT` - Repository root path +- `WORKTREE_PATH` - New worktree path +- `BRANCH` - Branch name + +**Examples for different build tools:** + +```bash +# Node.js (npm) +git config --add gtr.hook.postCreate "npm install" + +# Node.js (pnpm) +git config --add gtr.hook.postCreate "pnpm install" + +# Python +git config --add gtr.hook.postCreate "pip install -r requirements.txt" + +# Ruby +git config --add gtr.hook.postCreate "bundle install" + +# Rust +git config --add gtr.hook.postCreate "cargo build" +``` + +## Configuration Examples + +### Minimal Setup (Just Basics) + +```bash +git config --local gtr.worktrees.prefix "wt-" +git config --local gtr.defaultBranch "main" +``` + +### Full-Featured Setup (Node.js Project) + +```bash +# Worktree settings +git config --local gtr.worktrees.prefix "wt-" +git config --local gtr.worktrees.startId 2 + +# Editor +git config --local gtr.editor.default cursor + +# Copy environment templates +git config --local --add gtr.copy.include "**/.env.example" +git config --local --add gtr.copy.include "**/.env.development" +git config --local --add gtr.copy.exclude "**/.env.local" + +# Build hooks +git config --local --add gtr.hook.postCreate "pnpm install" +git config --local --add gtr.hook.postCreate "pnpm run build" +``` + +### Global Defaults + +```bash +# Set global preferences +git config --global gtr.editor.default cursor +git config --global gtr.ai.default aider +git config --global gtr.worktrees.startId 2 +``` + +## Advanced Usage + +### Working with Multiple Branches + +```bash +# Terminal 1: Work on feature +gtr create --branch feature-a --id 2 --open + +# Terminal 2: Review PR +gtr create --branch pr/123 --id 3 --open + +# Terminal 3: Run tests on main +gtr cd 1 # Original repo is worktree 1 ``` -## Requirements -- macOS (uses GitHub Desktop via `open -a`) -- git, pnpm, turbo -- Optional: `claude-code`, `cursor` on PATH +### Custom Workflows with Hooks -## Notes -- Default base branch is `main` (adjust inside `gtr.sh` if needed). -- Script prompts during create/remove (interactive). -- For non‑macOS, replace the GitHub Desktop `open -a` line. +Create a `.gtr-setup.sh` in your repo: -## Uninstall ```bash -sed -i.bak '/source .*\\.gtr\\.sh/d' ~/.zshrc -rm -f ~/.gtr.sh -exec zsh +#!/bin/sh +# .gtr-setup.sh - Project-specific gtr configuration + +git config --local gtr.worktrees.prefix "dev-" +git config --local gtr.editor.default cursor + +# Copy configs +git config --local --add gtr.copy.include ".env.example" +git config --local --add gtr.copy.include "docker-compose.yml" + +# Setup hooks +git config --local --add gtr.hook.postCreate "docker-compose up -d db" +git config --local --add gtr.hook.postCreate "npm install" +git config --local --add gtr.hook.postCreate "npm run db:migrate" ``` + +Then run: `sh .gtr-setup.sh` + +### Non-Interactive Automation + +Perfect for CI/CD or scripts: + +```bash +# Create worktree without prompts +gtr create --branch ci-test --id 99 --yes --no-copy + +# Remove without confirmation +gtr rm 99 --yes --delete-branch ``` + +## Troubleshooting + +### Worktree Creation Fails + +```bash +# Ensure you've fetched latest refs +git fetch origin + +# Check if branch already exists +git branch -a | grep your-branch + +# Manually specify tracking mode +gtr create --branch test --track remote +``` + +### Editor Not Opening + +```bash +# Verify editor command is available +command -v cursor # or: code, zed + +# Check configuration +gtr config get gtr.editor.default + +# Try opening manually +gtr open 2 --editor cursor +``` + +### File Copying Issues + +```bash +# Check your patterns +git config --get-all gtr.copy.include + +# Test patterns with find +cd /path/to/repo +find . -path "**/.env.example" +``` + +## Platform Support + +- ✅ **macOS** - Full support (Ventura+) +- ✅ **Linux** - Full support (Ubuntu, Fedora, Arch, etc.) +- ✅ **Windows** - Via Git Bash or WSL + +**Platform-specific notes:** +- **macOS**: GUI opening uses `open`, terminal spawning uses iTerm2/Terminal.app +- **Linux**: GUI opening uses `xdg-open`, terminal spawning uses gnome-terminal/konsole +- **Windows**: GUI opening uses `start`, requires Git Bash or WSL + +## Architecture + +``` +git-worktree-runner/ +├── bin/gtr # Main executable +├── lib/ # Core libraries +│ ├── core.sh # Git worktree operations +│ ├── config.sh # Configuration management +│ ├── platform.sh # OS-specific code +│ ├── ui.sh # User interface +│ ├── copy.sh # File copying +│ └── hooks.sh # Hook execution +├── adapters/ # Editor & AI tool plugins +│ ├── editor/ +│ └── ai/ +├── completions/ # Shell completions +└── templates/ # Example configs +``` + +## Contributing + +Contributions welcome! Areas where help is appreciated: + +- 🎨 **New editor adapters** - JetBrains IDEs, Neovim, etc. +- 🤖 **New AI tool adapters** - Continue.dev, Codeium, etc. +- 🐛 **Bug reports** - Platform-specific issues +- 📚 **Documentation** - Tutorials, examples, use cases +- ✨ **Features** - Propose enhancements via issues + +See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. + +## Related Projects + +- [git-worktree](https://git-scm.com/docs/git-worktree) - Official git documentation +- [Aider](https://aider.chat) - AI pair programming in your terminal +- [Cursor](https://cursor.com) - AI-powered code editor + +## License + +MIT License - see [LICENSE](LICENSE) for details. + +## Acknowledgments + +Built to streamline parallel development workflows with git worktrees. Inspired by the need for simple, configurable worktree management across different development environments. + +--- + +**Happy coding with worktrees! 🚀** + +For questions or issues, please [open an issue](https://github.com/anthropics/git-worktree-runner/issues). diff --git a/adapters/ai/aider.sh b/adapters/ai/aider.sh new file mode 100644 index 0000000..33592b7 --- /dev/null +++ b/adapters/ai/aider.sh @@ -0,0 +1,28 @@ +#!/bin/sh +# Aider AI coding assistant adapter + +# Check if Aider is available +ai_can_start() { + command -v aider >/dev/null 2>&1 +} + +# Start Aider in a directory +# Usage: ai_start path [args...] +ai_start() { + local path="$1" + shift + + if ! ai_can_start; then + log_error "Aider not found. Install with: pip install aider-chat" + log_info "See https://aider.chat for more information" + return 1 + fi + + if [ ! -d "$path" ]; then + log_error "Directory not found: $path" + return 1 + fi + + # Change to the directory and run aider with any additional arguments + (cd "$path" && aider "$@") +} diff --git a/adapters/ai/claudecode.sh b/adapters/ai/claudecode.sh new file mode 100644 index 0000000..3f14cc1 --- /dev/null +++ b/adapters/ai/claudecode.sh @@ -0,0 +1,27 @@ +#!/bin/sh +# Claude Code AI adapter + +# Check if Claude Code is available +ai_can_start() { + command -v claude-code >/dev/null 2>&1 +} + +# Start Claude Code in a directory +# Usage: ai_start path [args...] +ai_start() { + local path="$1" + shift + + if ! ai_can_start; then + log_error "Claude Code not found. Install from https://claude.com/claude-code" + return 1 + fi + + if [ ! -d "$path" ]; then + log_error "Directory not found: $path" + return 1 + fi + + # Change to the directory and run claude-code with any additional arguments + (cd "$path" && claude-code "$@") +} diff --git a/adapters/ai/codex.sh b/adapters/ai/codex.sh new file mode 100644 index 0000000..2176a2a --- /dev/null +++ b/adapters/ai/codex.sh @@ -0,0 +1,29 @@ +#!/bin/sh +# OpenAI Codex CLI adapter + +# Check if Codex is available +ai_can_start() { + command -v codex >/dev/null 2>&1 +} + +# Start Codex in a directory +# Usage: ai_start path [args...] +ai_start() { + local path="$1" + shift + + if ! ai_can_start; then + log_error "Codex CLI not found. Install with: npm install -g @openai/codex" + log_info "Or: brew install codex" + log_info "See https://github.com/openai/codex for more info" + return 1 + fi + + if [ ! -d "$path" ]; then + log_error "Directory not found: $path" + return 1 + fi + + # Change to the directory and run codex with any additional arguments + (cd "$path" && codex "$@") +} diff --git a/adapters/ai/continue.sh b/adapters/ai/continue.sh new file mode 100644 index 0000000..878e646 --- /dev/null +++ b/adapters/ai/continue.sh @@ -0,0 +1,28 @@ +#!/bin/sh +# Continue CLI adapter + +# Check if Continue is available +ai_can_start() { + command -v cn >/dev/null 2>&1 +} + +# Start Continue in a directory +# Usage: ai_start path [args...] +ai_start() { + local path="$1" + shift + + if ! ai_can_start; then + log_error "Continue CLI not found. Install from https://continue.dev" + log_info "See https://docs.continue.dev/cli/install for installation" + return 1 + fi + + if [ ! -d "$path" ]; then + log_error "Directory not found: $path" + return 1 + fi + + # Change to the directory and run cn with any additional arguments + (cd "$path" && cn "$@") +} diff --git a/adapters/ai/cursor.sh b/adapters/ai/cursor.sh new file mode 100644 index 0000000..e56fef6 --- /dev/null +++ b/adapters/ai/cursor.sh @@ -0,0 +1,33 @@ +#!/bin/sh +# Cursor AI agent adapter + +# Check if Cursor agent/CLI is available +ai_can_start() { + command -v cursor-agent >/dev/null 2>&1 || command -v cursor >/dev/null 2>&1 +} + +# Start Cursor agent in a directory +# Usage: ai_start path [args...] +ai_start() { + local path="$1" + shift + + if ! ai_can_start; then + log_error "Cursor not found. Install from https://cursor.com" + log_info "Make sure to enable the Cursor CLI/agent from the app" + return 1 + fi + + if [ ! -d "$path" ]; then + log_error "Directory not found: $path" + return 1 + fi + + # Try cursor-agent first, then fallback to cursor CLI commands + if command -v cursor-agent >/dev/null 2>&1; then + (cd "$path" && cursor-agent "$@") + elif command -v cursor >/dev/null 2>&1; then + # Try various Cursor CLI patterns (implementation varies by version) + (cd "$path" && cursor cli "$@") 2>/dev/null || (cd "$path" && cursor "$@") + fi +} diff --git a/adapters/editor/cursor.sh b/adapters/editor/cursor.sh new file mode 100644 index 0000000..5a1c989 --- /dev/null +++ b/adapters/editor/cursor.sh @@ -0,0 +1,20 @@ +#!/bin/sh +# Cursor editor adapter + +# Check if Cursor is available +editor_can_open() { + command -v cursor >/dev/null 2>&1 +} + +# Open a directory in Cursor +# Usage: editor_open path +editor_open() { + local path="$1" + + if ! editor_can_open; then + log_error "Cursor not found. Install from https://cursor.com or enable the shell command." + return 1 + fi + + cursor "$path" +} diff --git a/adapters/editor/vscode.sh b/adapters/editor/vscode.sh new file mode 100644 index 0000000..847e2e6 --- /dev/null +++ b/adapters/editor/vscode.sh @@ -0,0 +1,20 @@ +#!/bin/sh +# VS Code editor adapter + +# Check if VS Code is available +editor_can_open() { + command -v code >/dev/null 2>&1 +} + +# Open a directory in VS Code +# Usage: editor_open path +editor_open() { + local path="$1" + + if ! editor_can_open; then + log_error "VS Code 'code' command not found. Install from https://code.visualstudio.com" + return 1 + fi + + code "$path" +} diff --git a/adapters/editor/zed.sh b/adapters/editor/zed.sh new file mode 100644 index 0000000..91b5f52 --- /dev/null +++ b/adapters/editor/zed.sh @@ -0,0 +1,20 @@ +#!/bin/sh +# Zed editor adapter + +# Check if Zed is available +editor_can_open() { + command -v zed >/dev/null 2>&1 +} + +# Open a directory in Zed +# Usage: editor_open path +editor_open() { + local path="$1" + + if ! editor_can_open; then + log_error "Zed not found. Install from https://zed.dev" + return 1 + fi + + zed "$path" +} diff --git a/bin/gtr b/bin/gtr new file mode 100755 index 0000000..f572db3 --- /dev/null +++ b/bin/gtr @@ -0,0 +1,675 @@ +#!/bin/sh +# gtr - Git worktree runner +# Portable, cross-platform git worktree management + +set -e + +# Version +GTR_VERSION="2.0.0" + +# Find the script directory +GTR_DIR="$(cd "$(dirname "$0")/.." && pwd)" + +# Source library files +. "$GTR_DIR/lib/ui.sh" +. "$GTR_DIR/lib/config.sh" +. "$GTR_DIR/lib/platform.sh" +. "$GTR_DIR/lib/core.sh" +. "$GTR_DIR/lib/copy.sh" +. "$GTR_DIR/lib/hooks.sh" + +# Main dispatcher +main() { + local cmd="${1:-help}" + shift 2>/dev/null || true + + case "$cmd" in + create) + cmd_create "$@" + ;; + rm|remove) + cmd_remove "$@" + ;; + cd) + cmd_cd "$@" + ;; + open) + cmd_open "$@" + ;; + ai) + cmd_ai "$@" + ;; + list|ls) + cmd_list "$@" + ;; + ids) + cmd_ids "$@" + ;; + config) + cmd_config "$@" + ;; + version|--version|-v) + echo "gtr version $GTR_VERSION" + ;; + help|--help|-h) + cmd_help + ;; + *) + log_error "Unknown command: $cmd" + echo "Use 'gtr help' for available commands" + exit 1 + ;; + esac +} + +# Create command +cmd_create() { + local worktree_id="" + local branch_name="" + local from_ref="" + local track_mode="auto" + local editor="" + local ai_tool="" + local auto_id=0 + local skip_copy=0 + local yes_mode=0 + + # Parse flags and arguments + while [ $# -gt 0 ]; do + case "$1" in + --branch) + branch_name="$2" + shift 2 + ;; + --id) + worktree_id="$2" + shift 2 + ;; + --auto) + auto_id=1 + shift + ;; + --from) + from_ref="$2" + shift 2 + ;; + --track) + track_mode="$2" + shift 2 + ;; + --open) + case "${2-}" in + --*|"") + editor="$(cfg_default gtr.editor.default GTR_EDITOR_DEFAULT none)" + shift 1 + ;; + *) + editor="$2" + shift 2 + ;; + esac + ;; + --ai) + case "${2-}" in + --*|"") + ai_tool="$(cfg_default gtr.ai.default GTR_AI_DEFAULT none)" + shift 1 + ;; + *) + ai_tool="$2" + shift 2 + ;; + esac + ;; + --no-copy) + skip_copy=1 + shift + ;; + --yes|-y) + yes_mode=1 + shift + ;; + -*) + log_error "Unknown flag: $1" + exit 1 + ;; + *) + # Positional arguments + if [ -z "$worktree_id" ] && [ -z "$branch_name" ]; then + if echo "$1" | grep -qE '^[0-9]+$'; then + worktree_id="$1" + else + branch_name="$1" + fi + elif [ -z "$branch_name" ]; then + branch_name="$1" + fi + shift + ;; + esac + done + + # Get repo info + local repo_root + repo_root=$(discover_repo_root) || exit 1 + + local base_dir prefix start_id + base_dir=$(resolve_base_dir "$repo_root") + prefix=$(cfg_default gtr.worktrees.prefix GTR_WORKTREES_PREFIX "wt-") + start_id=$(cfg_default gtr.worktrees.startId GTR_WORKTREES_STARTID "2") + + # Auto-assign ID if needed + if [ -z "$worktree_id" ] || [ "$auto_id" -eq 1 ]; then + worktree_id=$(next_available_id "$base_dir" "$prefix" "$start_id") + fi + + # Get branch name if not provided + if [ -z "$branch_name" ]; then + if [ "$yes_mode" -eq 1 ]; then + log_error "Branch name required in non-interactive mode" + exit 1 + fi + branch_name=$(prompt_input "Enter branch name for worktree ${prefix}${worktree_id}:") + if [ -z "$branch_name" ]; then + log_error "Branch name required" + exit 1 + fi + fi + + # Determine from_ref + if [ -z "$from_ref" ]; then + from_ref=$(resolve_default_branch "$repo_root") + fi + + local worktree_path + worktree_path="$base_dir/${prefix}${worktree_id}" + + log_step "Creating worktree: ${prefix}${worktree_id}" + echo "📂 Location: $worktree_path" + echo "🌿 Branch: $branch_name" + + # Create the worktree + if ! create_worktree "$base_dir" "$prefix" "$worktree_id" "$branch_name" "$from_ref" "$track_mode"; then + exit 1 + fi + + # Copy files based on patterns + if [ "$skip_copy" -eq 0 ]; then + local includes excludes + includes=$(cfg_get_all gtr.copy.include) + excludes=$(cfg_get_all gtr.copy.exclude) + + if [ -n "$includes" ]; then + log_step "Copying files..." + copy_patterns "$repo_root" "$worktree_path" "$includes" "$excludes" + fi + fi + + # Run post-create hooks + run_hooks_in postCreate "$worktree_path" \ + REPO_ROOT="$repo_root" \ + WORKTREE_PATH="$worktree_path" \ + BRANCH="$branch_name" + + # Open in editor if requested + if [ -n "$editor" ] && [ "$editor" != "none" ]; then + load_editor_adapter "$editor" + if editor_can_open; then + log_step "Opening in $editor..." + editor_open "$worktree_path" + fi + fi + + # Start AI tool if requested + if [ -n "$ai_tool" ] && [ "$ai_tool" != "none" ]; then + load_ai_adapter "$ai_tool" + if ai_can_start; then + log_step "Starting $ai_tool..." + ai_start "$worktree_path" + fi + fi + + echo "" + log_info "Worktree created successfully!" + echo "🎯 Navigate with: cd $worktree_path" + echo "📂 Open with: gtr open $worktree_id" + echo "🤖 Start AI with: gtr ai $worktree_id" +} + +# Remove command +cmd_remove() { + local delete_branch=0 + local yes_mode=0 + local worktree_ids="" + + # Parse flags + while [ $# -gt 0 ]; do + case "$1" in + --delete-branch) + delete_branch=1 + shift + ;; + --yes|-y) + yes_mode=1 + shift + ;; + -*) + log_error "Unknown flag: $1" + exit 1 + ;; + *) + worktree_ids="$worktree_ids $1" + shift + ;; + esac + done + + if [ -z "$worktree_ids" ]; then + log_error "Usage: gtr rm [...] [--delete-branch] [--yes]" + exit 1 + fi + + local repo_root base_dir prefix + repo_root=$(discover_repo_root) || exit 1 + base_dir=$(resolve_base_dir "$repo_root") + prefix=$(cfg_default gtr.worktrees.prefix GTR_WORKTREES_PREFIX "wt-") + + for worktree_id in $worktree_ids; do + local worktree_path="$base_dir/${prefix}${worktree_id}" + + log_step "Removing worktree: ${prefix}${worktree_id}" + + if [ ! -d "$worktree_path" ]; then + log_warn "Worktree not found: ${prefix}${worktree_id}" + continue + fi + + # Get branch name before removal + local branch + branch=$(current_branch "$worktree_path") + + # Remove the worktree + if ! remove_worktree "$worktree_path"; then + continue + fi + + # Handle branch deletion + if [ -n "$branch" ]; then + if [ "$delete_branch" -eq 1 ]; then + if [ "$yes_mode" -eq 1 ] || prompt_yes_no "Also delete branch '$branch'?"; then + if git branch -D "$branch" 2>/dev/null; then + log_info "Branch deleted: $branch" + else + log_warn "Could not delete branch: $branch" + fi + fi + fi + fi + + # Run post-remove hooks + run_hooks postRemove \ + REPO_ROOT="$repo_root" \ + WORKTREE_PATH="$worktree_path" \ + BRANCH="$branch" + done +} + +# CD command (prints path for shell integration) +cmd_cd() { + if [ $# -ne 1 ]; then + log_error "Usage: gtr cd " + exit 1 + fi + + local worktree_id="$1" + local repo_root base_dir prefix + repo_root=$(discover_repo_root) || exit 1 + base_dir=$(resolve_base_dir "$repo_root") + prefix=$(cfg_default gtr.worktrees.prefix GTR_WORKTREES_PREFIX "wt-") + + local worktree_path="$base_dir/${prefix}${worktree_id}" + + if [ ! -d "$worktree_path" ]; then + log_error "Worktree not found: ${prefix}${worktree_id}" + exit 1 + fi + + local branch + branch=$(current_branch "$worktree_path") + + # Human messages to stderr so stdout can be used in command substitution + echo "📂 Switched to worktree: ${prefix}${worktree_id}" >&2 + echo "🌿 Current branch: $branch" >&2 + + # Print path to stdout for shell integration: cd "$(gtr cd 2)" + printf "%s\n" "$worktree_path" +} + +# Open command +cmd_open() { + local editor="" + local worktree_id="" + + # Parse arguments + while [ $# -gt 0 ]; do + case "$1" in + --editor) + editor="$2" + shift 2 + ;; + -*) + log_error "Unknown flag: $1" + exit 1 + ;; + *) + worktree_id="$1" + shift + ;; + esac + done + + if [ -z "$worktree_id" ]; then + log_error "Usage: gtr open [--editor ]" + exit 1 + fi + + # Get editor from config if not specified + if [ -z "$editor" ]; then + editor=$(cfg_default gtr.editor.default GTR_EDITOR_DEFAULT "none") + fi + + if [ "$editor" = "none" ]; then + # Just open in GUI file browser + local repo_root base_dir prefix + repo_root=$(discover_repo_root) || exit 1 + base_dir=$(resolve_base_dir "$repo_root") + prefix=$(cfg_default gtr.worktrees.prefix GTR_WORKTREES_PREFIX "wt-") + local worktree_path="$base_dir/${prefix}${worktree_id}" + + if [ ! -d "$worktree_path" ]; then + log_error "Worktree not found: ${prefix}${worktree_id}" + exit 1 + fi + + open_in_gui "$worktree_path" + log_info "Opened in file browser" + else + # Load editor adapter and open + load_editor_adapter "$editor" + + local repo_root base_dir prefix + repo_root=$(discover_repo_root) || exit 1 + base_dir=$(resolve_base_dir "$repo_root") + prefix=$(cfg_default gtr.worktrees.prefix GTR_WORKTREES_PREFIX "wt-") + local worktree_path="$base_dir/${prefix}${worktree_id}" + + if [ ! -d "$worktree_path" ]; then + log_error "Worktree not found: ${prefix}${worktree_id}" + exit 1 + fi + + log_step "Opening in $editor..." + editor_open "$worktree_path" + fi +} + +# AI command +cmd_ai() { + local ai_tool="" + local worktree_id="" + local ai_args="" + + # Parse arguments + while [ $# -gt 0 ]; do + case "$1" in + --tool) + ai_tool="$2" + shift 2 + ;; + --) + shift + ai_args="$*" + break + ;; + -*) + log_error "Unknown flag: $1" + exit 1 + ;; + *) + if [ -z "$worktree_id" ]; then + worktree_id="$1" + fi + shift + ;; + esac + done + + if [ -z "$worktree_id" ]; then + log_error "Usage: gtr ai [--tool ] [-- args...]" + exit 1 + fi + + # Get AI tool from config if not specified + if [ -z "$ai_tool" ]; then + ai_tool=$(cfg_default gtr.ai.default GTR_AI_DEFAULT "aider") + fi + + # Load AI adapter + load_ai_adapter "$ai_tool" + + local repo_root base_dir prefix + repo_root=$(discover_repo_root) || exit 1 + base_dir=$(resolve_base_dir "$repo_root") + prefix=$(cfg_default gtr.worktrees.prefix GTR_WORKTREES_PREFIX "wt-") + local worktree_path="$base_dir/${prefix}${worktree_id}" + + if [ ! -d "$worktree_path" ]; then + log_error "Worktree not found: ${prefix}${worktree_id}" + exit 1 + fi + + local branch + branch=$(current_branch "$worktree_path") + + log_step "Starting $ai_tool in worktree: ${prefix}${worktree_id}" + echo "📂 Directory: $worktree_path" + echo "🌿 Branch: $branch" + + # shellcheck disable=SC2086 + ai_start "$worktree_path" $ai_args +} + +# List command +cmd_list() { + echo "📋 Git worktrees:" + list_worktrees + + local repo_root base_dir + repo_root=$(discover_repo_root) || exit 1 + base_dir=$(resolve_base_dir "$repo_root") + + echo "" + echo "📁 Worktree directories:" + if [ -d "$base_dir" ]; then + ls -la "$base_dir" 2>/dev/null | grep "^d" | grep -v "^d.*\\.$" || echo " (none found)" + else + echo " (worktrees directory doesn't exist yet)" + fi +} + +# IDs command (machine-friendly output for completions) +cmd_ids() { + local repo_root base_dir prefix + repo_root=$(discover_repo_root) 2>/dev/null || return 0 + base_dir=$(resolve_base_dir "$repo_root") + prefix=$(cfg_default gtr.worktrees.prefix GTR_WORKTREES_PREFIX "wt-") + + if [ ! -d "$base_dir" ]; then + return 0 + fi + + # Extract IDs from worktree directories + for dir in "$base_dir/${prefix}"*; do + if [ -d "$dir" ]; then + local id + id=$(basename "$dir" | sed "s/^${prefix}//") + echo "$id" + fi + done +} + +# Config command +cmd_config() { + local action="${1:-get}" + local key="$2" + local value="$3" + local scope="local" + + # Check for --global flag + if [ "$key" = "--global" ] || [ "$value" = "--global" ]; then + scope="global" + [ "$key" = "--global" ] && key="$3" && value="$4" + [ "$value" = "--global" ] && value="$4" + fi + + case "$action" in + get) + if [ -z "$key" ]; then + log_error "Usage: gtr config get [--global]" + exit 1 + fi + cfg_get "$key" "$scope" + ;; + set) + if [ -z "$key" ] || [ -z "$value" ]; then + log_error "Usage: gtr config set [--global]" + exit 1 + fi + cfg_set "$key" "$value" "$scope" + log_info "Config set: $key = $value" + ;; + unset) + if [ -z "$key" ]; then + log_error "Usage: gtr config unset [--global]" + exit 1 + fi + cfg_unset "$key" "$scope" + log_info "Config unset: $key" + ;; + *) + log_error "Unknown config action: $action" + log_error "Usage: gtr config {get|set|unset} [value] [--global]" + exit 1 + ;; + esac +} + +# Load editor adapter +load_editor_adapter() { + local editor="$1" + local adapter_file="$GTR_DIR/adapters/editor/${editor}.sh" + + if [ ! -f "$adapter_file" ]; then + log_error "Unknown editor: $editor" + log_info "Available editors: cursor, vscode, zed" + exit 1 + fi + + . "$adapter_file" +} + +# Load AI adapter +load_ai_adapter() { + local ai_tool="$1" + local adapter_file="$GTR_DIR/adapters/ai/${ai_tool}.sh" + + if [ ! -f "$adapter_file" ]; then + log_error "Unknown AI tool: $ai_tool" + log_info "Available AI tools: aider, claudecode, codex, cursor, continue" + exit 1 + fi + + . "$adapter_file" +} + +# Help command +cmd_help() { + cat <<'EOF' +gtr - Git worktree runner + +USAGE: + gtr [options] + +COMMANDS: + create [--branch ] [--id |--auto] [--from ] + [--track auto|remote|local|none] [--open []] + [--ai []] [--no-copy] [--yes] + Create a new worktree + + rm [--delete-branch] [--yes] + Remove worktree(s) + + cd + Change to worktree directory + + open [--editor ] + Open worktree in editor or file browser + + ai [--tool ] [-- args...] + Start AI coding tool in worktree + + list + List all worktrees + + config {get|set|unset} [value] [--global] + Manage configuration + + version + Show version + + help + Show this help + +EXAMPLES: + # Create worktree with auto ID and branch name + gtr create --branch my-feature --auto + + # Create worktree with specific ID + gtr create --id 3 --branch ui-fixes + + # Create and open in Cursor + gtr create --branch fix --auto --open cursor + + # Open existing worktree in VS Code + gtr open 2 --editor vscode + + # Start Aider in worktree + gtr ai 2 --tool aider + + # Configure default editor + gtr config set gtr.editor.default cursor --global + + # Enable file copying + gtr config set gtr.copy.include "**/.env.example" + + # Add post-create hook + gtr config set gtr.hook.postCreate "npm install" + +CONFIGURATION: + gtr.worktrees.dir Worktrees base directory + gtr.worktrees.prefix Worktree name prefix (default: wt-) + gtr.worktrees.startId Starting ID (default: 2) + gtr.defaultBranch Default branch (default: auto) + gtr.editor.default Default editor (cursor, vscode, zed, none) + gtr.ai.default Default AI tool (aider, claudecode, codex, cursor, continue, none) + gtr.copy.include Files to copy (multi-valued) + gtr.copy.exclude Files to exclude (multi-valued) + gtr.hook.postCreate Post-create hooks (multi-valued) + gtr.hook.postRemove Post-remove hooks (multi-valued) + +See https://github.com/anthropics/git-worktree-runner for more info. +EOF +} + +# Run main +main "$@" diff --git a/completions/_gtr b/completions/_gtr new file mode 100644 index 0000000..f4f2c2a --- /dev/null +++ b/completions/_gtr @@ -0,0 +1,77 @@ +#compdef gtr +# Zsh completion for gtr + +_gtr() { + local -a commands + commands=( + 'create:Create a new worktree' + 'rm:Remove worktree(s)' + 'remove:Remove worktree(s)' + 'cd:Change to worktree directory' + 'open:Open worktree in editor or file browser' + 'ai:Start AI coding tool in worktree' + 'list:List all worktrees' + 'config:Manage configuration' + 'version:Show version' + 'help:Show help' + ) + + local -a worktree_ids + # Use gtr ids command for config-aware completion + worktree_ids=(${(f)"$(command gtr ids 2>/dev/null)"}) + + if (( CURRENT == 2 )); then + _describe 'commands' commands + elif (( CURRENT == 3 )); then + case "$words[2]" in + cd|open|ai|rm|remove) + _describe 'worktree IDs' worktree_ids + ;; + open) + _arguments \ + '--editor[Editor]:editor:(cursor vscode zed)' + ;; + ai) + _arguments \ + '--tool[AI tool]:tool:(aider claudecode codex cursor continue)' + ;; + create) + _arguments \ + '--branch[Branch name]:branch:' \ + '--id[Worktree ID]:id:' \ + '--auto[Auto-assign ID]' \ + '--from[Base ref]:ref:' \ + '--track[Track mode]:mode:(auto remote local none)' \ + '--open[Editor]:editor:(cursor vscode zed)' \ + '--ai[AI tool]:tool:(aider claudecode codex cursor continue)' \ + '--no-copy[Skip file copying]' \ + '--yes[Non-interactive mode]' + ;; + config) + _values 'config action' get set unset + ;; + esac + elif (( CURRENT == 4 )); then + case "$words[2]" in + config) + case "$words[3]" in + get|set|unset) + _values 'config key' \ + 'gtr.worktrees.dir' \ + 'gtr.worktrees.prefix' \ + 'gtr.worktrees.startId' \ + 'gtr.defaultBranch' \ + 'gtr.editor.default' \ + 'gtr.ai.default' \ + 'gtr.copy.include' \ + 'gtr.copy.exclude' \ + 'gtr.hook.postCreate' \ + 'gtr.hook.postRemove' + ;; + esac + ;; + esac + fi +} + +_gtr "$@" diff --git a/completions/gtr.bash b/completions/gtr.bash new file mode 100644 index 0000000..bdca9c3 --- /dev/null +++ b/completions/gtr.bash @@ -0,0 +1,56 @@ +#!/bin/bash +# Bash completion for gtr + +_gtr_completion() { + local cur prev words cword + _init_completion || return + + local cmd="${words[1]}" + + # Complete commands on first argument + if [ "$cword" -eq 1 ]; then + COMPREPLY=($(compgen -W "create rm remove cd open ai list config help version" -- "$cur")) + return 0 + fi + + # Commands that take worktree IDs + case "$cmd" in + cd|open|ai|rm|remove) + if [ "$cword" -eq 2 ]; then + # Use gtr ids command for config-aware completion + local ids + ids=$(command gtr ids 2>/dev/null || true) + COMPREPLY=($(compgen -W "$ids" -- "$cur")) + fi + ;; + create) + # Complete flags + if [[ "$cur" == -* ]]; then + COMPREPLY=($(compgen -W "--branch --id --auto --from --track --open --ai --no-copy --yes" -- "$cur")) + fi + ;; + open) + if [[ "$cur" == -* ]]; then + COMPREPLY=($(compgen -W "--editor" -- "$cur")) + elif [ "$prev" = "--editor" ]; then + COMPREPLY=($(compgen -W "cursor vscode zed" -- "$cur")) + fi + ;; + ai) + if [[ "$cur" == -* ]]; then + COMPREPLY=($(compgen -W "--tool" -- "$cur")) + elif [ "$prev" = "--tool" ]; then + COMPREPLY=($(compgen -W "aider claudecode codex cursor continue" -- "$cur")) + fi + ;; + config) + if [ "$cword" -eq 2 ]; then + COMPREPLY=($(compgen -W "get set unset" -- "$cur")) + elif [ "$cword" -eq 3 ]; then + COMPREPLY=($(compgen -W "gtr.worktrees.dir gtr.worktrees.prefix gtr.worktrees.startId gtr.defaultBranch gtr.editor.default gtr.ai.default gtr.copy.include gtr.copy.exclude gtr.hook.postCreate gtr.hook.postRemove" -- "$cur")) + fi + ;; + esac +} + +complete -F _gtr_completion gtr diff --git a/completions/gtr.fish b/completions/gtr.fish new file mode 100644 index 0000000..82327f8 --- /dev/null +++ b/completions/gtr.fish @@ -0,0 +1,58 @@ +# Fish completion for gtr + +# Commands +complete -c gtr -f -n "__fish_use_subcommand" -a "create" -d "Create a new worktree" +complete -c gtr -f -n "__fish_use_subcommand" -a "rm" -d "Remove worktree(s)" +complete -c gtr -f -n "__fish_use_subcommand" -a "remove" -d "Remove worktree(s)" +complete -c gtr -f -n "__fish_use_subcommand" -a "cd" -d "Change to worktree directory" +complete -c gtr -f -n "__fish_use_subcommand" -a "open" -d "Open worktree in editor" +complete -c gtr -f -n "__fish_use_subcommand" -a "ai" -d "Start AI coding tool" +complete -c gtr -f -n "__fish_use_subcommand" -a "list" -d "List all worktrees" +complete -c gtr -f -n "__fish_use_subcommand" -a "config" -d "Manage configuration" +complete -c gtr -f -n "__fish_use_subcommand" -a "version" -d "Show version" +complete -c gtr -f -n "__fish_use_subcommand" -a "help" -d "Show help" + +# Create command options +complete -c gtr -n "__fish_seen_subcommand_from create" -l branch -d "Branch name" -r +complete -c gtr -n "__fish_seen_subcommand_from create" -l id -d "Worktree ID" -r +complete -c gtr -n "__fish_seen_subcommand_from create" -l auto -d "Auto-assign ID" +complete -c gtr -n "__fish_seen_subcommand_from create" -l from -d "Base ref" -r +complete -c gtr -n "__fish_seen_subcommand_from create" -l track -d "Track mode" -r -a "auto remote local none" +complete -c gtr -n "__fish_seen_subcommand_from create" -l open -d "Open in editor" -r -a "cursor vscode zed" +complete -c gtr -n "__fish_seen_subcommand_from create" -l ai -d "AI tool" -r -a "aider claudecode codex cursor continue" +complete -c gtr -n "__fish_seen_subcommand_from create" -l no-copy -d "Skip file copying" +complete -c gtr -n "__fish_seen_subcommand_from create" -l yes -d "Non-interactive mode" + +# Remove command options +complete -c gtr -n "__fish_seen_subcommand_from rm remove" -l delete-branch -d "Delete branch" +complete -c gtr -n "__fish_seen_subcommand_from rm remove" -l yes -d "Non-interactive mode" + +# Open command options +complete -c gtr -n "__fish_seen_subcommand_from open" -l editor -d "Editor name" -r -a "cursor vscode zed" + +# AI command options +complete -c gtr -n "__fish_seen_subcommand_from ai" -l tool -d "AI tool name" -r -a "aider claudecode codex cursor continue" + +# Config command +complete -c gtr -n "__fish_seen_subcommand_from config" -f -a "get set unset" +complete -c gtr -n "__fish_seen_subcommand_from config; and __fish_seen_subcommand_from get set unset" -f -a "\ + gtr.worktrees.dir\t'Worktrees base directory' + gtr.worktrees.prefix\t'Worktree name prefix' + gtr.worktrees.startId\t'Starting ID' + gtr.defaultBranch\t'Default branch' + gtr.editor.default\t'Default editor' + gtr.ai.default\t'Default AI tool' + gtr.copy.include\t'Files to copy' + gtr.copy.exclude\t'Files to exclude' + gtr.hook.postCreate\t'Post-create hook' + gtr.hook.postRemove\t'Post-remove hook' +" + +# Helper function to get worktree IDs +function __gtr_worktree_ids + # Use gtr ids command for config-aware completion + command gtr ids 2>/dev/null +end + +# Complete worktree IDs for commands that need them +complete -c gtr -n "__fish_seen_subcommand_from cd open ai rm remove" -f -a "(__gtr_worktree_ids)" diff --git a/gtr.sh b/gtr.sh deleted file mode 100644 index b1342e7..0000000 --- a/gtr.sh +++ /dev/null @@ -1,460 +0,0 @@ -#!/bin/bash -# gtr - Git worktree helper for parallel Claude Code development -# Enhanced for CodeRabbit monorepo workflow - -gtr () { - local cmd="$1"; shift || { echo "Usage: gtr {create|rm|cd|claude|cursor|desktop|list} "; return 1; } - - # Base folder for worktrees (relative to current git repo) - local repo_root=$(git rev-parse --show-toplevel 2>/dev/null) - if [ -z "$repo_root" ]; then - echo "❌ Not in a git repository" - return 1 - fi - - local repo_name=$(basename "$repo_root") - local base="$(dirname "$repo_root")/${repo_name}-worktrees" - - case "$cmd" in - create) - local worktree_id="" - local branch_name="" - - # If no arguments, auto-assign ID starting from 2 - if [ $# -eq 0 ]; then - # Find the next available worktree id starting from 2 - local id=2 - while [ -d "$base/mono-$id" ]; do - ((id++)) - done - worktree_id="$id" - elif [ $# -eq 1 ]; then - # One argument - could be branch name (auto-assign ID) or worktree ID - if [ -d "$base/mono-$1" ]; then - echo "❌ Worktree mono-$1 already exists" - return 1 - elif [[ "$1" =~ ^[0-9]+$ ]] || [[ "$1" =~ ^(secondary|third|aux)$ ]]; then - # Looks like a worktree ID - worktree_id="$1" - else - # Assume it's a branch name, auto-assign ID - local id=2 - while [ -d "$base/mono-$id" ]; do - ((id++)) - done - worktree_id="$id" - branch_name="$1" - fi - else - # Two arguments: worktree ID and branch name - worktree_id="$1" - branch_name="$2" - fi - - local worktree_path="$base/mono-$worktree_id" - - # If no branch name provided, prompt for it - if [ -z "$branch_name" ]; then - printf "❓ Enter branch name for worktree mono-$worktree_id: " - read -r branch_name - if [ -z "$branch_name" ]; then - echo "❌ Branch name required" - return 1 - fi - fi - - echo "🚀 Creating worktree: mono-$worktree_id" - echo "📂 Location: $worktree_path" - echo "🌿 Branch: $branch_name" - - # Check if worktree already exists - if [ -d "$worktree_path" ]; then - echo "❌ Worktree mono-$worktree_id already exists at $worktree_path" - echo "💡 Use GitHub Desktop to switch branches in existing worktree" - return 1 - fi - - # First fetch to ensure we have latest remote refs - echo "🔄 Fetching remote branches..." - git fetch origin 2>/dev/null - - # Check if branch exists on remote - if git show-ref --verify --quiet "refs/remotes/origin/$branch_name"; then - echo "🌿 Branch '$branch_name' exists on remote" - - # Check if local branch also exists - if git show-ref --verify --quiet "refs/heads/$branch_name"; then - echo "⚠️ Local branch '$branch_name' also exists" - printf "❓ Use remote branch 'origin/$branch_name'? [Y/n] " - else - printf "❓ Create worktree from remote branch 'origin/$branch_name'? [Y/n] " - fi - - read -r reply - case "$reply" in - [nN]|[nN][oO]) - echo "❌ Aborted" - return 1 - ;; - *) - echo "🌿 Creating worktree from remote branch" - # Create worktree with a new local branch tracking the remote - if git worktree add "$worktree_path" -b "$branch_name" "origin/$branch_name" 2>/dev/null || \ - git worktree add "$worktree_path" "$branch_name" 2>/dev/null; then - echo "✅ Worktree created tracking origin/$branch_name" - else - echo "❌ Failed to create worktree" - return 1 - fi - ;; - esac - # Check if branch exists locally - elif git show-ref --verify --quiet "refs/heads/$branch_name"; then - echo "⚠️ Branch '$branch_name' exists locally only" - printf "❓ Use existing local branch '$branch_name'? [y/N] " - read -r reply - case "$reply" in - [yY]|[yY][eE][sS]) - echo "🌿 Using existing local branch: $branch_name" - - if git worktree add "$worktree_path" "$branch_name"; then - echo "✅ Worktree created with existing local branch" - else - echo "❌ Failed to create worktree with existing branch" - return 1 - fi - ;; - *) - echo "❌ Aborted" - return 1 - ;; - esac - else - echo "🌿 Creating new branch from main" - - # Create new branch from main - if git worktree add "$worktree_path" -b "$branch_name" main; then - echo "✅ Worktree created successfully" - else - echo "❌ Failed to create worktree" - return 1 - fi - fi - - # Open GitHub Desktop automatically - echo "" - echo "🖥️ Opening GitHub Desktop for worktree: mono-$worktree_id" - open -a "GitHub Desktop" "$worktree_path" - - # CodeRabbit-specific setup - cd "$worktree_path" - - # Copy environment files from all services - echo "📋 Copying .env.local files..." - cd "$repo_root" - find . -name ".env.local" -type f | while read -r env_file; do - # Get the directory path relative to repo root - rel_dir=$(dirname "$env_file") - # Create the directory in worktree if needed - mkdir -p "$worktree_path/$rel_dir" - # Copy the .env.local file - cp "$env_file" "$worktree_path/$env_file" - echo " ✅ Copied $env_file" - done - - # Copy all CLAUDE.md files preserving directory structure - echo "📋 Copying CLAUDE.md files..." - cd "$repo_root" - find . -name "CLAUDE.md" -type f | while read -r claude_file; do - # Get the directory path relative to repo root - rel_dir=$(dirname "$claude_file") - # Create the directory in worktree if needed - if [ "$rel_dir" != "." ]; then - mkdir -p "$worktree_path/$rel_dir" - fi - # Copy the CLAUDE.md file - cp "$claude_file" "$worktree_path/$claude_file" - echo " ✅ Copied $claude_file" - done - - # Copy run_services.sh script - if [ -f "$repo_root/run_services.sh" ]; then - cp "$repo_root/run_services.sh" "$worktree_path/" && echo "📋 Copied run_services.sh" - chmod +x "$worktree_path/run_services.sh" - fi - cd "$worktree_path" - - # Install dependencies if package.json exists - if [ -f "package.json" ]; then - echo "📦 Installing dependencies with pnpm..." - pnpm install - echo "✅ Dependencies installed" - - # Build the project - echo "🔨 Building the project with turbo..." - if turbo build; then - echo "✅ Build completed successfully" - else - echo "⚠️ Build completed with warnings/errors" - fi - fi - - echo "🎯 Navigate with: cd $worktree_path" - echo "🤖 Start Claude with: gtr claude $worktree_id" - echo "🖥️ Open GitHub Desktop: gtr desktop $worktree_id" - echo "💡 Switch branches later using GitHub Desktop" - cd "$repo_root" - ;; - - rm|remove) - if [ $# -eq 0 ]; then - echo "Usage: gtr rm " - echo "Example: gtr rm 2 # Removes mono-2" - return 1 - fi - - for worktree_id in "$@"; do - local worktree_path="$base/mono-$worktree_id" - - echo "🗑️ Removing worktree: mono-$worktree_id" - - if [ ! -d "$worktree_path" ]; then - echo "⚠️ Worktree mono-$worktree_id not found at $worktree_path" - continue - fi - - # Get the current branch from the worktree - local current_branch=$(cd "$worktree_path" 2>/dev/null && git branch --show-current) - - if git worktree remove "$worktree_path" 2>/dev/null; then - echo "✅ Worktree removed: $worktree_path" - - # Ask if they want to delete the branch too - if [ -n "$current_branch" ]; then - printf "❓ Also delete branch '$current_branch'? [y/N] " - read -r reply - case "$reply" in - [yY]|[yY][eE][sS]) - if git branch -D "$current_branch" 2>/dev/null; then - echo "✅ Branch deleted: $current_branch" - fi - ;; - *) - echo "ℹ️ Branch '$current_branch' kept" - ;; - esac - fi - else - echo "❌ Failed to remove worktree" - fi - echo "" - done - ;; - - cd) - if [ $# -ne 1 ]; then - echo "Usage: gtr cd " - echo "Example: gtr cd 2 # cd to mono-2" - return 1 - fi - - local worktree_id="$1" - local worktree_path="$base/mono-$worktree_id" - if [ -d "$worktree_path" ]; then - cd "$worktree_path" - local current_branch=$(git branch --show-current) - echo "📂 Switched to worktree: mono-$worktree_id" - echo "🌿 Current branch: $current_branch" - else - echo "❌ Worktree not found: mono-$worktree_id" - return 1 - fi - ;; - - claude) - if [ $# -ne 1 ]; then - echo "Usage: gtr claude " - echo "Example: gtr claude 2 # Start Claude in mono-2" - return 1 - fi - - local worktree_id="$1" - local worktree_path="$base/mono-$worktree_id" - - if [ ! -d "$worktree_path" ]; then - echo "❌ Worktree mono-$worktree_id not found" - echo "💡 Create it first with: gtr create $worktree_id [branch-name]" - return 1 - fi - - local current_branch=$(cd "$worktree_path" && git branch --show-current) - echo "🤖 Starting Claude Code in worktree: mono-$worktree_id" - echo "📂 Directory: $worktree_path" - echo "🌿 Current branch: $current_branch" - - ( cd "$worktree_path" && claude-code ) - ;; - - cursor) - if [ $# -ne 1 ]; then - echo "Usage: gtr cursor " - echo "Example: gtr cursor 2 # Open Cursor in mono-2" - return 1 - fi - - local worktree_id="$1" - local worktree_path="$base/mono-$worktree_id" - - if [ ! -d "$worktree_path" ]; then - echo "❌ Worktree mono-$worktree_id not found" - echo "💡 Create it first with: gtr create $worktree_id [branch-name]" - return 1 - fi - - local current_branch=$(cd "$worktree_path" && git branch --show-current) - echo "🪟 Opening Cursor in worktree: mono-$worktree_id" - echo "📂 Directory: $worktree_path" - echo "🌿 Current branch: $current_branch" - - cursor "$worktree_path" - ;; - - desktop|github) - if [ $# -ne 1 ]; then - echo "Usage: gtr desktop " - echo "Example: gtr desktop 2 # Open GitHub Desktop for mono-2" - return 1 - fi - - local worktree_id="$1" - local worktree_path="$base/mono-$worktree_id" - - if [ ! -d "$worktree_path" ]; then - echo "❌ Worktree mono-$worktree_id not found" - echo "💡 Create it first with: gtr create $worktree_id [branch-name]" - return 1 - fi - - local current_branch=$(cd "$worktree_path" && git branch --show-current) - echo "🖥️ Opening GitHub Desktop for worktree: mono-$worktree_id" - echo "📂 Directory: $worktree_path" - echo "🌿 Current branch: $current_branch" - - open -a "GitHub Desktop" "$worktree_path" - ;; - - list|ls) - echo "📋 Git worktrees:" - git worktree list - - # List worktrees in the worktrees directory - echo "" - echo "📁 Worktree directories:" - if [ -d "$base" ]; then - ls -la "$base" 2>/dev/null | grep "^d.*mono-" || echo " (none found)" - else - echo " (worktrees directory doesn't exist yet)" - fi - ;; - - help|--help|-h) - echo "gtr - Git worktree helper for parallel Claude Code development" - echo "" - echo "Commands:" - echo " create [branch] Auto-create next worktree (mono-2, mono-3, etc)" - echo " create [branch] Create specific worktree (mono-)" - echo " rm Remove worktree" - echo " cd Change to worktree directory" - echo " claude Start Claude Code in worktree" - echo " cursor Open Cursor editor in worktree" - echo " desktop Open GitHub Desktop for worktree" - echo " list List all worktrees" - echo " help Show this help" - echo "" - echo "Examples:" - echo " gtr create # Auto-creates mono-2 with new branch" - echo " gtr create tommy-mcp # Auto-creates mono-2 with branch tommy-mcp" - echo " gtr create 3 ui-fixes # Creates mono-3 with branch ui-fixes" - echo " gtr claude 2 # Start Claude in mono-2" - echo " gtr cursor 2 # Open Cursor in mono-2" - echo " gtr desktop 2 # Open GitHub Desktop for mono-2" - echo " cd ~/Documents/GitHub/mono-worktrees/mono-2" - echo "" - echo "Navigation shortcuts:" - echo " cdw # Jump to worktrees directory" - echo " cdm # Jump to main repo" - ;; - - *) - echo "❌ Unknown command: $cmd" - echo "Use 'gtr help' for available commands" - return 1 - ;; - esac -} - -# Auto-completion for gtr -if [ -n "$BASH_VERSION" ]; then - _gtr_completion() { - local cur="${COMP_WORDS[COMP_CWORD]}" - local cmd="${COMP_WORDS[1]}" - - case "$cmd" in - cd|claude|cursor|desktop|github|rm|remove) - local repo_root=$(git rev-parse --show-toplevel 2>/dev/null) - if [ -n "$repo_root" ]; then - local repo_name=$(basename "$repo_root") - local base="$(dirname "$repo_root")/${repo_name}-worktrees" - if [ -d "$base" ]; then - local worktrees=$(ls "$base" 2>/dev/null) - COMPREPLY=($(compgen -W "$worktrees" -- "$cur")) - fi - fi - ;; - *) - COMPREPLY=($(compgen -W "create rm cd claude cursor desktop list help" -- "$cur")) - ;; - esac - } - - complete -F _gtr_completion gtr -fi - -# Zsh completion -if [ -n "$ZSH_VERSION" ]; then - _gtr() { - local -a commands - commands=( - 'create:Create worktree from main branch' - 'rm:Remove worktree and branch' - 'cd:Change to worktree directory' - 'claude:Start Claude Code in worktree' - 'cursor:Open Cursor editor in worktree' - 'desktop:Open GitHub Desktop for worktree' - 'list:List all worktrees' - 'help:Show help' - ) - - local -a worktree_names - local repo_root=$(git rev-parse --show-toplevel 2>/dev/null) - if [ -n "$repo_root" ]; then - local repo_name=$(basename "$repo_root") - local base="$(dirname "$repo_root")/${repo_name}-worktrees" - if [ -d "$base" ]; then - worktree_names=(${(f)"$(ls "$base" 2>/dev/null)"}) - fi - fi - - if (( CURRENT == 2 )); then - _describe 'commands' commands - elif (( CURRENT == 3 )); then - case "$words[2]" in - cd|claude|cursor|desktop|github|rm|remove) - _describe 'worktrees' worktree_names - ;; - esac - fi - } - - compdef _gtr gtr -fi \ No newline at end of file diff --git a/lib/config.sh b/lib/config.sh new file mode 100644 index 0000000..f4b01f3 --- /dev/null +++ b/lib/config.sh @@ -0,0 +1,134 @@ +#!/bin/sh +# Configuration management via git config +# Default values are defined where they're used in lib/core.sh + +# Get a single config value +# Usage: cfg_get key [scope] +# scope: local (default), global, or system +cfg_get() { + local key="$1" + local scope="${2:-local}" + local flag="" + + case "$scope" in + global) flag="--global" ;; + system) flag="--system" ;; + local|*) flag="--local" ;; + esac + + git config $flag "$key" 2>/dev/null || true +} + +# Get all values for a multi-valued config key +# Usage: cfg_get_all key [scope] +cfg_get_all() { + local key="$1" + local scope="${2:-local}" + local flag="" + + case "$scope" in + global) flag="--global" ;; + system) flag="--system" ;; + local|*) flag="--local" ;; + esac + + git config $flag --get-all "$key" 2>/dev/null || true +} + +# Get a boolean config value +# Usage: cfg_bool key [default] +# Returns: 0 for true, 1 for false +cfg_bool() { + local key="$1" + local default="${2:-false}" + local value + + value=$(cfg_get "$key") + + if [ -z "$value" ]; then + value="$default" + fi + + case "$value" in + true|yes|1|on) + return 0 + ;; + false|no|0|off|*) + return 1 + ;; + esac +} + +# Set a config value +# Usage: cfg_set key value [--global] +cfg_set() { + local key="$1" + local value="$2" + local scope="${3:-local}" + local flag="" + + case "$scope" in + --global|global) flag="--global" ;; + --system|system) flag="--system" ;; + --local|local|*) flag="--local" ;; + esac + + git config $flag "$key" "$value" +} + +# Add a value to a multi-valued config key +# Usage: cfg_add key value [--global] +cfg_add() { + local key="$1" + local value="$2" + local scope="${3:-local}" + local flag="" + + case "$scope" in + --global|global) flag="--global" ;; + --system|system) flag="--system" ;; + --local|local|*) flag="--local" ;; + esac + + git config $flag --add "$key" "$value" +} + +# Unset a config value +# Usage: cfg_unset key [--global] +cfg_unset() { + local key="$1" + local scope="${2:-local}" + local flag="" + + case "$scope" in + --global|global) flag="--global" ;; + --system|system) flag="--system" ;; + --local|local|*) flag="--local" ;; + esac + + git config $flag --unset "$key" 2>/dev/null || true +} + +# Get config value with environment variable fallback +# Usage: cfg_default key env_name fallback_value +cfg_default() { + local key="$1" + local env_name="$2" + local fallback="$3" + local value + + # Try git config first + value=$(cfg_get "$key") + + # Fall back to environment variable + if [ -z "$value" ] && [ -n "$env_name" ]; then + value=$(eval echo "\$$env_name") + fi + + # Use fallback if still empty + if [ -z "$value" ]; then + value="$fallback" + fi + + printf "%s" "$value" +} diff --git a/lib/copy.sh b/lib/copy.sh new file mode 100644 index 0000000..6d91e6c --- /dev/null +++ b/lib/copy.sh @@ -0,0 +1,107 @@ +#!/bin/sh +# File copying utilities with pattern matching + +# Copy files matching patterns from source to destination +# Usage: copy_patterns src_root dst_root includes excludes [preserve_paths] +# includes: newline-separated glob patterns to include +# excludes: newline-separated glob patterns to exclude +# preserve_paths: true (default) to preserve directory structure +copy_patterns() { + local src_root="$1" + local dst_root="$2" + local includes="$3" + local excludes="$4" + local preserve_paths="${5:-true}" + + if [ -z "$includes" ]; then + # No patterns to copy + return 0 + fi + + # Change to source directory + local old_pwd + old_pwd=$(pwd) + cd "$src_root" || return 1 + + local copied_count=0 + + # Process each include pattern (avoid pipeline subshell) + while IFS= read -r pattern; do + [ -z "$pattern" ] && continue + + # Find files matching the pattern (avoid pipeline subshell) + while IFS= read -r file; do + # Remove leading ./ + file="${file#./}" + + # Check if file matches any exclude pattern + local excluded=0 + if [ -n "$excludes" ]; then + while IFS= read -r exclude_pattern; do + [ -z "$exclude_pattern" ] && continue + case "$file" in + $exclude_pattern) + excluded=1 + break + ;; + esac + done </dev/null; then + log_info "Copied $file" + copied_count=$((copied_count + 1)) + else + log_warn "Failed to copy $file" + fi + done </dev/null) +EOF + done </dev/null; then + return 0 + else + return 1 + fi +} diff --git a/lib/core.sh b/lib/core.sh new file mode 100644 index 0000000..f190daa --- /dev/null +++ b/lib/core.sh @@ -0,0 +1,233 @@ +#!/bin/sh +# Core git worktree operations + +# Discover the root of the current git repository +# Returns: absolute path to repo root +# Exit code: 0 on success, 1 if not in a git repo +discover_repo_root() { + local root + root=$(git rev-parse --show-toplevel 2>/dev/null) + + if [ -z "$root" ]; then + log_error "Not in a git repository" + return 1 + fi + + printf "%s" "$root" +} + +# Resolve the base directory for worktrees +# Usage: resolve_base_dir repo_root +resolve_base_dir() { + local repo_root="$1" + local repo_name + local base_dir + + repo_name=$(basename "$repo_root") + + # Check config first (gtr.worktrees.dir), then environment (GTR_WORKTREES_DIR), then default + base_dir=$(cfg_default "gtr.worktrees.dir" "GTR_WORKTREES_DIR" "") + + if [ -z "$base_dir" ]; then + # Default: -worktrees next to the repo + base_dir="$(dirname "$repo_root")/${repo_name}-worktrees" + elif [ "${base_dir#/}" = "$base_dir" ]; then + # Relative path - resolve from repo parent + base_dir="$(dirname "$repo_root")/$base_dir" + fi + + printf "%s" "$base_dir" +} + +# Resolve the default branch name +# Usage: resolve_default_branch [repo_root] +resolve_default_branch() { + local repo_root="${1:-$(pwd)}" + local default_branch + local configured_branch + + # Check config first + configured_branch=$(cfg_default "gtr.defaultBranch" "GTR_DEFAULT_BRANCH" "auto") + + if [ "$configured_branch" != "auto" ]; then + printf "%s" "$configured_branch" + return 0 + fi + + # Auto-detect from origin/HEAD + default_branch=$(git symbolic-ref --quiet refs/remotes/origin/HEAD 2>/dev/null | sed 's|refs/remotes/origin/||') + + if [ -n "$default_branch" ]; then + printf "%s" "$default_branch" + return 0 + fi + + # Fallback: try common branch names + if git show-ref --verify --quiet "refs/remotes/origin/main"; then + printf "main" + elif git show-ref --verify --quiet "refs/remotes/origin/master"; then + printf "master" + else + # Last resort: just use 'main' + printf "main" + fi +} + +# Find the next available worktree ID +# Usage: next_available_id base_dir prefix [start_id] +next_available_id() { + local base_dir="$1" + local prefix="$2" + local start_id="${3:-2}" + local id="$start_id" + + while [ -d "$base_dir/${prefix}${id}" ]; do + id=$((id + 1)) + done + + printf "%s" "$id" +} + +# Get the current branch of a worktree +# Usage: current_branch worktree_path +current_branch() { + local worktree_path="$1" + + if [ ! -d "$worktree_path" ]; then + return 1 + fi + + (cd "$worktree_path" && git branch --show-current 2>/dev/null) || true +} + +# Create a new git worktree +# Usage: create_worktree base_dir prefix id branch_name from_ref track_mode +# track_mode: auto, remote, local, or none +create_worktree() { + local base_dir="$1" + local prefix="$2" + local id="$3" + local branch_name="$4" + local from_ref="$5" + local track_mode="${6:-auto}" + local worktree_path="$base_dir/${prefix}${id}" + + # Check if worktree already exists + if [ -d "$worktree_path" ]; then + log_error "Worktree ${prefix}${id} already exists at $worktree_path" + return 1 + fi + + # Create base directory if needed + mkdir -p "$base_dir" + + # Fetch latest refs + log_step "Fetching remote branches..." + git fetch origin 2>/dev/null || log_warn "Could not fetch from origin" + + local remote_exists=0 + local local_exists=0 + + git show-ref --verify --quiet "refs/remotes/origin/$branch_name" && remote_exists=1 + git show-ref --verify --quiet "refs/heads/$branch_name" && local_exists=1 + + case "$track_mode" in + remote) + # Force use of remote branch + if [ "$remote_exists" -eq 1 ]; then + log_step "Creating worktree from remote branch origin/$branch_name" + if git worktree add "$worktree_path" -b "$branch_name" "origin/$branch_name" 2>/dev/null || \ + git worktree add "$worktree_path" "$branch_name" 2>/dev/null; then + log_info "Worktree created tracking origin/$branch_name" + printf "%s" "$worktree_path" + return 0 + fi + else + log_error "Remote branch origin/$branch_name does not exist" + return 1 + fi + ;; + + local) + # Force use of local branch + if [ "$local_exists" -eq 1 ]; then + log_step "Creating worktree from local branch $branch_name" + if git worktree add "$worktree_path" "$branch_name" 2>/dev/null; then + log_info "Worktree created with local branch $branch_name" + printf "%s" "$worktree_path" + return 0 + fi + else + log_error "Local branch $branch_name does not exist" + return 1 + fi + ;; + + none) + # Create new branch from from_ref + log_step "Creating new branch $branch_name from $from_ref" + if git worktree add "$worktree_path" -b "$branch_name" "$from_ref" 2>/dev/null; then + log_info "Worktree created with new branch $branch_name" + printf "%s" "$worktree_path" + return 0 + else + log_error "Failed to create worktree with new branch" + return 1 + fi + ;; + + auto|*) + # Auto-detect best option + if [ "$remote_exists" -eq 1 ]; then + log_step "Branch '$branch_name' exists on remote" + if git worktree add "$worktree_path" -b "$branch_name" "origin/$branch_name" 2>/dev/null || \ + git worktree add "$worktree_path" "$branch_name" 2>/dev/null; then + log_info "Worktree created tracking origin/$branch_name" + printf "%s" "$worktree_path" + return 0 + fi + elif [ "$local_exists" -eq 1 ]; then + log_step "Using existing local branch $branch_name" + if git worktree add "$worktree_path" "$branch_name" 2>/dev/null; then + log_info "Worktree created with local branch $branch_name" + printf "%s" "$worktree_path" + return 0 + fi + else + log_step "Creating new branch $branch_name from $from_ref" + if git worktree add "$worktree_path" -b "$branch_name" "$from_ref" 2>/dev/null; then + log_info "Worktree created with new branch $branch_name" + printf "%s" "$worktree_path" + return 0 + fi + fi + ;; + esac + + log_error "Failed to create worktree" + return 1 +} + +# Remove a git worktree +# Usage: remove_worktree worktree_path +remove_worktree() { + local worktree_path="$1" + + if [ ! -d "$worktree_path" ]; then + log_error "Worktree not found at $worktree_path" + return 1 + fi + + if git worktree remove "$worktree_path" 2>/dev/null; then + log_info "Worktree removed: $worktree_path" + return 0 + else + log_error "Failed to remove worktree" + return 1 + fi +} + +# List all worktrees +list_worktrees() { + git worktree list +} diff --git a/lib/hooks.sh b/lib/hooks.sh new file mode 100644 index 0000000..030206e --- /dev/null +++ b/lib/hooks.sh @@ -0,0 +1,80 @@ +#!/bin/sh +# Hook execution system + +# Run hooks for a specific phase +# Usage: run_hooks phase [env_vars...] +# Example: run_hooks postCreate REPO_ROOT="$root" WORKTREE_PATH="$path" +run_hooks() { + local phase="$1" + shift + + local hooks + hooks=$(cfg_get_all "gtr.hook.$phase") + + if [ -z "$hooks" ]; then + # No hooks configured for this phase + return 0 + fi + + log_step "Running $phase hooks..." + + local hook_count=0 + local failed=0 + + # Export provided environment variables + while [ $# -gt 0 ]; do + export "$1" + shift + done + + # Execute each hook (without subshell to preserve state) + while IFS= read -r hook; do + [ -z "$hook" ] && continue + + hook_count=$((hook_count + 1)) + log_info "Hook $hook_count: $hook" + + # Run hook and capture exit code + if eval "$hook"; then + log_info "Hook $hook_count completed successfully" + else + local rc=$? + log_error "Hook $hook_count failed with exit code $rc" + failed=$((failed + 1)) + fi + done </dev/null)" in + Darwin) + echo "darwin" + ;; + Linux) + echo "linux" + ;; + MINGW*|MSYS*|CYGWIN*) + echo "windows" + ;; + *) + echo "unknown" + ;; + esac + ;; + esac +} + +# Open a directory in the system's GUI file browser +# Usage: open_in_gui path +open_in_gui() { + local path="$1" + local os + + os=$(detect_os) + + case "$os" in + darwin) + open "$path" 2>/dev/null || true + ;; + linux) + # Try common Linux file managers + if command -v xdg-open >/dev/null 2>&1; then + xdg-open "$path" >/dev/null 2>&1 || true + elif command -v gnome-open >/dev/null 2>&1; then + gnome-open "$path" >/dev/null 2>&1 || true + fi + ;; + windows) + if command -v cygpath >/dev/null 2>&1; then + cmd.exe /c start "" "$(cygpath -w "$path")" 2>/dev/null || true + else + cmd.exe /c start "" "$path" 2>/dev/null || true + fi + ;; + *) + log_warn "Cannot open GUI on unknown OS" + return 1 + ;; + esac +} + +# Spawn a new terminal window/tab in a directory +# Usage: spawn_terminal_in path title [command] +# Note: Best-effort implementation, may not work on all systems +spawn_terminal_in() { + local path="$1" + local title="$2" + local cmd="${3:-}" + local os + + os=$(detect_os) + + case "$os" in + darwin) + # Try iTerm2 first, then Terminal.app + if osascript -e 'tell application "System Events" to get name of first application process whose frontmost is true' 2>/dev/null | grep -q "iTerm"; then + osascript <<-EOF 2>/dev/null || true + tell application "iTerm" + tell current window + create tab with default profile + tell current session + write text "cd \"$path\"" + set name to "$title" + $([ -n "$cmd" ] && echo "write text \"$cmd\"") + end tell + end tell + end tell + EOF + else + osascript <<-EOF 2>/dev/null || true + tell application "Terminal" + do script "cd \"$path\"; $cmd" + set custom title of front window to "$title" + end tell + EOF + fi + ;; + linux) + # Try common terminal emulators + if command -v gnome-terminal >/dev/null 2>&1; then + gnome-terminal --working-directory="$path" --title="$title" -- sh -c "$cmd; exec \$SHELL" 2>/dev/null || true + elif command -v konsole >/dev/null 2>&1; then + konsole --workdir "$path" -p "tabtitle=$title" -e sh -c "$cmd; exec \$SHELL" 2>/dev/null || true + elif command -v xterm >/dev/null 2>&1; then + xterm -T "$title" -e "cd \"$path\" && $cmd && exec \$SHELL" 2>/dev/null || true + else + log_warn "No supported terminal emulator found" + return 1 + fi + ;; + windows) + # Try Windows Terminal, then fallback to cmd + if command -v wt >/dev/null 2>&1; then + wt -d "$path" "$cmd" 2>/dev/null || true + else + cmd.exe /c start "$title" cmd.exe /k "cd /d \"$path\" && $cmd" 2>/dev/null || true + fi + ;; + *) + log_warn "Cannot spawn terminal on unknown OS" + return 1 + ;; + esac +} diff --git a/lib/ui.sh b/lib/ui.sh new file mode 100644 index 0000000..993db82 --- /dev/null +++ b/lib/ui.sh @@ -0,0 +1,70 @@ +#!/bin/sh +# UI utilities for logging and prompting + +log_info() { + printf "✅ %s\n" "$*" +} + +log_warn() { + printf "⚠️ %s\n" "$*" +} + +log_error() { + printf "❌ %s\n" "$*" >&2 +} + +log_step() { + printf "🚀 %s\n" "$*" +} + +log_question() { + printf "❓ %s" "$*" +} + +# Prompt for yes/no confirmation +# Usage: prompt_yes_no "Question text" [default] +# Returns: 0 for yes, 1 for no +prompt_yes_no() { + local question="$1" + local default="${2:-n}" + local prompt_suffix="[y/N]" + + if [ "$default" = "y" ]; then + prompt_suffix="[Y/n]" + fi + + log_question "$question $prompt_suffix " + read -r reply + + case "$reply" in + [yY]|[yY][eE][sS]) + return 0 + ;; + [nN]|[nN][oO]) + return 1 + ;; + "") + [ "$default" = "y" ] && return 0 || return 1 + ;; + *) + [ "$default" = "y" ] && return 0 || return 1 + ;; + esac +} + +# Prompt for text input +# Usage: prompt_input "Question text" [variable_name] +# If variable_name provided, sets it, otherwise echoes result +prompt_input() { + local question="$1" + local var_name="$2" + + log_question "$question " + read -r input + + if [ -n "$var_name" ]; then + eval "$var_name=\"\$input\"" + else + printf "%s" "$input" + fi +} diff --git a/run_services.example.sh b/run_services.example.sh deleted file mode 100644 index 67371b5..0000000 --- a/run_services.example.sh +++ /dev/null @@ -1,15 +0,0 @@ - #!/usr/bin/env bash - set -euo pipefail - BASE_DIR="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" - - require() { command -v "$1" >/dev/null || { echo "Missing: $1"; exit 1; }; } - require ttab; require pnpm; require turbo - - run_in_new_terminal() { ttab -d "$BASE_DIR/$2" -t "$1" "$3"; } - - # pnpm install && turbo build # uncomment if desired - run_in_new_terminal "DB API Server" "apps/db-api-server" "pnpm run dev" - # add your services... - if command -v ngrok >/dev/null && [ -f "$BASE_DIR/ngrok.yaml" ]; then - run_in_new_terminal "ngrok - PR Reviewer" "." "ngrok start --all --config ngrok.yaml" - fi diff --git a/templates/gtr.config.example b/templates/gtr.config.example new file mode 100644 index 0000000..f4e8380 --- /dev/null +++ b/templates/gtr.config.example @@ -0,0 +1,72 @@ +# Example gtr configuration +# Copy this to your repository and customize as needed + +# === Worktree Settings === + +# Base directory for worktrees (relative to repo parent or absolute path) +# Default: -worktrees +# gtr.worktrees.dir = my-worktrees + +# Prefix for worktree directory names +# Default: wt- +# gtr.worktrees.prefix = dev- + +# Starting ID for auto-assigned worktrees +# Default: 2 +# gtr.worktrees.startId = 1 + +# Default branch to create new branches from +# Options: auto (detect from origin/HEAD), main, master, or any branch name +# Default: auto +# gtr.defaultBranch = main + +# === Editor Settings === + +# Default editor to open worktrees +# Options: cursor, vscode, zed, or none +# Default: none +# gtr.editor.default = cursor + +# === AI Tool Settings === + +# Default AI coding tool +# Options: aider, or none +# Default: none +# gtr.ai.default = aider + +# === File Copying === + +# Files to copy to new worktrees (glob patterns, can specify multiple) +# Default: none +# gtr.copy.include = **/.env.example +# gtr.copy.include = **/CLAUDE.md +# gtr.copy.include = apps/*/run.sh + +# Files to exclude from copying (glob patterns) +# gtr.copy.exclude = **/.env +# gtr.copy.exclude = **/.env.local +# gtr.copy.exclude = **/secrets.json + +# === Hooks === + +# Commands to run after creating a worktree (can specify multiple, run in order) +# gtr.hook.postCreate = npm install +# gtr.hook.postCreate = npm run build + +# Commands to run after removing a worktree +# gtr.hook.postRemove = echo "Cleaned up worktree" + +# === How to Apply This Config === + +# For local repository only: +# git config --local gtr.editor.default cursor +# git config --local --add gtr.copy.include "**/.env.example" +# git config --local --add gtr.hook.postCreate "pnpm install" + +# For all repositories (global): +# git config --global gtr.worktrees.startId 2 +# git config --global gtr.editor.default vscode + +# Or use gtr's config command: +# gtr config set gtr.editor.default cursor +# gtr config set gtr.editor.default cursor --global diff --git a/templates/run_services.example.sh b/templates/run_services.example.sh new file mode 100755 index 0000000..1964c50 --- /dev/null +++ b/templates/run_services.example.sh @@ -0,0 +1,71 @@ +#!/bin/sh +# Example service runner script for development +# This demonstrates a generic pattern for running multiple services in a worktree + +set -e + +# Get the repository root +REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" + +echo "🚀 Starting development services..." +echo "📂 Repository: $REPO_ROOT" +echo "" + +# Example: Run a database +# Uncomment and customize for your needs +# echo "Starting database..." +# docker-compose up -d postgres +# Or: pg_ctl -D /usr/local/var/postgres start + +# Example: Run API server in background +# echo "Starting API server..." +# cd "$REPO_ROOT/apps/api" && npm run dev & + +# Example: Run frontend dev server +# echo "Starting frontend..." +# cd "$REPO_ROOT/apps/web" && npm run dev & + +# Example: Run multiple services with a process manager +# if command -v overmind >/dev/null 2>&1; then +# echo "Starting services with Overmind..." +# cd "$REPO_ROOT" && overmind start +# elif command -v foreman >/dev/null 2>&1; then +# echo "Starting services with Foreman..." +# cd "$REPO_ROOT" && foreman start +# else +# echo "No process manager found. Install overmind or foreman." +# fi + +# Example: Run services in new terminal tabs (macOS with iTerm2) +# if command -v osascript >/dev/null 2>&1; then +# osascript <<-EOF +# tell application "iTerm" +# tell current window +# create tab with default profile +# tell current session +# write text "cd '$REPO_ROOT/apps/api' && npm run dev" +# end tell +# end tell +# end tell +# EOF +# fi + +# Example: Run with tmux sessions +# if command -v tmux >/dev/null 2>&1; then +# tmux new-session -d -s dev "cd $REPO_ROOT/apps/api && npm run dev" +# tmux split-window -h "cd $REPO_ROOT/apps/web && npm run dev" +# tmux attach-session -t dev +# fi + +echo "✅ Services started!" +echo "" +echo "💡 Customize this script for your project's needs:" +echo " - Docker containers" +echo " - Development servers" +echo " - Background workers" +echo " - Database migrations" +echo "" +echo "To stop services, use Ctrl+C or your process manager's stop command." + +# Keep script running if services are in background +# wait diff --git a/templates/setup-example.sh b/templates/setup-example.sh new file mode 100755 index 0000000..fd2a7e9 --- /dev/null +++ b/templates/setup-example.sh @@ -0,0 +1,36 @@ +#!/bin/sh +# Example setup script for gtr configuration +# Customize this for your project + +set -e + +echo "🔧 Configuring gtr for this repository..." + +# Worktree settings +git config --local gtr.worktrees.prefix "wt-" +git config --local gtr.worktrees.startId 2 +git config --local gtr.defaultBranch "auto" + +# Editor (change to your preference: cursor, vscode, zed) +# git config --local gtr.editor.default cursor + +# File copying (add patterns for your project) +# git config --local --add gtr.copy.include "**/.env.example" +# git config --local --add gtr.copy.include "**/CLAUDE.md" + +# Hooks (customize for your build system) +# git config --local --add gtr.hook.postCreate "npm install" +# git config --local --add gtr.hook.postCreate "npm run build" + +# Or for pnpm projects: +# git config --local --add gtr.hook.postCreate "pnpm install" +# git config --local --add gtr.hook.postCreate "pnpm run build" + +# Or for other tools: +# git config --local --add gtr.hook.postCreate "bundle install" +# git config --local --add gtr.hook.postCreate "cargo build" + +echo "✅ gtr configured!" +echo "" +echo "View config with: git config --local --list | grep gtr" +echo "Create a worktree with: gtr create --branch my-feature --auto" From c2bf148c9c5e3775c2a800d6031b20d01e97cdbe Mon Sep 17 00:00:00 2001 From: Hans Elizaga Date: Fri, 3 Oct 2025 17:42:39 -0700 Subject: [PATCH 02/19] Implement command renaming and enhance functionality - Added .gitignore to exclude CLAUDE.md. - Renamed `create` command to `new` for clarity and consistency. - Updated README.md to reflect command changes and improved examples. - Enhanced command options for `gtr` to support both worktree IDs and branch names. - Introduced new commands: `go` for navigating to worktrees and `clean` for removing stale worktrees. - Improved completion scripts for better user experience in shell environments. --- .gitignore | 1 + README.md | 201 +++++++++------ adapters/ai/claudecode.sh | 11 +- bin/gtr | 515 ++++++++++++++++++++++++++++---------- completions/_gtr | 58 +++-- completions/gtr.bash | 50 ++-- completions/gtr.fish | 47 ++-- lib/core.sh | 95 ++++++- 8 files changed, 684 insertions(+), 294 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ceb2b98 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +CLAUDE.md diff --git a/README.md b/README.md index ef871d3..9052445 100644 --- a/README.md +++ b/README.md @@ -18,16 +18,16 @@ Git worktrees let you check out multiple branches at once in separate directorie While `git worktree` is powerful, it requires remembering paths and manually setting up each worktree. `gtr` adds: -| Task | With `git worktree` | With `gtr` | -|------|---------------------|------------| -| Create worktree | `git worktree add ../repo-feature feature` | `gtr create --branch feature --auto` | -| Open in editor | `cd ../repo-feature && cursor .` | `gtr open 2 --editor cursor` | -| Start AI tool | `cd ../repo-feature && aider` | `gtr ai 2 --tool aider` | -| Copy config files | Manual copy/paste | Auto-copy via `gtr.copy.include` | -| Run build steps | Manual `npm install && npm run build` | Auto-run via `gtr.hook.postCreate` | -| List worktrees | `git worktree list` (shows paths) | `gtr list` (shows IDs + status) | -| Switch to worktree | `cd ../repo-feature` | `cd "$(gtr cd 2)"` | -| Clean up | `git worktree remove ../repo-feature` | `gtr rm 2` | +| Task | With `git worktree` | With `gtr` | +| ------------------ | ------------------------------------------ | ---------------------------------- | +| Create worktree | `git worktree add ../repo-feature feature` | `gtr new feature` | +| Open in editor | `cd ../repo-feature && cursor .` | `gtr open 2` | +| Start AI tool | `cd ../repo-feature && aider` | `gtr ai 2` | +| Copy config files | Manual copy/paste | Auto-copy via `gtr.copy.include` | +| Run build steps | Manual `npm install && npm run build` | Auto-run via `gtr.hook.postCreate` | +| List worktrees | `git worktree list` (shows paths) | `gtr list` (shows IDs + status) | +| Switch to worktree | `cd ../repo-feature` | `cd "$(gtr go 2)"` | +| Clean up | `git worktree remove ../repo-feature` | `gtr rm 2` | **TL;DR:** `gtr` wraps `git worktree` with quality-of-life features for modern development workflows (AI tools, editors, automation). @@ -63,123 +63,154 @@ source ~/.zshrc ### Shell Completions (Optional) **Bash:** + ```bash echo 'source /path/to/git-worktree-runner/completions/gtr.bash' >> ~/.bashrc ``` **Zsh:** + ```bash echo 'source /path/to/git-worktree-runner/completions/_gtr' >> ~/.zshrc ``` **Fish:** + ```bash ln -s /path/to/git-worktree-runner/completions/gtr.fish ~/.config/fish/completions/ ``` ## Quick Start +**Basic workflow (no flags needed):** ```bash -# Create a worktree with auto-assigned ID -gtr create --branch my-feature --auto - -# Create worktree with specific ID -gtr create --id 3 --branch ui-fixes - -# List all worktrees -gtr list - -# Open worktree in editor (if configured) -gtr open 2 --editor cursor +# One-time setup +gtr config set gtr.editor.default cursor +gtr config set gtr.ai.default aider -# Start AI tool in worktree -gtr ai 2 --tool aider +# Daily use - simple, no flags +gtr new my-feature # Create worktree (auto-assigns ID) +gtr list # See all worktrees +gtr open my-feature # Open in cursor (from config) +gtr ai my-feature # Start aider (from config) +cd "$(gtr go my-feature)" # Navigate to worktree +gtr rm 2 # Remove when done +``` -# Remove worktree -gtr rm 2 +**Advanced examples:** +```bash +# Override defaults +gtr open 2 --editor vscode +gtr new hotfix --from v1.2.3 --id 99 -# Change to worktree directory -gtr cd 2 +# Destructive operations +gtr rm 2 --delete-branch --force ``` ## Commands -### `gtr create` +### `gtr new` -Create a new git worktree. +Create a new git worktree. IDs are auto-assigned by default. ```bash -gtr create [options] [branch-name] +gtr new [options] -Options: - --branch Branch name - --id Worktree ID (default: auto-assigned) - --auto Auto-assign next available ID - --from Create branch from ref (default: main/master) - --track Track mode: auto|remote|local|none - --open [editor] Open in editor after creation - --ai [tool] Start AI tool after creation - --no-copy Skip file copying - --yes Non-interactive mode +Options (all optional): + --id Specific worktree ID (rarely needed) + --from Create from specific ref (default: main/master) + --track Track mode: auto|remote|local|none + --editor Override default editor + --ai Override default AI tool + --no-copy Skip file copying + --no-fetch Skip git fetch + --yes Non-interactive mode ``` **Examples:** + ```bash -# Auto-assign ID, prompt for branch -gtr create --auto +# Create worktree (auto-assigns ID) +gtr new my-feature # Specific ID and branch -gtr create --id 2 --branch feature-x +gtr new feature-x --id 2 # Create from specific ref -gtr create --branch hotfix --from v1.2.3 +gtr new hotfix --from v1.2.3 # Create and open in Cursor -gtr create --branch ui --auto --open cursor +gtr new ui --editor cursor # Create and start Aider -gtr create --branch refactor --auto --ai aider +gtr new refactor --ai aider ``` ### `gtr open` -Open a worktree in an editor or file browser. +Open a worktree in an editor or file browser. Accepts either ID or branch name. ```bash -gtr open [--editor ] +gtr open [options] Options: - --editor Editor: cursor, vscode, zed + --editor Editor: cursor, vscode, zed ``` **Examples:** + ```bash -# Open in default editor +# Open by ID (uses default editor from config) gtr open 2 -# Open in specific editor -gtr open 2 --editor cursor +# Open by branch name with specific editor +gtr open my-feature --editor cursor + +# Override default editor +gtr open 2 --editor zed +``` + +### `gtr go` + +Navigate to a worktree directory. Prints path to stdout for shell integration. + +```bash +gtr go +``` + +**Examples:** + +```bash +# Change to worktree by ID +cd "$(gtr go 2)" + +# Change to worktree by branch name +cd "$(gtr go my-feature)" ``` ### `gtr ai` -Start an AI coding tool in a worktree. +Start an AI coding tool in a worktree. Accepts either ID or branch name. ```bash -gtr ai [--tool ] [-- args...] +gtr ai [options] [-- args...] Options: - --tool AI tool: aider - -- Pass remaining args to tool + --tool AI tool: aider, claudecode, etc. + -- Pass remaining args to tool ``` **Examples:** + ```bash -# Start default AI tool +# Start default AI tool by ID (uses gtr.ai.default) gtr ai 2 +# Start by branch name with specific tool +gtr ai my-feature --tool aider + # Start Aider with specific model -gtr ai 2 --tool aider -- --model gpt-4o +gtr ai 2 --tool aider -- --model gpt-5 ``` ### `gtr rm` @@ -190,11 +221,13 @@ Remove worktree(s). gtr rm [...] [options] Options: - --delete-branch Also delete the branch - --yes Non-interactive mode + --delete-branch Also delete the branch + --force Force removal even with uncommitted changes + --yes Non-interactive mode ``` **Examples:** + ```bash # Remove single worktree gtr rm 2 @@ -204,6 +237,9 @@ gtr rm 2 --delete-branch # Remove multiple worktrees gtr rm 2 3 4 --yes + +# Force remove with uncommitted changes +gtr rm 2 --force ``` ### `gtr list` @@ -211,15 +247,11 @@ gtr rm 2 3 4 --yes List all git worktrees. ```bash -gtr list -``` - -### `gtr cd` - -Change to worktree directory (prints info for shell integration). +gtr list [--porcelain|--ids] -```bash -gtr cd +Options: + --porcelain Machine-readable output (tab-separated) + --ids Output only worktree IDs (for scripting) ``` ### `gtr config` @@ -233,6 +265,7 @@ gtr config unset [--global] ``` **Examples:** + ```bash # Set default editor locally gtr config set gtr.editor.default cursor @@ -272,6 +305,7 @@ gtr.editor.default = cursor ``` **Setup editors:** + - **Cursor**: Install from [cursor.com](https://cursor.com), enable shell command - **VS Code**: Install from [code.visualstudio.com](https://code.visualstudio.com), enable `code` command - **Zed**: Install from [zed.dev](https://zed.dev), `zed` command available automatically @@ -285,22 +319,23 @@ gtr.ai.default = none **Supported AI Tools:** -| Tool | Install | Use Case | Command Example | -|------|---------|----------|-----------------| -| **[Aider](https://aider.chat)** | `pip install aider-chat` | Pair programming, edit files with AI | `gtr ai 2 --tool aider` | -| **[Claude Code](https://claude.com/claude-code)** | Install from claude.com | Terminal-native coding agent | `gtr ai 2 --tool claudecode` | -| **[Codex CLI](https://github.com/openai/codex)** | `npm install -g @openai/codex` | OpenAI coding assistant | `gtr ai 2 --tool codex -- "add tests"` | -| **[Cursor](https://cursor.com)** | Install from cursor.com | AI-powered editor with CLI agent | `gtr ai 2 --tool cursor` | -| **[Continue](https://continue.dev)** | See [docs](https://docs.continue.dev/cli/install) | Open-source coding agent | `gtr ai 2 --tool continue` | +| Tool | Install | Use Case | Command Example | +| ------------------------------------------------- | ------------------------------------------------- | ------------------------------------ | -------------------------------------- | +| **[Aider](https://aider.chat)** | `pip install aider-chat` | Pair programming, edit files with AI | `gtr ai 2 --tool aider` | +| **[Claude Code](https://claude.com/claude-code)** | Install from claude.com | Terminal-native coding agent | `gtr ai 2 --tool claudecode` | +| **[Codex CLI](https://github.com/openai/codex)** | `npm install -g @openai/codex` | OpenAI coding assistant | `gtr ai 2 --tool codex -- "add tests"` | +| **[Cursor](https://cursor.com)** | Install from cursor.com | AI-powered editor with CLI agent | `gtr ai 2 --tool cursor` | +| **[Continue](https://continue.dev)** | See [docs](https://docs.continue.dev/cli/install) | Open-source coding agent | `gtr ai 2 --tool continue` | **Examples:** + ```bash # Set default AI tool globally gtr config set gtr.ai.default aider --global # Use specific tools per worktree gtr ai 2 --tool claudecode -- --plan "refactor auth" -gtr ai 3 --tool aider -- --model gpt-4o +gtr ai 3 --tool aider -- --model gpt-5 gtr ai 4 --tool continue -- --headless ``` @@ -335,6 +370,7 @@ git config --add gtr.hook.postRemove "echo 'Cleaned up!'" ``` **Environment variables available in hooks:** + - `REPO_ROOT` - Repository root path - `WORKTREE_PATH` - New worktree path - `BRANCH` - Branch name @@ -402,13 +438,13 @@ git config --global gtr.worktrees.startId 2 ```bash # Terminal 1: Work on feature -gtr create --branch feature-a --id 2 --open +gtr new feature-a --id 2 --editor cursor # Terminal 2: Review PR -gtr create --branch pr/123 --id 3 --open +gtr new pr/123 --id 3 --editor cursor -# Terminal 3: Run tests on main -gtr cd 1 # Original repo is worktree 1 +# Terminal 3: Navigate to main branch (repo root) +cd "$(gtr go 1)" # ID 1 is always the repo root ``` ### Custom Workflows with Hooks @@ -440,7 +476,7 @@ Perfect for CI/CD or scripts: ```bash # Create worktree without prompts -gtr create --branch ci-test --id 99 --yes --no-copy +gtr new ci-test --id 99 --yes --no-copy # Remove without confirmation gtr rm 99 --yes --delete-branch @@ -458,7 +494,7 @@ git fetch origin git branch -a | grep your-branch # Manually specify tracking mode -gtr create --branch test --track remote +gtr new test --track remote ``` ### Editor Not Opening @@ -492,6 +528,7 @@ find . -path "**/.env.example" - ✅ **Windows** - Via Git Bash or WSL **Platform-specific notes:** + - **macOS**: GUI opening uses `open`, terminal spawning uses iTerm2/Terminal.app - **Linux**: GUI opening uses `xdg-open`, terminal spawning uses gnome-terminal/konsole - **Windows**: GUI opening uses `start`, requires Git Bash or WSL diff --git a/adapters/ai/claudecode.sh b/adapters/ai/claudecode.sh index 3f14cc1..8ed94e2 100644 --- a/adapters/ai/claudecode.sh +++ b/adapters/ai/claudecode.sh @@ -3,7 +3,7 @@ # Check if Claude Code is available ai_can_start() { - command -v claude-code >/dev/null 2>&1 + command -v claude >/dev/null 2>&1 || command -v claude-code >/dev/null 2>&1 } # Start Claude Code in a directory @@ -14,6 +14,7 @@ ai_start() { if ! ai_can_start; then log_error "Claude Code not found. Install from https://claude.com/claude-code" + log_info "The CLI is called 'claude' (or 'claude-code' in older versions)" return 1 fi @@ -22,6 +23,10 @@ ai_start() { return 1 fi - # Change to the directory and run claude-code with any additional arguments - (cd "$path" && claude-code "$@") + # Try 'claude' first (official binary name), fallback to 'claude-code' + if command -v claude >/dev/null 2>&1; then + (cd "$path" && claude "$@") + else + (cd "$path" && claude-code "$@") + fi } diff --git a/bin/gtr b/bin/gtr index f572db3..d0dfb97 100755 --- a/bin/gtr +++ b/bin/gtr @@ -5,7 +5,7 @@ set -e # Version -GTR_VERSION="2.0.0" +GTR_VERSION="1.0.0" # Find the script directory GTR_DIR="$(cd "$(dirname "$0")/.." && pwd)" @@ -24,14 +24,14 @@ main() { shift 2>/dev/null || true case "$cmd" in - create) + new) cmd_create "$@" ;; - rm|remove) + rm) cmd_remove "$@" ;; - cd) - cmd_cd "$@" + go) + cmd_go "$@" ;; open) cmd_open "$@" @@ -39,11 +39,17 @@ main() { ai) cmd_ai "$@" ;; - list|ls) + ls|list) cmd_list "$@" ;; - ids) - cmd_ids "$@" + clean) + cmd_clean "$@" + ;; + doctor) + cmd_doctor "$@" + ;; + adapter|adapters) + cmd_adapter "$@" ;; config) cmd_config "$@" @@ -70,25 +76,19 @@ cmd_create() { local track_mode="auto" local editor="" local ai_tool="" - local auto_id=0 + local explicit_id=0 local skip_copy=0 + local skip_fetch=0 local yes_mode=0 # Parse flags and arguments while [ $# -gt 0 ]; do case "$1" in - --branch) - branch_name="$2" - shift 2 - ;; --id) worktree_id="$2" + explicit_id=1 shift 2 ;; - --auto) - auto_id=1 - shift - ;; --from) from_ref="$2" shift 2 @@ -97,7 +97,7 @@ cmd_create() { track_mode="$2" shift 2 ;; - --open) + --editor) case "${2-}" in --*|"") editor="$(cfg_default gtr.editor.default GTR_EDITOR_DEFAULT none)" @@ -125,7 +125,11 @@ cmd_create() { skip_copy=1 shift ;; - --yes|-y) + --no-fetch) + skip_fetch=1 + shift + ;; + --yes) yes_mode=1 shift ;; @@ -134,14 +138,8 @@ cmd_create() { exit 1 ;; *) - # Positional arguments - if [ -z "$worktree_id" ] && [ -z "$branch_name" ]; then - if echo "$1" | grep -qE '^[0-9]+$'; then - worktree_id="$1" - else - branch_name="$1" - fi - elif [ -z "$branch_name" ]; then + # Positional argument: treat as branch name + if [ -z "$branch_name" ]; then branch_name="$1" fi shift @@ -158,8 +156,8 @@ cmd_create() { prefix=$(cfg_default gtr.worktrees.prefix GTR_WORKTREES_PREFIX "wt-") start_id=$(cfg_default gtr.worktrees.startId GTR_WORKTREES_STARTID "2") - # Auto-assign ID if needed - if [ -z "$worktree_id" ] || [ "$auto_id" -eq 1 ]; then + # Auto-assign ID if not explicitly provided (NEW DEFAULT BEHAVIOR) + if [ "$explicit_id" -eq 0 ]; then worktree_id=$(next_available_id "$base_dir" "$prefix" "$start_id") fi @@ -189,7 +187,7 @@ cmd_create() { echo "🌿 Branch: $branch_name" # Create the worktree - if ! create_worktree "$base_dir" "$prefix" "$worktree_id" "$branch_name" "$from_ref" "$track_mode"; then + if ! create_worktree "$base_dir" "$prefix" "$worktree_id" "$branch_name" "$from_ref" "$track_mode" "$skip_fetch"; then exit 1 fi @@ -240,6 +238,7 @@ cmd_create() { cmd_remove() { local delete_branch=0 local yes_mode=0 + local force=0 local worktree_ids="" # Parse flags @@ -249,10 +248,14 @@ cmd_remove() { delete_branch=1 shift ;; - --yes|-y) + --yes) yes_mode=1 shift ;; + --force) + force=1 + shift + ;; -*) log_error "Unknown flag: $1" exit 1 @@ -265,7 +268,7 @@ cmd_remove() { done if [ -z "$worktree_ids" ]; then - log_error "Usage: gtr rm [...] [--delete-branch] [--yes]" + log_error "Usage: gtr rm [...] [--delete-branch] [--force] [--yes]" exit 1 fi @@ -289,7 +292,7 @@ cmd_remove() { branch=$(current_branch "$worktree_path") # Remove the worktree - if ! remove_worktree "$worktree_path"; then + if ! remove_worktree "$worktree_path" "$force"; then continue fi @@ -314,41 +317,42 @@ cmd_remove() { done } -# CD command (prints path for shell integration) -cmd_cd() { +# Go command (navigate to worktree - prints path for shell integration) +cmd_go() { if [ $# -ne 1 ]; then - log_error "Usage: gtr cd " + log_error "Usage: gtr go " exit 1 fi - local worktree_id="$1" + local identifier="$1" local repo_root base_dir prefix repo_root=$(discover_repo_root) || exit 1 base_dir=$(resolve_base_dir "$repo_root") prefix=$(cfg_default gtr.worktrees.prefix GTR_WORKTREES_PREFIX "wt-") - local worktree_path="$base_dir/${prefix}${worktree_id}" - - if [ ! -d "$worktree_path" ]; then - log_error "Worktree not found: ${prefix}${worktree_id}" - exit 1 - fi - - local branch - branch=$(current_branch "$worktree_path") + # Resolve target (supports both ID and branch name) + local target worktree_id worktree_path branch + target=$(resolve_target "$identifier" "$repo_root" "$base_dir" "$prefix") || exit 1 + worktree_id=$(echo "$target" | cut -f1) + worktree_path=$(echo "$target" | cut -f2) + branch=$(echo "$target" | cut -f3) # Human messages to stderr so stdout can be used in command substitution - echo "📂 Switched to worktree: ${prefix}${worktree_id}" >&2 - echo "🌿 Current branch: $branch" >&2 + if [ "$worktree_id" = "1" ]; then + echo "📂 Repo root (id 1)" >&2 + else + echo "📂 Worktree ${prefix}${worktree_id}" >&2 + fi + echo "🌿 Branch: $branch" >&2 - # Print path to stdout for shell integration: cd "$(gtr cd 2)" + # Print path to stdout for shell integration: cd "$(gtr go 2)" printf "%s\n" "$worktree_path" } # Open command cmd_open() { local editor="" - local worktree_id="" + local identifier="" # Parse arguments while [ $# -gt 0 ]; do @@ -362,14 +366,14 @@ cmd_open() { exit 1 ;; *) - worktree_id="$1" + identifier="$1" shift ;; esac done - if [ -z "$worktree_id" ]; then - log_error "Usage: gtr open [--editor ]" + if [ -z "$identifier" ]; then + log_error "Usage: gtr open [--editor ]" exit 1 fi @@ -378,36 +382,25 @@ cmd_open() { editor=$(cfg_default gtr.editor.default GTR_EDITOR_DEFAULT "none") fi - if [ "$editor" = "none" ]; then - # Just open in GUI file browser - local repo_root base_dir prefix - repo_root=$(discover_repo_root) || exit 1 - base_dir=$(resolve_base_dir "$repo_root") - prefix=$(cfg_default gtr.worktrees.prefix GTR_WORKTREES_PREFIX "wt-") - local worktree_path="$base_dir/${prefix}${worktree_id}" + local repo_root base_dir prefix + repo_root=$(discover_repo_root) || exit 1 + base_dir=$(resolve_base_dir "$repo_root") + prefix=$(cfg_default gtr.worktrees.prefix GTR_WORKTREES_PREFIX "wt-") - if [ ! -d "$worktree_path" ]; then - log_error "Worktree not found: ${prefix}${worktree_id}" - exit 1 - fi + # Resolve target (supports both ID and branch name) + local target worktree_id worktree_path branch + target=$(resolve_target "$identifier" "$repo_root" "$base_dir" "$prefix") || exit 1 + worktree_id=$(echo "$target" | cut -f1) + worktree_path=$(echo "$target" | cut -f2) + branch=$(echo "$target" | cut -f3) + if [ "$editor" = "none" ]; then + # Just open in GUI file browser open_in_gui "$worktree_path" log_info "Opened in file browser" else # Load editor adapter and open load_editor_adapter "$editor" - - local repo_root base_dir prefix - repo_root=$(discover_repo_root) || exit 1 - base_dir=$(resolve_base_dir "$repo_root") - prefix=$(cfg_default gtr.worktrees.prefix GTR_WORKTREES_PREFIX "wt-") - local worktree_path="$base_dir/${prefix}${worktree_id}" - - if [ ! -d "$worktree_path" ]; then - log_error "Worktree not found: ${prefix}${worktree_id}" - exit 1 - fi - log_step "Opening in $editor..." editor_open "$worktree_path" fi @@ -416,7 +409,7 @@ cmd_open() { # AI command cmd_ai() { local ai_tool="" - local worktree_id="" + local identifier="" local ai_args="" # Parse arguments @@ -436,22 +429,30 @@ cmd_ai() { exit 1 ;; *) - if [ -z "$worktree_id" ]; then - worktree_id="$1" + if [ -z "$identifier" ]; then + identifier="$1" fi shift ;; esac done - if [ -z "$worktree_id" ]; then - log_error "Usage: gtr ai [--tool ] [-- args...]" + if [ -z "$identifier" ]; then + log_error "Usage: gtr ai [--tool ] [-- args...]" exit 1 fi # Get AI tool from config if not specified if [ -z "$ai_tool" ]; then - ai_tool=$(cfg_default gtr.ai.default GTR_AI_DEFAULT "aider") + ai_tool=$(cfg_default gtr.ai.default GTR_AI_DEFAULT "none") + fi + + # Check if AI tool is configured + if [ "$ai_tool" = "none" ]; then + log_error "No AI tool configured" + log_info "Set default: gtr config set gtr.ai.default aider" + log_info "Or use: gtr ai $identifier --tool aider" + exit 1 fi # Load AI adapter @@ -461,15 +462,13 @@ cmd_ai() { repo_root=$(discover_repo_root) || exit 1 base_dir=$(resolve_base_dir "$repo_root") prefix=$(cfg_default gtr.worktrees.prefix GTR_WORKTREES_PREFIX "wt-") - local worktree_path="$base_dir/${prefix}${worktree_id}" - if [ ! -d "$worktree_path" ]; then - log_error "Worktree not found: ${prefix}${worktree_id}" - exit 1 - fi - - local branch - branch=$(current_branch "$worktree_path") + # Resolve target (supports both ID and branch name) + local target worktree_id worktree_path branch + target=$(resolve_target "$identifier" "$repo_root" "$base_dir" "$prefix") || exit 1 + worktree_id=$(echo "$target" | cut -f1) + worktree_path=$(echo "$target" | cut -f2) + branch=$(echo "$target" | cut -f3) log_step "Starting $ai_tool in worktree: ${prefix}${worktree_id}" echo "📂 Directory: $worktree_path" @@ -481,41 +480,259 @@ cmd_ai() { # List command cmd_list() { - echo "📋 Git worktrees:" - list_worktrees + local porcelain=0 + local ids_only=0 - local repo_root base_dir - repo_root=$(discover_repo_root) || exit 1 + # Parse flags + while [ $# -gt 0 ]; do + case "$1" in + --porcelain) + porcelain=1 + shift + ;; + --ids) + ids_only=1 + shift + ;; + *) + shift + ;; + esac + done + + local repo_root base_dir prefix + repo_root=$(discover_repo_root) 2>/dev/null || return 0 base_dir=$(resolve_base_dir "$repo_root") + prefix=$(cfg_default gtr.worktrees.prefix GTR_WORKTREES_PREFIX "wt-") + # IDs only (for completions) + if [ "$ids_only" -eq 1 ]; then + # Always include ID 1 (repo root) + echo "1" + + if [ -d "$base_dir" ]; then + find "$base_dir" -maxdepth 1 -type d -name "${prefix}*" 2>/dev/null | while IFS= read -r dir; do + basename "$dir" | sed "s/^${prefix}//" + done | sort -n + fi + return 0 + fi + + # Machine-readable output (porcelain) + if [ "$porcelain" -eq 1 ]; then + # Always include ID 1 (repo root) + local branch + branch=$(git -C "$repo_root" branch --show-current 2>/dev/null) + [ -z "$branch" ] && branch="(detached)" + printf "%s\t%s\t%s\t%s\n" "1" "$repo_root" "$branch" "ok" + + if [ -d "$base_dir" ]; then + # Find all worktree directories and output: idpathbranchstatus + find "$base_dir" -maxdepth 1 -type d -name "${prefix}*" 2>/dev/null | while IFS= read -r dir; do + local id branch + id=$(basename "$dir" | sed "s/^${prefix}//") + branch=$(current_branch "$dir") + [ -z "$branch" ] && branch="(detached)" + printf "%s\t%s\t%s\t%s\n" "$id" "$dir" "$branch" "ok" + done | sort -t$'\t' -k1 -n + fi + return 0 + fi + + # Human-readable output - table format + echo "📋 Git Worktrees" echo "" - echo "📁 Worktree directories:" + printf "%-6s %-25s %s\n" "ID" "BRANCH" "PATH" + printf "%-6s %-25s %s\n" "──" "──────" "────" + + # Always show repo root as ID 1 + local branch + branch=$(git -C "$repo_root" branch --show-current 2>/dev/null) + [ -z "$branch" ] && branch="(detached)" + printf "%-6s %-25s %s\n" "1" "$branch" "$repo_root" + + # Show worktrees if [ -d "$base_dir" ]; then - ls -la "$base_dir" 2>/dev/null | grep "^d" | grep -v "^d.*\\.$" || echo " (none found)" - else - echo " (worktrees directory doesn't exist yet)" + find "$base_dir" -maxdepth 1 -type d -name "${prefix}*" 2>/dev/null | while IFS= read -r dir; do + local id branch + id=$(basename "$dir" | sed "s/^${prefix}//") + branch=$(current_branch "$dir") + [ -z "$branch" ] && branch="(detached)" + printf "%-6s %-25s %s\n" "$id" "$branch" "$dir" + done | sort -t' ' -k1 -n fi + + echo "" + echo "💡 Tip: Use 'gtr list --porcelain' for machine-readable output" } -# IDs command (machine-friendly output for completions) -cmd_ids() { +# Clean command (remove prunable worktrees) +cmd_clean() { + log_step "Cleaning up stale worktrees..." + + # Run git worktree prune + if git worktree prune 2>/dev/null; then + log_info "Pruned stale worktree administrative files" + fi + local repo_root base_dir prefix - repo_root=$(discover_repo_root) 2>/dev/null || return 0 + repo_root=$(discover_repo_root) || exit 1 base_dir=$(resolve_base_dir "$repo_root") prefix=$(cfg_default gtr.worktrees.prefix GTR_WORKTREES_PREFIX "wt-") if [ ! -d "$base_dir" ]; then + log_info "No worktrees directory to clean" return 0 fi - # Extract IDs from worktree directories - for dir in "$base_dir/${prefix}"*; do - if [ -d "$dir" ]; then - local id - id=$(basename "$dir" | sed "s/^${prefix}//") - echo "$id" + # Find and remove empty directories + local cleaned=0 + find "$base_dir" -maxdepth 1 -type d -empty 2>/dev/null | while IFS= read -r dir; do + if [ "$dir" != "$base_dir" ]; then + rmdir "$dir" 2>/dev/null && cleaned=$((cleaned + 1)) + log_info "Removed empty directory: $(basename "$dir")" + fi + done + + log_info "✅ Cleanup complete" +} + +# Doctor command (health check) +cmd_doctor() { + echo "🏥 Running gtr health check..." + echo "" + + local issues=0 + + # Check git + if command -v git >/dev/null 2>&1; then + local git_version + git_version=$(git --version) + echo "✅ Git: $git_version" + else + echo "❌ Git: not found" + issues=$((issues + 1)) + fi + + # Check repo + local repo_root + if repo_root=$(discover_repo_root 2>/dev/null); then + echo "✅ Repository: $repo_root" + + # Check worktree base dir + local base_dir prefix + base_dir=$(resolve_base_dir "$repo_root") + prefix=$(cfg_default gtr.worktrees.prefix GTR_WORKTREES_PREFIX "wt-") + + if [ -d "$base_dir" ]; then + local count + count=$(find "$base_dir" -maxdepth 1 -type d -name "${prefix}*" 2>/dev/null | wc -l | tr -d ' ') + echo "✅ Worktrees directory: $base_dir ($count worktrees)" + else + echo "ℹ️ Worktrees directory: $base_dir (not created yet)" + fi + else + echo "❌ Not in a git repository" + issues=$((issues + 1)) + fi + + # Check configured editor + local editor + editor=$(cfg_default gtr.editor.default GTR_EDITOR_DEFAULT "none") + if [ "$editor" != "none" ]; then + if command -v "$editor" >/dev/null 2>&1; then + echo "✅ Editor: $editor (found)" + else + echo "⚠️ Editor: $editor (configured but not found in PATH)" + fi + else + echo "ℹ️ Editor: none configured" + fi + + # Check configured AI tool + local ai_tool + ai_tool=$(cfg_default gtr.ai.default GTR_AI_DEFAULT "none") + if [ "$ai_tool" != "none" ]; then + # Check if adapter exists + local adapter_file="$GTR_DIR/adapters/ai/${ai_tool}.sh" + if [ -f "$adapter_file" ]; then + . "$adapter_file" + if ai_can_start 2>/dev/null; then + echo "✅ AI tool: $ai_tool (found)" + else + echo "⚠️ AI tool: $ai_tool (configured but not found in PATH)" + fi + else + echo "⚠️ AI tool: $ai_tool (adapter not found)" + fi + else + echo "ℹ️ AI tool: none configured" + fi + + # Check OS + local os + os=$(detect_os) + echo "✅ Platform: $os" + + echo "" + if [ "$issues" -eq 0 ]; then + echo "🎉 Everything looks good!" + return 0 + else + echo "⚠️ Found $issues issue(s)" + return 1 + fi +} + +# Adapter command (list available adapters) +cmd_adapter() { + echo "🔌 Available Adapters" + echo "" + + # Editor adapters + echo "📝 Editor Adapters:" + echo "" + printf "%-15s %-10s %s\n" "NAME" "STATUS" "NOTES" + printf "%-15s %-10s %s\n" "────" "──────" "─────" + + for adapter_file in "$GTR_DIR"/adapters/editor/*.sh; do + if [ -f "$adapter_file" ]; then + local adapter_name + adapter_name=$(basename "$adapter_file" .sh) + . "$adapter_file" + + if command -v "$adapter_name" >/dev/null 2>&1; then + printf "%-15s %-10s %s\n" "$adapter_name" "✅ ready" "" + else + printf "%-15s %-10s %s\n" "$adapter_name" "⚠️ missing" "Install from $adapter_name.com" + fi + fi + done + + echo "" + echo "🤖 AI Tool Adapters:" + echo "" + printf "%-15s %-10s %s\n" "NAME" "STATUS" "NOTES" + printf "%-15s %-10s %s\n" "────" "──────" "─────" + + for adapter_file in "$GTR_DIR"/adapters/ai/*.sh; do + if [ -f "$adapter_file" ]; then + local adapter_name + adapter_name=$(basename "$adapter_file" .sh) + . "$adapter_file" + + if ai_can_start 2>/dev/null; then + printf "%-15s %-10s %s\n" "$adapter_name" "✅ ready" "" + else + printf "%-15s %-10s %s\n" "$adapter_name" "⚠️ missing" "Not found in PATH" + fi fi done + + echo "" + echo "💡 Tip: Set defaults with:" + echo " gtr config set gtr.editor.default " + echo " gtr config set gtr.ai.default " } # Config command @@ -601,25 +818,46 @@ USAGE: gtr [options] COMMANDS: - create [--branch ] [--id |--auto] [--from ] - [--track auto|remote|local|none] [--open []] - [--ai []] [--no-copy] [--yes] - Create a new worktree + new [options] + Create a new worktree (auto-assigns ID by default) + --id : specify exact ID (rarely needed) + --from : create from specific ref + --track : tracking mode (auto|remote|local|none) + --editor : override default editor + --ai : override default AI tool + --no-copy: skip file copying + --no-fetch: skip git fetch + --yes: non-interactive mode + + go + Navigate to worktree (prints path for: cd "$(gtr go 2)") + Accepts either numeric ID or branch name + + open [--editor ] + Open worktree in editor or file browser + Uses gtr.editor.default if not specified - rm [--delete-branch] [--yes] + ai [--tool ] [-- args...] + Start AI coding tool in worktree + Uses gtr.ai.default if not specified + + rm [...] [options] Remove worktree(s) + --delete-branch: also delete the branch + --force: force removal (dirty worktree) + --yes: skip confirmation - cd - Change to worktree directory + ls, list [--porcelain|--ids] + List all worktrees (ID 1 = repo root) - open [--editor ] - Open worktree in editor or file browser + clean + Remove stale/prunable worktrees - ai [--tool ] [-- args...] - Start AI coding tool in worktree + doctor + Health check (verify git, editors, AI tools) - list - List all worktrees + adapter + List available editor & AI tool adapters config {get|set|unset} [value] [--global] Manage configuration @@ -631,29 +869,34 @@ COMMANDS: Show this help EXAMPLES: - # Create worktree with auto ID and branch name - gtr create --branch my-feature --auto + # Simplest: create with branch name (auto-assigns ID) + gtr new my-feature - # Create worktree with specific ID - gtr create --id 3 --branch ui-fixes + # Configure defaults (one-time setup) + gtr config set gtr.editor.default cursor + gtr config set gtr.ai.default aider - # Create and open in Cursor - gtr create --branch fix --auto --open cursor + # Then use without flags (uses defaults) + gtr open 2 # Opens in cursor (from config) + gtr ai 2 # Starts aider (from config) - # Open existing worktree in VS Code - gtr open 2 --editor vscode + # Or override defaults + gtr open my-feature --editor vscode + gtr ai my-feature --tool claudecode - # Start Aider in worktree - gtr ai 2 --tool aider + # Navigate by ID or branch name + gtr go 2 + gtr go my-feature + cd "$(gtr go my-feature)" - # Configure default editor - gtr config set gtr.editor.default cursor --global + # Create and open in editor + gtr new ui-update --editor cursor --ai aider - # Enable file copying - gtr config set gtr.copy.include "**/.env.example" + # Remove with branch deletion + gtr rm 2 --delete-branch --yes - # Add post-create hook - gtr config set gtr.hook.postCreate "npm install" + # Health check + gtr doctor CONFIGURATION: gtr.worktrees.dir Worktrees base directory diff --git a/completions/_gtr b/completions/_gtr index f4f2c2a..f64f2b7 100644 --- a/completions/_gtr +++ b/completions/_gtr @@ -4,55 +4,67 @@ _gtr() { local -a commands commands=( - 'create:Create a new worktree' + 'new:Create a new worktree' + 'go:Navigate to worktree' 'rm:Remove worktree(s)' - 'remove:Remove worktree(s)' - 'cd:Change to worktree directory' - 'open:Open worktree in editor or file browser' - 'ai:Start AI coding tool in worktree' + 'open:Open worktree in editor' + 'ai:Start AI coding tool' + 'ls:List all worktrees' 'list:List all worktrees' + 'clean:Remove stale worktrees' + 'doctor:Health check' + 'adapter:List available adapters' 'config:Manage configuration' 'version:Show version' 'help:Show help' ) - local -a worktree_ids - # Use gtr ids command for config-aware completion - worktree_ids=(${(f)"$(command gtr ids 2>/dev/null)"}) + local -a worktree_ids branches all_options + # Use gtr list --ids for config-aware completion + worktree_ids=(${(f)"$(command gtr list --ids 2>/dev/null)"}) + # Get branch names + branches=(${(f)"$(git branch --format='%(refname:short)' 2>/dev/null)"}) + # Combine IDs and branches + all_options=("${worktree_ids[@]}" "${branches[@]}") if (( CURRENT == 2 )); then _describe 'commands' commands elif (( CURRENT == 3 )); then case "$words[2]" in - cd|open|ai|rm|remove) - _describe 'worktree IDs' worktree_ids + go|open|ai|rm) + _describe 'worktree IDs or branches' all_options ;; - open) - _arguments \ - '--editor[Editor]:editor:(cursor vscode zed)' - ;; - ai) + new) _arguments \ - '--tool[AI tool]:tool:(aider claudecode codex cursor continue)' - ;; - create) - _arguments \ - '--branch[Branch name]:branch:' \ '--id[Worktree ID]:id:' \ - '--auto[Auto-assign ID]' \ '--from[Base ref]:ref:' \ '--track[Track mode]:mode:(auto remote local none)' \ - '--open[Editor]:editor:(cursor vscode zed)' \ + '--editor[Editor]:editor:(cursor vscode zed)' \ '--ai[AI tool]:tool:(aider claudecode codex cursor continue)' \ '--no-copy[Skip file copying]' \ + '--no-fetch[Skip git fetch]' \ '--yes[Non-interactive mode]' ;; config) _values 'config action' get set unset ;; esac - elif (( CURRENT == 4 )); then + elif (( CURRENT >= 4 )); then case "$words[2]" in + open) + _arguments \ + '--editor[Editor]:editor:(cursor vscode zed)' + ;; + ai) + _arguments \ + '--tool[AI tool]:tool:(aider claudecode codex cursor continue)' + ;; + rm) + _arguments \ + '--delete-branch[Delete branch]' \ + '--force[Force removal even if dirty]' \ + '--yes[Non-interactive mode]' + ;; config) case "$words[3]" in get|set|unset) diff --git a/completions/gtr.bash b/completions/gtr.bash index bdca9c3..b09c09b 100644 --- a/completions/gtr.bash +++ b/completions/gtr.bash @@ -9,38 +9,48 @@ _gtr_completion() { # Complete commands on first argument if [ "$cword" -eq 1 ]; then - COMPREPLY=($(compgen -W "create rm remove cd open ai list config help version" -- "$cur")) + COMPREPLY=($(compgen -W "new go open ai rm ls list clean doctor adapter config help version" -- "$cur")) return 0 fi - # Commands that take worktree IDs + # Commands that take worktree IDs or branch names case "$cmd" in - cd|open|ai|rm|remove) + go|open|ai|rm) if [ "$cword" -eq 2 ]; then - # Use gtr ids command for config-aware completion - local ids - ids=$(command gtr ids 2>/dev/null || true) - COMPREPLY=($(compgen -W "$ids" -- "$cur")) + # Complete with both IDs and branch names + local ids branches all_options + ids=$(command gtr list --ids 2>/dev/null || true) + branches=$(git branch --format='%(refname:short)' 2>/dev/null || true) + all_options="$ids $branches" + COMPREPLY=($(compgen -W "$all_options" -- "$cur")) + elif [[ "$cur" == -* ]]; then + case "$cmd" in + rm) + COMPREPLY=($(compgen -W "--delete-branch --force --yes" -- "$cur")) + ;; + open) + COMPREPLY=($(compgen -W "--editor" -- "$cur")) + ;; + ai) + COMPREPLY=($(compgen -W "--tool" -- "$cur")) + ;; + esac + elif [ "$prev" = "--editor" ]; then + COMPREPLY=($(compgen -W "cursor vscode zed" -- "$cur")) + elif [ "$prev" = "--tool" ]; then + COMPREPLY=($(compgen -W "aider claudecode codex cursor continue" -- "$cur")) fi ;; - create) + new) # Complete flags if [[ "$cur" == -* ]]; then - COMPREPLY=($(compgen -W "--branch --id --auto --from --track --open --ai --no-copy --yes" -- "$cur")) - fi - ;; - open) - if [[ "$cur" == -* ]]; then - COMPREPLY=($(compgen -W "--editor" -- "$cur")) + COMPREPLY=($(compgen -W "--id --from --track --editor --ai --no-copy --no-fetch --yes" -- "$cur")) elif [ "$prev" = "--editor" ]; then COMPREPLY=($(compgen -W "cursor vscode zed" -- "$cur")) - fi - ;; - ai) - if [[ "$cur" == -* ]]; then - COMPREPLY=($(compgen -W "--tool" -- "$cur")) - elif [ "$prev" = "--tool" ]; then + elif [ "$prev" = "--ai" ]; then COMPREPLY=($(compgen -W "aider claudecode codex cursor continue" -- "$cur")) + elif [ "$prev" = "--track" ]; then + COMPREPLY=($(compgen -W "auto remote local none" -- "$cur")) fi ;; config) diff --git a/completions/gtr.fish b/completions/gtr.fish index 82327f8..929748b 100644 --- a/completions/gtr.fish +++ b/completions/gtr.fish @@ -1,31 +1,34 @@ # Fish completion for gtr # Commands -complete -c gtr -f -n "__fish_use_subcommand" -a "create" -d "Create a new worktree" +complete -c gtr -f -n "__fish_use_subcommand" -a "new" -d "Create a new worktree" +complete -c gtr -f -n "__fish_use_subcommand" -a "go" -d "Navigate to worktree" complete -c gtr -f -n "__fish_use_subcommand" -a "rm" -d "Remove worktree(s)" -complete -c gtr -f -n "__fish_use_subcommand" -a "remove" -d "Remove worktree(s)" -complete -c gtr -f -n "__fish_use_subcommand" -a "cd" -d "Change to worktree directory" complete -c gtr -f -n "__fish_use_subcommand" -a "open" -d "Open worktree in editor" complete -c gtr -f -n "__fish_use_subcommand" -a "ai" -d "Start AI coding tool" +complete -c gtr -f -n "__fish_use_subcommand" -a "ls" -d "List all worktrees" complete -c gtr -f -n "__fish_use_subcommand" -a "list" -d "List all worktrees" +complete -c gtr -f -n "__fish_use_subcommand" -a "clean" -d "Remove stale worktrees" +complete -c gtr -f -n "__fish_use_subcommand" -a "doctor" -d "Health check" +complete -c gtr -f -n "__fish_use_subcommand" -a "adapter" -d "List available adapters" complete -c gtr -f -n "__fish_use_subcommand" -a "config" -d "Manage configuration" complete -c gtr -f -n "__fish_use_subcommand" -a "version" -d "Show version" complete -c gtr -f -n "__fish_use_subcommand" -a "help" -d "Show help" -# Create command options -complete -c gtr -n "__fish_seen_subcommand_from create" -l branch -d "Branch name" -r -complete -c gtr -n "__fish_seen_subcommand_from create" -l id -d "Worktree ID" -r -complete -c gtr -n "__fish_seen_subcommand_from create" -l auto -d "Auto-assign ID" -complete -c gtr -n "__fish_seen_subcommand_from create" -l from -d "Base ref" -r -complete -c gtr -n "__fish_seen_subcommand_from create" -l track -d "Track mode" -r -a "auto remote local none" -complete -c gtr -n "__fish_seen_subcommand_from create" -l open -d "Open in editor" -r -a "cursor vscode zed" -complete -c gtr -n "__fish_seen_subcommand_from create" -l ai -d "AI tool" -r -a "aider claudecode codex cursor continue" -complete -c gtr -n "__fish_seen_subcommand_from create" -l no-copy -d "Skip file copying" -complete -c gtr -n "__fish_seen_subcommand_from create" -l yes -d "Non-interactive mode" +# New command options +complete -c gtr -n "__fish_seen_subcommand_from new" -l id -d "Worktree ID (rarely needed)" -r +complete -c gtr -n "__fish_seen_subcommand_from new" -l from -d "Base ref" -r +complete -c gtr -n "__fish_seen_subcommand_from new" -l track -d "Track mode" -r -a "auto remote local none" +complete -c gtr -n "__fish_seen_subcommand_from new" -l editor -d "Override default editor" -r -a "cursor vscode zed" +complete -c gtr -n "__fish_seen_subcommand_from new" -l ai -d "Override default AI tool" -r -a "aider claudecode codex cursor continue" +complete -c gtr -n "__fish_seen_subcommand_from new" -l no-copy -d "Skip file copying" +complete -c gtr -n "__fish_seen_subcommand_from new" -l no-fetch -d "Skip git fetch" +complete -c gtr -n "__fish_seen_subcommand_from new" -l yes -d "Non-interactive mode" # Remove command options -complete -c gtr -n "__fish_seen_subcommand_from rm remove" -l delete-branch -d "Delete branch" -complete -c gtr -n "__fish_seen_subcommand_from rm remove" -l yes -d "Non-interactive mode" +complete -c gtr -n "__fish_seen_subcommand_from rm" -l delete-branch -d "Delete branch" +complete -c gtr -n "__fish_seen_subcommand_from rm" -l force -d "Force removal even if dirty" +complete -c gtr -n "__fish_seen_subcommand_from rm" -l yes -d "Non-interactive mode" # Open command options complete -c gtr -n "__fish_seen_subcommand_from open" -l editor -d "Editor name" -r -a "cursor vscode zed" @@ -48,11 +51,13 @@ complete -c gtr -n "__fish_seen_subcommand_from config; and __fish_seen_subcomma gtr.hook.postRemove\t'Post-remove hook' " -# Helper function to get worktree IDs -function __gtr_worktree_ids - # Use gtr ids command for config-aware completion - command gtr ids 2>/dev/null +# Helper function to get worktree IDs and branch names +function __gtr_worktree_ids_and_branches + # Get worktree IDs + command gtr list --ids 2>/dev/null + # Get branch names + git branch --format='%(refname:short)' 2>/dev/null end -# Complete worktree IDs for commands that need them -complete -c gtr -n "__fish_seen_subcommand_from cd open ai rm remove" -f -a "(__gtr_worktree_ids)" +# Complete worktree IDs and branch names for commands that need them +complete -c gtr -n "__fish_seen_subcommand_from go open ai rm" -f -a "(__gtr_worktree_ids_and_branches)" diff --git a/lib/core.sh b/lib/core.sh index f190daa..21ebc63 100644 --- a/lib/core.sh +++ b/lib/core.sh @@ -100,9 +100,70 @@ current_branch() { (cd "$worktree_path" && git branch --show-current 2>/dev/null) || true } +# Resolve a worktree target from ID or branch name +# Usage: resolve_target identifier repo_root base_dir prefix +# Returns: tab-separated "id\tpath\tbranch" on success +# Exit code: 0 on success, 1 if not found +resolve_target() { + local identifier="$1" + local repo_root="$2" + local base_dir="$3" + local prefix="$4" + local id path branch + + # Check if identifier is numeric (ID) or a branch name + if echo "$identifier" | grep -qE '^[0-9]+$'; then + # Numeric ID + id="$identifier" + + if [ "$id" = "1" ]; then + # ID 1 is always the repo root + path="$repo_root" + branch=$(git -C "$repo_root" branch --show-current 2>/dev/null) + printf "%s\t%s\t%s\n" "$id" "$path" "$branch" + return 0 + fi + + # Other IDs map to worktree directories + path="$base_dir/${prefix}${id}" + if [ ! -d "$path" ]; then + log_error "Worktree not found: ${prefix}${id}" + return 1 + fi + branch=$(current_branch "$path") + printf "%s\t%s\t%s\n" "$id" "$path" "$branch" + return 0 + else + # Branch name - search for matching worktree + # First check if it's the current branch in repo root + branch=$(git -C "$repo_root" branch --show-current 2>/dev/null) + if [ "$branch" = "$identifier" ]; then + printf "1\t%s\t%s\n" "$repo_root" "$identifier" + return 0 + fi + + # Search worktree directories for matching branch + if [ -d "$base_dir" ]; then + for dir in "$base_dir/${prefix}"*; do + [ -d "$dir" ] || continue + branch=$(current_branch "$dir") + if [ "$branch" = "$identifier" ]; then + id=$(basename "$dir" | sed "s/^${prefix}//") + printf "%s\t%s\t%s\n" "$id" "$dir" "$branch" + return 0 + fi + done + fi + + log_error "Worktree not found for branch: $identifier" + return 1 + fi +} + # Create a new git worktree -# Usage: create_worktree base_dir prefix id branch_name from_ref track_mode +# Usage: create_worktree base_dir prefix id branch_name from_ref track_mode [skip_fetch] # track_mode: auto, remote, local, or none +# skip_fetch: 0 (default, fetch) or 1 (skip) create_worktree() { local base_dir="$1" local prefix="$2" @@ -110,6 +171,7 @@ create_worktree() { local branch_name="$4" local from_ref="$5" local track_mode="${6:-auto}" + local skip_fetch="${7:-0}" local worktree_path="$base_dir/${prefix}${id}" # Check if worktree already exists @@ -121,9 +183,11 @@ create_worktree() { # Create base directory if needed mkdir -p "$base_dir" - # Fetch latest refs - log_step "Fetching remote branches..." - git fetch origin 2>/dev/null || log_warn "Could not fetch from origin" + # Fetch latest refs (unless --no-fetch) + if [ "$skip_fetch" -eq 0 ]; then + log_step "Fetching remote branches..." + git fetch origin 2>/dev/null || log_warn "Could not fetch from origin" + fi local remote_exists=0 local local_exists=0 @@ -177,11 +241,18 @@ create_worktree() { ;; auto|*) - # Auto-detect best option - if [ "$remote_exists" -eq 1 ]; then + # Auto-detect best option with proper tracking + if [ "$remote_exists" -eq 1 ] && [ "$local_exists" -eq 0 ]; then + # Remote exists, no local branch - create local with tracking log_step "Branch '$branch_name' exists on remote" - if git worktree add "$worktree_path" -b "$branch_name" "origin/$branch_name" 2>/dev/null || \ - git worktree add "$worktree_path" "$branch_name" 2>/dev/null; then + + # Create tracking branch first for explicit upstream configuration + if git branch --track "$branch_name" "origin/$branch_name" 2>/dev/null; then + log_info "Created local branch tracking origin/$branch_name" + fi + + # Now add worktree using the tracking branch + if git worktree add "$worktree_path" "$branch_name" 2>/dev/null; then log_info "Worktree created tracking origin/$branch_name" printf "%s" "$worktree_path" return 0 @@ -212,13 +283,19 @@ create_worktree() { # Usage: remove_worktree worktree_path remove_worktree() { local worktree_path="$1" + local force="${2:-0}" if [ ! -d "$worktree_path" ]; then log_error "Worktree not found at $worktree_path" return 1 fi - if git worktree remove "$worktree_path" 2>/dev/null; then + local force_flag="" + if [ "$force" -eq 1 ]; then + force_flag="--force" + fi + + if git worktree remove $force_flag "$worktree_path" 2>/dev/null; then log_info "Worktree removed: $worktree_path" return 0 else From 26450e2c01b2a0a321ee85d444d4cdf493ee1929 Mon Sep 17 00:00:00 2001 From: Hans Elizaga Date: Fri, 3 Oct 2025 19:34:13 -0700 Subject: [PATCH 03/19] Enhance README.md with detailed usage instructions and examples - Added a new section explaining how `gtr` works with multiple repositories, emphasizing repository-scoped worktrees and IDs. - Updated command examples to reflect the ability to use branch names instead of just IDs for commands like `gtr rm` and `gtr open`. - Clarified prerequisites for using `gtr` by specifying the need to navigate into a git repository first. - Improved overall structure and clarity of the README to better guide users in utilizing `gtr` effectively. --- README.md | 99 +++++++++++++++++++++++++++++++++++++++++++++---------- bin/gtr | 64 ++++++++++++++++++++++------------- 2 files changed, 122 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 9052445..15a6998 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,26 @@ `gtr` makes it simple to create, manage, and work with [git worktrees](https://git-scm.com/docs/git-worktree), enabling you to work on multiple branches simultaneously without stashing or switching contexts. +## How It Works + +**gtr is repository-scoped** - each git repository has its own independent set of worktrees: + +- Run `gtr` commands from within any git repository +- Each repo has separate worktree IDs (starting at 2, ID 1 is the main repo) +- IDs are local to each repo - no conflicts across projects +- Switch repos with `cd`, then run `gtr` commands for that repo + +**Example - Working across multiple repos:** +```bash +cd ~/GitHub/frontend +gtr new auth-feature # Creates frontend worktree (ID 2) +gtr list # Shows only frontend worktrees + +cd ~/GitHub/backend +gtr new auth-api # Creates backend worktree (also ID 2 - different repo!) +gtr list # Shows only backend worktrees +``` + ## Why Git Worktrees? Git worktrees let you check out multiple branches at once in separate directories. This is invaluable when you need to: @@ -21,19 +41,20 @@ While `git worktree` is powerful, it requires remembering paths and manually set | Task | With `git worktree` | With `gtr` | | ------------------ | ------------------------------------------ | ---------------------------------- | | Create worktree | `git worktree add ../repo-feature feature` | `gtr new feature` | -| Open in editor | `cd ../repo-feature && cursor .` | `gtr open 2` | -| Start AI tool | `cd ../repo-feature && aider` | `gtr ai 2` | +| Open in editor | `cd ../repo-feature && cursor .` | `gtr open feature` | +| Start AI tool | `cd ../repo-feature && aider` | `gtr ai feature` | | Copy config files | Manual copy/paste | Auto-copy via `gtr.copy.include` | | Run build steps | Manual `npm install && npm run build` | Auto-run via `gtr.hook.postCreate` | | List worktrees | `git worktree list` (shows paths) | `gtr list` (shows IDs + status) | -| Switch to worktree | `cd ../repo-feature` | `cd "$(gtr go 2)"` | -| Clean up | `git worktree remove ../repo-feature` | `gtr rm 2` | +| Switch to worktree | `cd ../repo-feature` | `cd "$(gtr go feature)"` | +| Clean up | `git worktree remove ../repo-feature` | `gtr rm feature` | **TL;DR:** `gtr` wraps `git worktree` with quality-of-life features for modern development workflows (AI tools, editors, automation). ## Features - 🚀 **Simple commands** - Create and manage worktrees with intuitive CLI +- 📁 **Repository-scoped** - Each repo has independent worktrees and IDs - 🔧 **Configurable** - Git-config based settings, no YAML/TOML parsers needed - 🎨 **Editor integration** - Open worktrees in Cursor, VS Code, or Zed - 🤖 **AI tool support** - Launch Aider or other AI coding tools @@ -82,19 +103,24 @@ ln -s /path/to/git-worktree-runner/completions/gtr.fish ~/.config/fish/completio ## Quick Start +**Prerequisites:** `cd` into a git repository first. + **Basic workflow (no flags needed):** ```bash -# One-time setup +# Navigate to your git repo +cd ~/GitHub/my-project + +# One-time setup (per repository) gtr config set gtr.editor.default cursor gtr config set gtr.ai.default aider # Daily use - simple, no flags -gtr new my-feature # Create worktree (auto-assigns ID) -gtr list # See all worktrees -gtr open my-feature # Open in cursor (from config) +gtr new my-feature # Create worktree (auto-opens in cursor) +gtr list # See all worktrees in this repo +gtr open my-feature # Open in cursor again if needed gtr ai my-feature # Start aider (from config) cd "$(gtr go my-feature)" # Navigate to worktree -gtr rm 2 # Remove when done +gtr rm my-feature # Remove when done ``` **Advanced examples:** @@ -215,10 +241,10 @@ gtr ai 2 --tool aider -- --model gpt-5 ### `gtr rm` -Remove worktree(s). +Remove worktree(s). Accepts either ID or branch name. ```bash -gtr rm [...] [options] +gtr rm [...] [options] Options: --delete-branch Also delete the branch @@ -229,17 +255,20 @@ Options: **Examples:** ```bash -# Remove single worktree +# Remove by branch name +gtr rm my-feature + +# Remove by ID gtr rm 2 # Remove and delete branch -gtr rm 2 --delete-branch +gtr rm my-feature --delete-branch # Remove multiple worktrees -gtr rm 2 3 4 --yes +gtr rm feature-a feature-b hotfix --yes # Force remove with uncommitted changes -gtr rm 2 --force +gtr rm my-feature --force ``` ### `gtr list` @@ -334,9 +363,9 @@ gtr.ai.default = none gtr config set gtr.ai.default aider --global # Use specific tools per worktree -gtr ai 2 --tool claudecode -- --plan "refactor auth" -gtr ai 3 --tool aider -- --model gpt-5 -gtr ai 4 --tool continue -- --headless +gtr ai auth-feature --tool claudecode -- --plan "refactor auth" +gtr ai ui-redesign --tool aider -- --model gpt-5 +gtr ai performance --tool continue -- --headless ``` ### File Copying @@ -447,6 +476,40 @@ gtr new pr/123 --id 3 --editor cursor cd "$(gtr go 1)" # ID 1 is always the repo root ``` +### Working with Multiple Repositories + +Each repository has its own independent set of worktrees and IDs. Switch repos with `cd`: + +```bash +# Frontend repo +cd ~/GitHub/frontend +gtr list +# ID BRANCH PATH +# 1 main ~/GitHub/frontend +# 2 auth-feature ~/GitHub/frontend-worktrees/wt-2 +# 3 nav-redesign ~/GitHub/frontend-worktrees/wt-3 + +gtr open auth-feature # Open frontend auth work +gtr ai nav-redesign # AI on frontend nav work + +# Backend repo (separate worktrees, separate IDs) +cd ~/GitHub/backend +gtr list +# ID BRANCH PATH +# 1 main ~/GitHub/backend +# 2 api-auth ~/GitHub/backend-worktrees/wt-2 # Different ID 2! +# 5 websockets ~/GitHub/backend-worktrees/wt-5 + +gtr open api-auth # Open backend auth work +gtr ai websockets # AI on backend websockets + +# Switch back to frontend +cd ~/GitHub/frontend +gtr open auth-feature # Opens frontend auth (use branch names!) +``` + +**Key point:** IDs are per-repository, not global. ID 2 in frontend ≠ ID 2 in backend. + ### Custom Workflows with Hooks Create a `.gtr-setup.sh` in your repo: diff --git a/bin/gtr b/bin/gtr index d0dfb97..5f22229 100755 --- a/bin/gtr +++ b/bin/gtr @@ -209,7 +209,12 @@ cmd_create() { WORKTREE_PATH="$worktree_path" \ BRANCH="$branch_name" - # Open in editor if requested + # Open in editor if requested or if default is configured + if [ -z "$editor" ]; then + # No explicit --editor flag, check for default + editor=$(cfg_default gtr.editor.default GTR_EDITOR_DEFAULT "none") + fi + if [ -n "$editor" ] && [ "$editor" != "none" ]; then load_editor_adapter "$editor" if editor_can_open; then @@ -218,7 +223,12 @@ cmd_create() { fi fi - # Start AI tool if requested + # Start AI tool if requested or if default is configured + if [ -z "$ai_tool" ]; then + # No explicit --ai flag, check for default + ai_tool=$(cfg_default gtr.ai.default GTR_AI_DEFAULT "none") + fi + if [ -n "$ai_tool" ] && [ "$ai_tool" != "none" ]; then load_ai_adapter "$ai_tool" if ai_can_start; then @@ -239,7 +249,7 @@ cmd_remove() { local delete_branch=0 local yes_mode=0 local force=0 - local worktree_ids="" + local identifiers="" # Parse flags while [ $# -gt 0 ]; do @@ -261,14 +271,14 @@ cmd_remove() { exit 1 ;; *) - worktree_ids="$worktree_ids $1" + identifiers="$identifiers $1" shift ;; esac done - if [ -z "$worktree_ids" ]; then - log_error "Usage: gtr rm [...] [--delete-branch] [--force] [--yes]" + if [ -z "$identifiers" ]; then + log_error "Usage: gtr rm [...] [--delete-branch] [--force] [--yes]" exit 1 fi @@ -277,19 +287,21 @@ cmd_remove() { base_dir=$(resolve_base_dir "$repo_root") prefix=$(cfg_default gtr.worktrees.prefix GTR_WORKTREES_PREFIX "wt-") - for worktree_id in $worktree_ids; do - local worktree_path="$base_dir/${prefix}${worktree_id}" - - log_step "Removing worktree: ${prefix}${worktree_id}" - - if [ ! -d "$worktree_path" ]; then - log_warn "Worktree not found: ${prefix}${worktree_id}" + for identifier in $identifiers; do + # Resolve target (supports both ID and branch name) + local target worktree_id worktree_path branch_name + target=$(resolve_target "$identifier" "$repo_root" "$base_dir" "$prefix") || continue + worktree_id=$(echo "$target" | cut -f1) + worktree_path=$(echo "$target" | cut -f2) + branch_name=$(echo "$target" | cut -f3) + + # Cannot remove ID 1 (main repository) + if [ "$worktree_id" = "1" ]; then + log_error "Cannot remove main repository (ID 1)" continue fi - # Get branch name before removal - local branch - branch=$(current_branch "$worktree_path") + log_step "Removing worktree: ${prefix}${worktree_id} (branch: $branch_name)" # Remove the worktree if ! remove_worktree "$worktree_path" "$force"; then @@ -297,13 +309,13 @@ cmd_remove() { fi # Handle branch deletion - if [ -n "$branch" ]; then + if [ -n "$branch_name" ]; then if [ "$delete_branch" -eq 1 ]; then - if [ "$yes_mode" -eq 1 ] || prompt_yes_no "Also delete branch '$branch'?"; then - if git branch -D "$branch" 2>/dev/null; then - log_info "Branch deleted: $branch" + if [ "$yes_mode" -eq 1 ] || prompt_yes_no "Also delete branch '$branch_name'?"; then + if git branch -D "$branch_name" 2>/dev/null; then + log_info "Branch deleted: $branch_name" else - log_warn "Could not delete branch: $branch" + log_warn "Could not delete branch: $branch_name" fi fi fi @@ -313,7 +325,7 @@ cmd_remove() { run_hooks postRemove \ REPO_ROOT="$repo_root" \ WORKTREE_PATH="$worktree_path" \ - BRANCH="$branch" + BRANCH="$branch_name" done } @@ -814,7 +826,10 @@ cmd_help() { cat <<'EOF' gtr - Git worktree runner +Run from within a git repository. Each repo has independent worktrees and IDs. + USAGE: + cd ~/your-repo # Navigate to git repo first gtr [options] COMMANDS: @@ -869,10 +884,13 @@ COMMANDS: Show this help EXAMPLES: + # Navigate to your git repo first + cd ~/GitHub/my-project + # Simplest: create with branch name (auto-assigns ID) gtr new my-feature - # Configure defaults (one-time setup) + # Configure defaults (one-time setup per repo) gtr config set gtr.editor.default cursor gtr config set gtr.ai.default aider From dd2df321bd9879fe148021bfe0420eb92a6c6e5b Mon Sep 17 00:00:00 2001 From: Hans Elizaga Date: Fri, 3 Oct 2025 20:08:27 -0700 Subject: [PATCH 04/19] Add CHANGELOG.md for version 1.0.0 and update README.md with bash-completion installation instructions - Created CHANGELOG.md to document notable changes and adhere to semantic versioning. - Updated README.md to include installation instructions for bash-completion on macOS and Ubuntu/Debian. - Modified setup-example.sh to reflect the new command syntax for creating worktrees. --- CHANGELOG.md | 41 ++++++++++++++++++++++++++++++++++++++ README.md | 11 +++++++++- templates/setup-example.sh | 2 +- 3 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8a3b40e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,41 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.0] - 2025-01-XX + +### Added + +- **Repository-scoped worktrees** - Each git repo has independent worktrees and IDs +- **Auto-ID allocation** - Worktrees auto-assigned starting from configurable `gtr.worktrees.startId` (default: 2) +- **Branch-name-first UX** - All commands accept either numeric IDs or branch names (`gtr open my-feature`) +- **Auto-open/Auto-AI on create** - When `gtr.editor.default` or `gtr.ai.default` are configured, `gtr new` automatically opens the editor and/or starts AI tool +- **Editor adapters** - Support for Cursor, VS Code, and Zed +- **AI tool adapters** - Support for Aider, Claude Code, Continue, Codex, and Cursor AI +- **Smart file copying** - Selectively copy files to new worktrees via `gtr.copy.include` and `gtr.copy.exclude` globs +- **Hooks system** - Run custom commands after worktree creation (`gtr.hook.postCreate`) and removal (`gtr.hook.postRemove`) +- **Shell completions** - Tab completion for Bash, Zsh, and Fish +- **Cross-platform support** - Works on macOS, Linux, and Windows (Git Bash/WSL) +- **Utility commands**: + - `gtr new ` - Create worktree with smart branch tracking + - `gtr go ` - Navigate to worktree (shell integration) + - `gtr open ` - Open in editor + - `gtr ai ` - Start AI coding tool + - `gtr rm ` - Remove worktree(s) + - `gtr list` - List all worktrees with human/machine-readable output + - `gtr clean` - Remove stale worktrees + - `gtr doctor` - Health check for git, editors, and AI tools + - `gtr adapter` - List available editor/AI adapters + - `gtr config` - Manage git-config based settings + +### Technical + +- **POSIX-sh compliance** - Pure shell script with zero external dependencies beyond git +- **Modular architecture** - Clean separation of core, config, platform, UI, copy, and hooks logic +- **Adapter pattern** - Pluggable editor and AI tool integrations +- **Stream separation** - Data to stdout, messages to stderr for composability + +[1.0.0]: https://github.com/anthropics/git-worktree-runner/releases/tag/v1.0.0 diff --git a/README.md b/README.md index 15a6998..6fcd6a9 100644 --- a/README.md +++ b/README.md @@ -83,10 +83,19 @@ source ~/.zshrc ### Shell Completions (Optional) -**Bash:** +**Bash** (requires `bash-completion` v2): ```bash +# Install bash-completion first (if not already installed) +# macOS: +brew install bash-completion@2 + +# Ubuntu/Debian: +sudo apt install bash-completion + +# Then enable gtr completions: echo 'source /path/to/git-worktree-runner/completions/gtr.bash' >> ~/.bashrc +source ~/.bashrc ``` **Zsh:** diff --git a/templates/setup-example.sh b/templates/setup-example.sh index fd2a7e9..c5841e0 100755 --- a/templates/setup-example.sh +++ b/templates/setup-example.sh @@ -33,4 +33,4 @@ git config --local gtr.defaultBranch "auto" echo "✅ gtr configured!" echo "" echo "View config with: git config --local --list | grep gtr" -echo "Create a worktree with: gtr create --branch my-feature --auto" +echo "Create a worktree with: gtr new my-feature" From d47b5fe160d41f955cd437df5ec2e65f6ff50770 Mon Sep 17 00:00:00 2001 From: Hans Elizaga Date: Fri, 3 Oct 2025 20:25:37 -0700 Subject: [PATCH 05/19] Refactor README.md and CHANGELOG.md for clarity and command updates - Updated CHANGELOG.md to include new explicit command design and config-based defaults. - Revised README.md to simplify the basic workflow, emphasizing explicit commands and removing unnecessary flags. - Added examples for chaining commands and clarified usage of AI tools and editors. - Introduced a new AI adapter script for Claude Code to enhance integration with the gtr tool. --- CHANGELOG.md | 3 +- README.md | 127 +++++++++--------- adapters/ai/{claudecode.sh => claude.sh} | 0 bin/gtr | 162 ++++++----------------- 4 files changed, 101 insertions(+), 191 deletions(-) rename adapters/ai/{claudecode.sh => claude.sh} (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a3b40e..fedead8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Repository-scoped worktrees** - Each git repo has independent worktrees and IDs - **Auto-ID allocation** - Worktrees auto-assigned starting from configurable `gtr.worktrees.startId` (default: 2) - **Branch-name-first UX** - All commands accept either numeric IDs or branch names (`gtr open my-feature`) -- **Auto-open/Auto-AI on create** - When `gtr.editor.default` or `gtr.ai.default` are configured, `gtr new` automatically opens the editor and/or starts AI tool +- **Explicit command design** - Each command does one thing (`new` creates, `open` opens, `ai` starts AI). No auto-behavior or override flags +- **Config-based defaults** - Set `gtr.editor.default` and `gtr.ai.default` once, use everywhere without flags - **Editor adapters** - Support for Cursor, VS Code, and Zed - **AI tool adapters** - Support for Aider, Claude Code, Continue, Codex, and Cursor AI - **Smart file copying** - Selectively copy files to new worktrees via `gtr.copy.include` and `gtr.copy.exclude` globs diff --git a/README.md b/README.md index 6fcd6a9..e3dcb33 100644 --- a/README.md +++ b/README.md @@ -114,32 +114,40 @@ ln -s /path/to/git-worktree-runner/completions/gtr.fish ~/.config/fish/completio **Prerequisites:** `cd` into a git repository first. -**Basic workflow (no flags needed):** +**Basic workflow:** ```bash # Navigate to your git repo cd ~/GitHub/my-project # One-time setup (per repository) gtr config set gtr.editor.default cursor -gtr config set gtr.ai.default aider +gtr config set gtr.ai.default claude -# Daily use - simple, no flags -gtr new my-feature # Create worktree (auto-opens in cursor) -gtr list # See all worktrees in this repo -gtr open my-feature # Open in cursor again if needed -gtr ai my-feature # Start aider (from config) -cd "$(gtr go my-feature)" # Navigate to worktree -gtr rm my-feature # Remove when done +# Daily workflow - explicit commands +gtr new my-feature # Create worktree +gtr open my-feature # Open in cursor (from config) +gtr ai my-feature # Start claude (from config) + +# Or chain them together +gtr new my-feature && gtr open my-feature && gtr ai my-feature + +# Navigate to worktree +cd "$(gtr go my-feature)" + +# List all worktrees +gtr list + +# Remove when done +gtr rm my-feature ``` -**Advanced examples:** +**Advanced:** ```bash -# Override defaults -gtr open 2 --editor vscode +# Create from specific ref gtr new hotfix --from v1.2.3 --id 99 -# Destructive operations -gtr rm 2 --delete-branch --force +# Remove with branch deletion +gtr rm my-feature --delete-branch --force ``` ## Commands @@ -155,8 +163,6 @@ Options (all optional): --id Specific worktree ID (rarely needed) --from Create from specific ref (default: main/master) --track Track mode: auto|remote|local|none - --editor Override default editor - --ai Override default AI tool --no-copy Skip file copying --no-fetch Skip git fetch --yes Non-interactive mode @@ -168,41 +174,30 @@ Options (all optional): # Create worktree (auto-assigns ID) gtr new my-feature -# Specific ID and branch -gtr new feature-x --id 2 - # Create from specific ref gtr new hotfix --from v1.2.3 -# Create and open in Cursor -gtr new ui --editor cursor - -# Create and start Aider -gtr new refactor --ai aider +# Then open and start AI +gtr open hotfix +gtr ai hotfix ``` ### `gtr open` -Open a worktree in an editor or file browser. Accepts either ID or branch name. +Open a worktree in an editor. Uses `gtr.editor.default` from config. ```bash -gtr open [options] - -Options: - --editor Editor: cursor, vscode, zed +gtr open ``` **Examples:** ```bash -# Open by ID (uses default editor from config) +# Open by ID (uses gtr.editor.default) gtr open 2 -# Open by branch name with specific editor -gtr open my-feature --editor cursor - -# Override default editor -gtr open 2 --editor zed +# Open by branch name +gtr open my-feature ``` ### `gtr go` @@ -225,27 +220,23 @@ cd "$(gtr go my-feature)" ### `gtr ai` -Start an AI coding tool in a worktree. Accepts either ID or branch name. +Start an AI coding tool in a worktree. Uses `gtr.ai.default` from config. ```bash -gtr ai [options] [-- args...] - -Options: - --tool AI tool: aider, claudecode, etc. - -- Pass remaining args to tool +gtr ai [-- args...] ``` **Examples:** ```bash -# Start default AI tool by ID (uses gtr.ai.default) +# Start AI tool by ID (uses gtr.ai.default) gtr ai 2 -# Start by branch name with specific tool -gtr ai my-feature --tool aider +# Start by branch name +gtr ai my-feature -# Start Aider with specific model -gtr ai 2 --tool aider -- --model gpt-5 +# Pass arguments to the AI tool +gtr ai my-feature -- --model gpt-4 ``` ### `gtr rm` @@ -351,30 +342,34 @@ gtr.editor.default = cursor ### AI Tool Settings ```bash -# Default AI tool: none (or aider, claudecode, codex, cursor, continue) +# Default AI tool: none (or aider, claude, codex, cursor, continue) gtr.ai.default = none ``` **Supported AI Tools:** -| Tool | Install | Use Case | Command Example | -| ------------------------------------------------- | ------------------------------------------------- | ------------------------------------ | -------------------------------------- | -| **[Aider](https://aider.chat)** | `pip install aider-chat` | Pair programming, edit files with AI | `gtr ai 2 --tool aider` | -| **[Claude Code](https://claude.com/claude-code)** | Install from claude.com | Terminal-native coding agent | `gtr ai 2 --tool claudecode` | -| **[Codex CLI](https://github.com/openai/codex)** | `npm install -g @openai/codex` | OpenAI coding assistant | `gtr ai 2 --tool codex -- "add tests"` | -| **[Cursor](https://cursor.com)** | Install from cursor.com | AI-powered editor with CLI agent | `gtr ai 2 --tool cursor` | -| **[Continue](https://continue.dev)** | See [docs](https://docs.continue.dev/cli/install) | Open-source coding agent | `gtr ai 2 --tool continue` | +| Tool | Install | Use Case | Set as Default | +| ------------------------------------------------- | ------------------------------------------------- | ------------------------------------ | ------------------------------------- | +| **[Aider](https://aider.chat)** | `pip install aider-chat` | Pair programming, edit files with AI | `gtr config set gtr.ai.default aider` | +| **[Claude Code](https://claude.com/claude-code)** | Install from claude.com | Terminal-native coding agent | `gtr config set gtr.ai.default claude` | +| **[Codex CLI](https://github.com/openai/codex)** | `npm install -g @openai/codex` | OpenAI coding assistant | `gtr config set gtr.ai.default codex` | +| **[Cursor](https://cursor.com)** | Install from cursor.com | AI-powered editor with CLI agent | `gtr config set gtr.ai.default cursor` | +| **[Continue](https://continue.dev)** | See [docs](https://docs.continue.dev/cli/install) | Open-source coding agent | `gtr config set gtr.ai.default continue` | **Examples:** ```bash -# Set default AI tool globally -gtr config set gtr.ai.default aider --global +# Set default AI tool for this repo +gtr config set gtr.ai.default claude + +# Or set globally for all repos +gtr config set gtr.ai.default claude --global -# Use specific tools per worktree -gtr ai auth-feature --tool claudecode -- --plan "refactor auth" -gtr ai ui-redesign --tool aider -- --model gpt-5 -gtr ai performance --tool continue -- --headless +# Then just use gtr ai +gtr ai my-feature + +# Pass arguments to the tool +gtr ai my-feature -- --plan "refactor auth" ``` ### File Copying @@ -466,7 +461,7 @@ git config --local --add gtr.hook.postCreate "pnpm run build" ```bash # Set global preferences git config --global gtr.editor.default cursor -git config --global gtr.ai.default aider +git config --global gtr.ai.default claude git config --global gtr.worktrees.startId 2 ``` @@ -476,10 +471,12 @@ git config --global gtr.worktrees.startId 2 ```bash # Terminal 1: Work on feature -gtr new feature-a --id 2 --editor cursor +gtr new feature-a --id 2 +gtr open feature-a # Terminal 2: Review PR -gtr new pr/123 --id 3 --editor cursor +gtr new pr/123 --id 3 +gtr open pr/123 # Terminal 3: Navigate to main branch (repo root) cd "$(gtr go 1)" # ID 1 is always the repo root @@ -578,8 +575,8 @@ command -v cursor # or: code, zed # Check configuration gtr config get gtr.editor.default -# Try opening manually -gtr open 2 --editor cursor +# Try opening again +gtr open 2 ``` ### File Copying Issues diff --git a/adapters/ai/claudecode.sh b/adapters/ai/claude.sh similarity index 100% rename from adapters/ai/claudecode.sh rename to adapters/ai/claude.sh diff --git a/bin/gtr b/bin/gtr index 5f22229..ad3a5fd 100755 --- a/bin/gtr +++ b/bin/gtr @@ -74,8 +74,6 @@ cmd_create() { local branch_name="" local from_ref="" local track_mode="auto" - local editor="" - local ai_tool="" local explicit_id=0 local skip_copy=0 local skip_fetch=0 @@ -97,30 +95,6 @@ cmd_create() { track_mode="$2" shift 2 ;; - --editor) - case "${2-}" in - --*|"") - editor="$(cfg_default gtr.editor.default GTR_EDITOR_DEFAULT none)" - shift 1 - ;; - *) - editor="$2" - shift 2 - ;; - esac - ;; - --ai) - case "${2-}" in - --*|"") - ai_tool="$(cfg_default gtr.ai.default GTR_AI_DEFAULT none)" - shift 1 - ;; - *) - ai_tool="$2" - shift 2 - ;; - esac - ;; --no-copy) skip_copy=1 shift @@ -209,39 +183,13 @@ cmd_create() { WORKTREE_PATH="$worktree_path" \ BRANCH="$branch_name" - # Open in editor if requested or if default is configured - if [ -z "$editor" ]; then - # No explicit --editor flag, check for default - editor=$(cfg_default gtr.editor.default GTR_EDITOR_DEFAULT "none") - fi - - if [ -n "$editor" ] && [ "$editor" != "none" ]; then - load_editor_adapter "$editor" - if editor_can_open; then - log_step "Opening in $editor..." - editor_open "$worktree_path" - fi - fi - - # Start AI tool if requested or if default is configured - if [ -z "$ai_tool" ]; then - # No explicit --ai flag, check for default - ai_tool=$(cfg_default gtr.ai.default GTR_AI_DEFAULT "none") - fi - - if [ -n "$ai_tool" ] && [ "$ai_tool" != "none" ]; then - load_ai_adapter "$ai_tool" - if ai_can_start; then - log_step "Starting $ai_tool..." - ai_start "$worktree_path" - fi - fi - echo "" - log_info "Worktree created successfully!" - echo "🎯 Navigate with: cd $worktree_path" - echo "📂 Open with: gtr open $worktree_id" - echo "🤖 Start AI with: gtr ai $worktree_id" + log_info "Worktree created: $worktree_path" + echo "" + echo "Next steps:" + echo " gtr open $branch_name # Open in editor" + echo " gtr ai $branch_name # Start AI tool" + echo " cd \"\$(gtr go $branch_name)\" # Navigate to worktree" } # Remove command @@ -363,36 +311,16 @@ cmd_go() { # Open command cmd_open() { - local editor="" - local identifier="" - - # Parse arguments - while [ $# -gt 0 ]; do - case "$1" in - --editor) - editor="$2" - shift 2 - ;; - -*) - log_error "Unknown flag: $1" - exit 1 - ;; - *) - identifier="$1" - shift - ;; - esac - done - - if [ -z "$identifier" ]; then - log_error "Usage: gtr open [--editor ]" + if [ $# -ne 1 ]; then + log_error "Usage: gtr open " exit 1 fi - # Get editor from config if not specified - if [ -z "$editor" ]; then - editor=$(cfg_default gtr.editor.default GTR_EDITOR_DEFAULT "none") - fi + local identifier="$1" + + # Get editor from config + local editor + editor=$(cfg_default gtr.editor.default GTR_EDITOR_DEFAULT "none") local repo_root base_dir prefix repo_root=$(discover_repo_root) || exit 1 @@ -420,17 +348,12 @@ cmd_open() { # AI command cmd_ai() { - local ai_tool="" local identifier="" local ai_args="" # Parse arguments while [ $# -gt 0 ]; do case "$1" in - --tool) - ai_tool="$2" - shift 2 - ;; --) shift ai_args="$*" @@ -450,20 +373,18 @@ cmd_ai() { done if [ -z "$identifier" ]; then - log_error "Usage: gtr ai [--tool ] [-- args...]" + log_error "Usage: gtr ai [-- args...]" exit 1 fi - # Get AI tool from config if not specified - if [ -z "$ai_tool" ]; then - ai_tool=$(cfg_default gtr.ai.default GTR_AI_DEFAULT "none") - fi + # Get AI tool from config + local ai_tool + ai_tool=$(cfg_default gtr.ai.default GTR_AI_DEFAULT "none") # Check if AI tool is configured if [ "$ai_tool" = "none" ]; then log_error "No AI tool configured" - log_info "Set default: gtr config set gtr.ai.default aider" - log_info "Or use: gtr ai $identifier --tool aider" + log_info "Set default: gtr config set gtr.ai.default claude" exit 1 fi @@ -814,7 +735,7 @@ load_ai_adapter() { if [ ! -f "$adapter_file" ]; then log_error "Unknown AI tool: $ai_tool" - log_info "Available AI tools: aider, claudecode, codex, cursor, continue" + log_info "Available AI tools: aider, claude, codex, cursor, continue" exit 1 fi @@ -838,8 +759,6 @@ COMMANDS: --id : specify exact ID (rarely needed) --from : create from specific ref --track : tracking mode (auto|remote|local|none) - --editor : override default editor - --ai : override default AI tool --no-copy: skip file copying --no-fetch: skip git fetch --yes: non-interactive mode @@ -848,15 +767,13 @@ COMMANDS: Navigate to worktree (prints path for: cd "$(gtr go 2)") Accepts either numeric ID or branch name - open [--editor ] - Open worktree in editor or file browser - Uses gtr.editor.default if not specified + open + Open worktree in editor (uses gtr.editor.default) - ai [--tool ] [-- args...] - Start AI coding tool in worktree - Uses gtr.ai.default if not specified + ai [-- args...] + Start AI coding tool in worktree (uses gtr.ai.default) - rm [...] [options] + rm [...] [options] Remove worktree(s) --delete-branch: also delete the branch --force: force removal (dirty worktree) @@ -887,31 +804,26 @@ EXAMPLES: # Navigate to your git repo first cd ~/GitHub/my-project - # Simplest: create with branch name (auto-assigns ID) - gtr new my-feature - # Configure defaults (one-time setup per repo) gtr config set gtr.editor.default cursor - gtr config set gtr.ai.default aider + gtr config set gtr.ai.default claude - # Then use without flags (uses defaults) - gtr open 2 # Opens in cursor (from config) - gtr ai 2 # Starts aider (from config) + # Typical workflow: create, open, start AI + gtr new my-feature + gtr open my-feature + gtr ai my-feature - # Or override defaults - gtr open my-feature --editor vscode - gtr ai my-feature --tool claudecode + # Or chain them together + gtr new my-feature && gtr open my-feature && gtr ai my-feature - # Navigate by ID or branch name - gtr go 2 - gtr go my-feature + # Navigate to worktree cd "$(gtr go my-feature)" - # Create and open in editor - gtr new ui-update --editor cursor --ai aider + # List all worktrees + gtr list - # Remove with branch deletion - gtr rm 2 --delete-branch --yes + # Remove when done + gtr rm my-feature --delete-branch # Health check gtr doctor @@ -922,7 +834,7 @@ CONFIGURATION: gtr.worktrees.startId Starting ID (default: 2) gtr.defaultBranch Default branch (default: auto) gtr.editor.default Default editor (cursor, vscode, zed, none) - gtr.ai.default Default AI tool (aider, claudecode, codex, cursor, continue, none) + gtr.ai.default Default AI tool (aider, claude, codex, cursor, continue, none) gtr.copy.include Files to copy (multi-valued) gtr.copy.exclude Files to exclude (multi-valued) gtr.hook.postCreate Post-create hooks (multi-valued) From e46047f150de400b9764cdf12b01ca7822bba989 Mon Sep 17 00:00:00 2001 From: Hans Elizaga Date: Fri, 3 Oct 2025 20:55:48 -0700 Subject: [PATCH 06/19] Enhance editor and config command handling in gtr - Improved editor detection by checking for an adapter script and validating its ability to open. - Updated config command to allow flexible argument parsing for action, key, and value, with support for global scope. - Removed deprecated editor and AI tool options from completion scripts to streamline user experience. --- bin/gtr | 59 ++++++++++++++++++++++++++++++++------------ completions/_gtr | 10 -------- completions/gtr.bash | 16 +----------- completions/gtr.fish | 8 ------ 4 files changed, 44 insertions(+), 49 deletions(-) diff --git a/bin/gtr b/bin/gtr index ad3a5fd..38fb696 100755 --- a/bin/gtr +++ b/bin/gtr @@ -573,10 +573,17 @@ cmd_doctor() { local editor editor=$(cfg_default gtr.editor.default GTR_EDITOR_DEFAULT "none") if [ "$editor" != "none" ]; then - if command -v "$editor" >/dev/null 2>&1; then - echo "✅ Editor: $editor (found)" + # Check if adapter exists + local editor_adapter="$GTR_DIR/adapters/editor/${editor}.sh" + if [ -f "$editor_adapter" ]; then + . "$editor_adapter" + if editor_can_open 2>/dev/null; then + echo "✅ Editor: $editor (found)" + else + echo "⚠️ Editor: $editor (configured but not found in PATH)" + fi else - echo "⚠️ Editor: $editor (configured but not found in PATH)" + echo "⚠️ Editor: $editor (adapter not found)" fi else echo "ℹ️ Editor: none configured" @@ -634,10 +641,10 @@ cmd_adapter() { adapter_name=$(basename "$adapter_file" .sh) . "$adapter_file" - if command -v "$adapter_name" >/dev/null 2>&1; then + if editor_can_open 2>/dev/null; then printf "%-15s %-10s %s\n" "$adapter_name" "✅ ready" "" else - printf "%-15s %-10s %s\n" "$adapter_name" "⚠️ missing" "Install from $adapter_name.com" + printf "%-15s %-10s %s\n" "$adapter_name" "⚠️ missing" "Not found in PATH" fi fi done @@ -670,17 +677,37 @@ cmd_adapter() { # Config command cmd_config() { - local action="${1:-get}" - local key="$2" - local value="$3" local scope="local" + local action="" key="" value="" - # Check for --global flag - if [ "$key" = "--global" ] || [ "$value" = "--global" ]; then - scope="global" - [ "$key" = "--global" ] && key="$3" && value="$4" - [ "$value" = "--global" ] && value="$4" - fi + # Parse args flexibly: action, key, value, and --global anywhere + while [ $# -gt 0 ]; do + case "$1" in + --global|global) + scope="global" + shift + ;; + get|set|unset) + action="$1" + shift + ;; + *) + if [ -z "$key" ]; then + key="$1" + shift + elif [ -z "$value" ] && [ "$action" = "set" ]; then + value="$1" + shift + else + # Unknown extra token + shift + fi + ;; + esac + done + + # Default action is get + action="${action:-get}" case "$action" in get) @@ -696,7 +723,7 @@ cmd_config() { exit 1 fi cfg_set "$key" "$value" "$scope" - log_info "Config set: $key = $value" + log_info "Config set: $key = $value ($scope)" ;; unset) if [ -z "$key" ]; then @@ -704,7 +731,7 @@ cmd_config() { exit 1 fi cfg_unset "$key" "$scope" - log_info "Config unset: $key" + log_info "Config unset: $key ($scope)" ;; *) log_error "Unknown config action: $action" diff --git a/completions/_gtr b/completions/_gtr index f64f2b7..73921ad 100644 --- a/completions/_gtr +++ b/completions/_gtr @@ -39,8 +39,6 @@ _gtr() { '--id[Worktree ID]:id:' \ '--from[Base ref]:ref:' \ '--track[Track mode]:mode:(auto remote local none)' \ - '--editor[Editor]:editor:(cursor vscode zed)' \ - '--ai[AI tool]:tool:(aider claudecode codex cursor continue)' \ '--no-copy[Skip file copying]' \ '--no-fetch[Skip git fetch]' \ '--yes[Non-interactive mode]' @@ -51,14 +49,6 @@ _gtr() { esac elif (( CURRENT >= 4 )); then case "$words[2]" in - open) - _arguments \ - '--editor[Editor]:editor:(cursor vscode zed)' - ;; - ai) - _arguments \ - '--tool[AI tool]:tool:(aider claudecode codex cursor continue)' - ;; rm) _arguments \ '--delete-branch[Delete branch]' \ diff --git a/completions/gtr.bash b/completions/gtr.bash index b09c09b..4f8c590 100644 --- a/completions/gtr.bash +++ b/completions/gtr.bash @@ -28,27 +28,13 @@ _gtr_completion() { rm) COMPREPLY=($(compgen -W "--delete-branch --force --yes" -- "$cur")) ;; - open) - COMPREPLY=($(compgen -W "--editor" -- "$cur")) - ;; - ai) - COMPREPLY=($(compgen -W "--tool" -- "$cur")) - ;; esac - elif [ "$prev" = "--editor" ]; then - COMPREPLY=($(compgen -W "cursor vscode zed" -- "$cur")) - elif [ "$prev" = "--tool" ]; then - COMPREPLY=($(compgen -W "aider claudecode codex cursor continue" -- "$cur")) fi ;; new) # Complete flags if [[ "$cur" == -* ]]; then - COMPREPLY=($(compgen -W "--id --from --track --editor --ai --no-copy --no-fetch --yes" -- "$cur")) - elif [ "$prev" = "--editor" ]; then - COMPREPLY=($(compgen -W "cursor vscode zed" -- "$cur")) - elif [ "$prev" = "--ai" ]; then - COMPREPLY=($(compgen -W "aider claudecode codex cursor continue" -- "$cur")) + COMPREPLY=($(compgen -W "--id --from --track --no-copy --no-fetch --yes" -- "$cur")) elif [ "$prev" = "--track" ]; then COMPREPLY=($(compgen -W "auto remote local none" -- "$cur")) fi diff --git a/completions/gtr.fish b/completions/gtr.fish index 929748b..56bf4a3 100644 --- a/completions/gtr.fish +++ b/completions/gtr.fish @@ -19,8 +19,6 @@ complete -c gtr -f -n "__fish_use_subcommand" -a "help" -d "Show help" complete -c gtr -n "__fish_seen_subcommand_from new" -l id -d "Worktree ID (rarely needed)" -r complete -c gtr -n "__fish_seen_subcommand_from new" -l from -d "Base ref" -r complete -c gtr -n "__fish_seen_subcommand_from new" -l track -d "Track mode" -r -a "auto remote local none" -complete -c gtr -n "__fish_seen_subcommand_from new" -l editor -d "Override default editor" -r -a "cursor vscode zed" -complete -c gtr -n "__fish_seen_subcommand_from new" -l ai -d "Override default AI tool" -r -a "aider claudecode codex cursor continue" complete -c gtr -n "__fish_seen_subcommand_from new" -l no-copy -d "Skip file copying" complete -c gtr -n "__fish_seen_subcommand_from new" -l no-fetch -d "Skip git fetch" complete -c gtr -n "__fish_seen_subcommand_from new" -l yes -d "Non-interactive mode" @@ -30,12 +28,6 @@ complete -c gtr -n "__fish_seen_subcommand_from rm" -l delete-branch -d "Delete complete -c gtr -n "__fish_seen_subcommand_from rm" -l force -d "Force removal even if dirty" complete -c gtr -n "__fish_seen_subcommand_from rm" -l yes -d "Non-interactive mode" -# Open command options -complete -c gtr -n "__fish_seen_subcommand_from open" -l editor -d "Editor name" -r -a "cursor vscode zed" - -# AI command options -complete -c gtr -n "__fish_seen_subcommand_from ai" -l tool -d "AI tool name" -r -a "aider claudecode codex cursor continue" - # Config command complete -c gtr -n "__fish_seen_subcommand_from config" -f -a "get set unset" complete -c gtr -n "__fish_seen_subcommand_from config; and __fish_seen_subcommand_from get set unset" -f -a "\ From e2d1732bbe85035f11f4cf9835073705a832b9fb Mon Sep 17 00:00:00 2001 From: Hans Elizaga Date: Fri, 3 Oct 2025 21:11:08 -0700 Subject: [PATCH 07/19] Update README.md with requirements and change shell shebangs to bash for compatibility - Added a new "Requirements" section to README.md specifying Git and Bash versions needed. - Changed shebangs from `#!/bin/sh` to `#!/usr/bin/env bash` in multiple adapter and utility scripts for improved compatibility and clarity. --- README.md | 5 +++++ adapters/ai/aider.sh | 2 +- adapters/ai/claude.sh | 2 +- adapters/ai/codex.sh | 2 +- adapters/ai/continue.sh | 2 +- adapters/ai/cursor.sh | 2 +- adapters/editor/cursor.sh | 2 +- adapters/editor/vscode.sh | 2 +- adapters/editor/zed.sh | 2 +- bin/gtr | 12 +++++++----- lib/config.sh | 2 +- lib/copy.sh | 2 +- lib/core.sh | 2 +- lib/hooks.sh | 2 +- lib/platform.sh | 2 +- lib/ui.sh | 2 +- 16 files changed, 26 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index e3dcb33..588cd77 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,11 @@ While `git worktree` is powerful, it requires remembering paths and manually set - 🌍 **Cross-platform** - Works on macOS, Linux, and Windows (Git Bash) - 🎯 **Shell completions** - Tab completion for Bash, Zsh, and Fish +## Requirements + +- **Git** 2.5+ (for `git worktree` support) +- **Bash** 4.0+ (standard on macOS, Linux, and Windows Git Bash) + ## Installation ### Quick Install (macOS/Linux) diff --git a/adapters/ai/aider.sh b/adapters/ai/aider.sh index 33592b7..191c3d6 100644 --- a/adapters/ai/aider.sh +++ b/adapters/ai/aider.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/usr/bin/env bash # Aider AI coding assistant adapter # Check if Aider is available diff --git a/adapters/ai/claude.sh b/adapters/ai/claude.sh index 8ed94e2..ab6b3f0 100644 --- a/adapters/ai/claude.sh +++ b/adapters/ai/claude.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/usr/bin/env bash # Claude Code AI adapter # Check if Claude Code is available diff --git a/adapters/ai/codex.sh b/adapters/ai/codex.sh index 2176a2a..582f8c3 100644 --- a/adapters/ai/codex.sh +++ b/adapters/ai/codex.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/usr/bin/env bash # OpenAI Codex CLI adapter # Check if Codex is available diff --git a/adapters/ai/continue.sh b/adapters/ai/continue.sh index 878e646..a4672c9 100644 --- a/adapters/ai/continue.sh +++ b/adapters/ai/continue.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/usr/bin/env bash # Continue CLI adapter # Check if Continue is available diff --git a/adapters/ai/cursor.sh b/adapters/ai/cursor.sh index e56fef6..8ea910c 100644 --- a/adapters/ai/cursor.sh +++ b/adapters/ai/cursor.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/usr/bin/env bash # Cursor AI agent adapter # Check if Cursor agent/CLI is available diff --git a/adapters/editor/cursor.sh b/adapters/editor/cursor.sh index 5a1c989..68a030a 100644 --- a/adapters/editor/cursor.sh +++ b/adapters/editor/cursor.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/usr/bin/env bash # Cursor editor adapter # Check if Cursor is available diff --git a/adapters/editor/vscode.sh b/adapters/editor/vscode.sh index 847e2e6..8ac1ed0 100644 --- a/adapters/editor/vscode.sh +++ b/adapters/editor/vscode.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/usr/bin/env bash # VS Code editor adapter # Check if VS Code is available diff --git a/adapters/editor/zed.sh b/adapters/editor/zed.sh index 91b5f52..8664e8b 100644 --- a/adapters/editor/zed.sh +++ b/adapters/editor/zed.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/usr/bin/env bash # Zed editor adapter # Check if Zed is available diff --git a/bin/gtr b/bin/gtr index 38fb696..96f0e61 100755 --- a/bin/gtr +++ b/bin/gtr @@ -1,4 +1,4 @@ -#!/bin/sh +#!/usr/bin/env bash # gtr - Git worktree runner # Portable, cross-platform git worktree management @@ -467,7 +467,7 @@ cmd_list() { branch=$(current_branch "$dir") [ -z "$branch" ] && branch="(detached)" printf "%s\t%s\t%s\t%s\n" "$id" "$dir" "$branch" "ok" - done | sort -t$'\t' -k1 -n + done | LC_ALL=C sort -n -k1,1 fi return 0 fi @@ -492,7 +492,7 @@ cmd_list() { branch=$(current_branch "$dir") [ -z "$branch" ] && branch="(detached)" printf "%-6s %-25s %s\n" "$id" "$branch" "$dir" - done | sort -t' ' -k1 -n + done | LC_ALL=C sort -n -k1,1 fi echo "" @@ -522,8 +522,10 @@ cmd_clean() { local cleaned=0 find "$base_dir" -maxdepth 1 -type d -empty 2>/dev/null | while IFS= read -r dir; do if [ "$dir" != "$base_dir" ]; then - rmdir "$dir" 2>/dev/null && cleaned=$((cleaned + 1)) - log_info "Removed empty directory: $(basename "$dir")" + if rmdir "$dir" 2>/dev/null; then + cleaned=$((cleaned + 1)) + log_info "Removed empty directory: $(basename "$dir")" + fi fi done diff --git a/lib/config.sh b/lib/config.sh index f4b01f3..699cf37 100644 --- a/lib/config.sh +++ b/lib/config.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/usr/bin/env bash # Configuration management via git config # Default values are defined where they're used in lib/core.sh diff --git a/lib/copy.sh b/lib/copy.sh index 6d91e6c..db1d550 100644 --- a/lib/copy.sh +++ b/lib/copy.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/usr/bin/env bash # File copying utilities with pattern matching # Copy files matching patterns from source to destination diff --git a/lib/core.sh b/lib/core.sh index 21ebc63..13e321f 100644 --- a/lib/core.sh +++ b/lib/core.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/usr/bin/env bash # Core git worktree operations # Discover the root of the current git repository diff --git a/lib/hooks.sh b/lib/hooks.sh index 030206e..b9f2f13 100644 --- a/lib/hooks.sh +++ b/lib/hooks.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/usr/bin/env bash # Hook execution system # Run hooks for a specific phase diff --git a/lib/platform.sh b/lib/platform.sh index 3cf15b3..5aaa386 100644 --- a/lib/platform.sh +++ b/lib/platform.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/usr/bin/env bash # Platform-specific utilities # Detect operating system diff --git a/lib/ui.sh b/lib/ui.sh index 993db82..d33dbff 100644 --- a/lib/ui.sh +++ b/lib/ui.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/usr/bin/env bash # UI utilities for logging and prompting log_info() { From 7ec3e4e624a0745b2e8fe5e35802c08e8f42a3ea Mon Sep 17 00:00:00 2001 From: Hans Elizaga Date: Fri, 3 Oct 2025 21:28:08 -0700 Subject: [PATCH 08/19] Update CONTRIBUTING.md and improve script compatibility - Changed shell script shebangs from `#!/bin/sh` to `#!/usr/bin/env bash` for consistency and clarity. - Updated best practices to specify a requirement for Bash 3.2+ and allowed Bash 4.0+ features. - Enhanced the `gtr` script to resolve symlinks and allow environment variable overrides for the script directory. - Modified `copy.sh` to utilize native Bash globbing with `shopt` settings for improved file pattern matching. --- CONTRIBUTING.md | 18 ++++++++++-------- bin/gtr | 14 ++++++++++++-- lib/copy.sh | 20 +++++++++++++++----- 3 files changed, 37 insertions(+), 15 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 996cc5c..99580be 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -51,12 +51,12 @@ git-worktree-runner/ #### Shell Script Best Practices -- **POSIX compliance**: Write POSIX-compatible shell code (use `#!/bin/sh`) +- **Bash requirement**: All scripts use Bash (use `#!/usr/bin/env bash`) - **Set strict mode**: Use `set -e` to exit on errors - **Quote variables**: Always quote variables: `"$var"` - **Use local variables**: Declare function-local vars with `local` - **Error handling**: Check return codes and provide clear error messages -- **No bashisms**: Avoid bash-specific features unless absolutely necessary +- **Target Bash 3.2+**: Code runs on Bash 3.2+ (macOS default), but Bash 4.0+ features (like globstar) are allowed where appropriate #### Code Style @@ -69,8 +69,8 @@ git-worktree-runner/ #### Example: -```sh -#!/bin/sh +```bash +#!/usr/bin/env bash # Brief description of what this file does # Function description @@ -94,8 +94,8 @@ do_something() { 1. Create `adapters/editor/yourname.sh`: -```sh -#!/bin/sh +```bash +#!/usr/bin/env bash # YourEditor adapter editor_can_open() { @@ -122,8 +122,8 @@ editor_open() { 1. Create `adapters/ai/yourtool.sh`: -```sh -#!/bin/sh +```bash +#!/usr/bin/env bash # YourTool AI adapter ai_can_start() { @@ -207,6 +207,7 @@ Currently, testing is manual. Please test your changes on: ``` **Types:** + - `feat`: New feature - `fix`: Bug fix - `docs`: Documentation changes @@ -215,6 +216,7 @@ Currently, testing is manual. Please test your changes on: - `chore`: Maintenance tasks **Examples:** + ``` feat: add JetBrains IDE adapter diff --git a/bin/gtr b/bin/gtr index 96f0e61..754fdc2 100755 --- a/bin/gtr +++ b/bin/gtr @@ -7,8 +7,18 @@ set -e # Version GTR_VERSION="1.0.0" -# Find the script directory -GTR_DIR="$(cd "$(dirname "$0")/.." && pwd)" +# Find the script directory (resolve symlinks; allow env override) +resolve_script_dir() { + local src="${BASH_SOURCE[0]}" + while [ -h "$src" ]; do + local dir + dir="$(cd -P "$(dirname "$src")" && pwd)" + src="$(readlink "$src")" + [[ $src != /* ]] && src="$dir/$src" + done + cd -P "$(dirname "$src")/.." && pwd +} +: "${GTR_DIR:=$(resolve_script_dir)}" # Source library files . "$GTR_DIR/lib/ui.sh" diff --git a/lib/copy.sh b/lib/copy.sh index db1d550..3a4b348 100644 --- a/lib/copy.sh +++ b/lib/copy.sh @@ -23,14 +23,23 @@ copy_patterns() { old_pwd=$(pwd) cd "$src_root" || return 1 + # Enable globstar for ** patterns (Bash 4.0+) + # nullglob: patterns that don't match expand to nothing + # dotglob: * matches hidden files + # globstar: ** matches directories recursively + shopt -s nullglob dotglob globstar + local copied_count=0 # Process each include pattern (avoid pipeline subshell) while IFS= read -r pattern; do [ -z "$pattern" ] && continue - # Find files matching the pattern (avoid pipeline subshell) - while IFS= read -r file; do + # Use native Bash glob expansion (supports **) + for file in $pattern; do + # Skip if not a file + [ -f "$file" ] || continue + # Remove leading ./ file="${file#./}" @@ -73,13 +82,14 @@ EOF else log_warn "Failed to copy $file" fi - done </dev/null) -EOF + done done < Date: Fri, 3 Oct 2025 21:39:31 -0700 Subject: [PATCH 09/19] Enhance Git branch detection and improve cleanup logging - Updated `gtr` script to use `git branch --show-current` with a fallback to `git rev-parse` for better compatibility across Git versions. - Improved `cmd_clean` function to provide detailed logging on the number of empty directories removed or if none were found. - Enhanced `copy_patterns` function in `copy.sh` to handle glob patterns more securely and efficiently, including a fallback for Bash versions without `globstar` support. - Adjusted `run_hooks` in `hooks.sh` to execute hooks in a subshell, preserving the environment state and improving reliability. --- bin/gtr | 14 ++++- lib/copy.sh | 167 ++++++++++++++++++++++++++++++++++++--------------- lib/core.sh | 13 +++- lib/hooks.sh | 11 ++-- 4 files changed, 146 insertions(+), 59 deletions(-) diff --git a/bin/gtr b/bin/gtr index 754fdc2..730f09c 100755 --- a/bin/gtr +++ b/bin/gtr @@ -465,7 +465,9 @@ cmd_list() { if [ "$porcelain" -eq 1 ]; then # Always include ID 1 (repo root) local branch + # Try --show-current (Git 2.22+), fallback to rev-parse for older Git branch=$(git -C "$repo_root" branch --show-current 2>/dev/null) + [ -z "$branch" ] && branch=$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null) [ -z "$branch" ] && branch="(detached)" printf "%s\t%s\t%s\t%s\n" "1" "$repo_root" "$branch" "ok" @@ -490,7 +492,9 @@ cmd_list() { # Always show repo root as ID 1 local branch + # Try --show-current (Git 2.22+), fallback to rev-parse for older Git branch=$(git -C "$repo_root" branch --show-current 2>/dev/null) + [ -z "$branch" ] && branch=$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null) [ -z "$branch" ] && branch="(detached)" printf "%-6s %-25s %s\n" "1" "$branch" "$repo_root" @@ -530,16 +534,20 @@ cmd_clean() { # Find and remove empty directories local cleaned=0 - find "$base_dir" -maxdepth 1 -type d -empty 2>/dev/null | while IFS= read -r dir; do + while IFS= read -r dir; do if [ "$dir" != "$base_dir" ]; then if rmdir "$dir" 2>/dev/null; then cleaned=$((cleaned + 1)) log_info "Removed empty directory: $(basename "$dir")" fi fi - done + done < <(find "$base_dir" -maxdepth 1 -type d -empty 2>/dev/null) - log_info "✅ Cleanup complete" + if [ "$cleaned" -gt 0 ]; then + log_info "✅ Cleanup complete ($cleaned director$([ "$cleaned" -eq 1 ] && echo 'y' || echo 'ies') removed)" + else + log_info "✅ Cleanup complete (no empty directories found)" + fi } # Doctor command (health check) diff --git a/lib/copy.sh b/lib/copy.sh index 3a4b348..b33dff6 100644 --- a/lib/copy.sh +++ b/lib/copy.sh @@ -23,11 +23,19 @@ copy_patterns() { old_pwd=$(pwd) cd "$src_root" || return 1 - # Enable globstar for ** patterns (Bash 4.0+) + # Save current shell options + local shopt_save + shopt_save="$(shopt -p nullglob dotglob globstar 2>/dev/null || true)" + + # Try to enable globstar for ** patterns (Bash 4.0+) # nullglob: patterns that don't match expand to nothing # dotglob: * matches hidden files # globstar: ** matches directories recursively - shopt -s nullglob dotglob globstar + local have_globstar=0 + if shopt -s globstar 2>/dev/null; then + have_globstar=1 + fi + shopt -s nullglob dotglob 2>/dev/null || true local copied_count=0 @@ -35,60 +43,119 @@ copy_patterns() { while IFS= read -r pattern; do [ -z "$pattern" ] && continue - # Use native Bash glob expansion (supports **) - for file in $pattern; do - # Skip if not a file - [ -f "$file" ] || continue - - # Remove leading ./ - file="${file#./}" - - # Check if file matches any exclude pattern - local excluded=0 - if [ -n "$excludes" ]; then - while IFS= read -r exclude_pattern; do - [ -z "$exclude_pattern" ] && continue - case "$file" in - $exclude_pattern) - excluded=1 - break - ;; - esac - done </dev/null; then + log_info "Copied $file" + copied_count=$((copied_count + 1)) + else + log_warn "Failed to copy $file" + fi + done </dev/null) +EOF + else + # Use native Bash glob expansion (supports ** if available) + for file in $pattern; do + # Skip if not a file + [ -f "$file" ] || continue + + # Remove leading ./ + file="${file#./}" + + # Check if file matches any exclude pattern + local excluded=0 + if [ -n "$excludes" ]; then + while IFS= read -r exclude_pattern; do + [ -z "$exclude_pattern" ] && continue + case "$file" in + $exclude_pattern) + excluded=1 + break + ;; + esac + done </dev/null; then - log_info "Copied $file" - copied_count=$((copied_count + 1)) - else - log_warn "Failed to copy $file" - fi - done + fi + + # Skip if excluded + [ "$excluded" -eq 1 ] && continue + + # Determine destination path + local dest_file + if [ "$preserve_paths" = "true" ]; then + dest_file="$dst_root/$file" + else + dest_file="$dst_root/$(basename "$file")" + fi + + # Create destination directory + local dest_dir + dest_dir=$(dirname "$dest_file") + mkdir -p "$dest_dir" + + # Copy the file + if cp "$file" "$dest_file" 2>/dev/null; then + log_info "Copied $file" + copied_count=$((copied_count + 1)) + else + log_warn "Failed to copy $file" + fi + done + fi done </dev/null || true cd "$old_pwd" || return 1 diff --git a/lib/core.sh b/lib/core.sh index 13e321f..01b36e2 100644 --- a/lib/core.sh +++ b/lib/core.sh @@ -92,12 +92,19 @@ next_available_id() { # Usage: current_branch worktree_path current_branch() { local worktree_path="$1" + local branch if [ ! -d "$worktree_path" ]; then return 1 fi - (cd "$worktree_path" && git branch --show-current 2>/dev/null) || true + # Try --show-current (Git 2.22+), fallback to rev-parse for older Git + branch=$(cd "$worktree_path" && git branch --show-current 2>/dev/null) + if [ -z "$branch" ]; then + branch=$(cd "$worktree_path" && git rev-parse --abbrev-ref HEAD 2>/dev/null) + fi + + printf "%s" "$branch" } # Resolve a worktree target from ID or branch name @@ -119,7 +126,9 @@ resolve_target() { if [ "$id" = "1" ]; then # ID 1 is always the repo root path="$repo_root" + # Try --show-current (Git 2.22+), fallback to rev-parse for older Git branch=$(git -C "$repo_root" branch --show-current 2>/dev/null) + [ -z "$branch" ] && branch=$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null) printf "%s\t%s\t%s\n" "$id" "$path" "$branch" return 0 fi @@ -136,7 +145,9 @@ resolve_target() { else # Branch name - search for matching worktree # First check if it's the current branch in repo root + # Try --show-current (Git 2.22+), fallback to rev-parse for older Git branch=$(git -C "$repo_root" branch --show-current 2>/dev/null) + [ -z "$branch" ] && branch=$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null) if [ "$branch" = "$identifier" ]; then printf "1\t%s\t%s\n" "$repo_root" "$identifier" return 0 diff --git a/lib/hooks.sh b/lib/hooks.sh index b9f2f13..b1fd846 100644 --- a/lib/hooks.sh +++ b/lib/hooks.sh @@ -21,21 +21,22 @@ run_hooks() { local hook_count=0 local failed=0 - # Export provided environment variables + # Build export commands for environment variables + local exports="" while [ $# -gt 0 ]; do - export "$1" + exports="$exports export $1;" shift done - # Execute each hook (without subshell to preserve state) + # Execute each hook in a subshell to isolate side effects while IFS= read -r hook; do [ -z "$hook" ] && continue hook_count=$((hook_count + 1)) log_info "Hook $hook_count: $hook" - # Run hook and capture exit code - if eval "$hook"; then + # Run hook in subshell with exports, capture exit code + if ( eval "$exports $hook" ); then log_info "Hook $hook_count completed successfully" else local rc=$? From 86e086dd5da4ce43edc878db2bde0aea326cbb3b Mon Sep 17 00:00:00 2001 From: Hans Elizaga Date: Fri, 3 Oct 2025 21:59:02 -0700 Subject: [PATCH 10/19] Refactor copy_patterns and run_hooks for improved security and reliability - Updated `copy_patterns` in `copy.sh` to enhance security by refining the pattern rejection logic for absolute paths and parent directory traversal. - Modified `run_hooks` in `hooks.sh` to capture environment variable assignments in an array, ensuring proper quoting and isolation of side effects during hook execution. --- lib/copy.sh | 6 +++--- lib/hooks.sh | 19 +++++++++++-------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/lib/copy.sh b/lib/copy.sh index b33dff6..a2a3c98 100644 --- a/lib/copy.sh +++ b/lib/copy.sh @@ -43,10 +43,10 @@ copy_patterns() { while IFS= read -r pattern; do [ -z "$pattern" ] && continue - # Security: reject absolute paths and parent directory escapes + # Security: reject absolute paths and parent directory traversal case "$pattern" in - /*|*..*) - log_warn "Skipping unsafe pattern (absolute path or '..'): $pattern" + /*|*/../*|../*|*/..|..) + log_warn "Skipping unsafe pattern (absolute path or '..' path segment): $pattern" continue ;; esac diff --git a/lib/hooks.sh b/lib/hooks.sh index b1fd846..6e01c03 100644 --- a/lib/hooks.sh +++ b/lib/hooks.sh @@ -21,12 +21,8 @@ run_hooks() { local hook_count=0 local failed=0 - # Build export commands for environment variables - local exports="" - while [ $# -gt 0 ]; do - exports="$exports export $1;" - shift - done + # Capture environment variable assignments in array to preserve quoting + local envs=("$@") # Execute each hook in a subshell to isolate side effects while IFS= read -r hook; do @@ -35,8 +31,15 @@ run_hooks() { hook_count=$((hook_count + 1)) log_info "Hook $hook_count: $hook" - # Run hook in subshell with exports, capture exit code - if ( eval "$exports $hook" ); then + # Run hook in subshell with properly quoted environment exports + if ( + # Export each KEY=VALUE exactly as passed, safely quoted + for kv in "${envs[@]}"; do + export "$kv" + done + # Execute the hook + eval "$hook" + ); then log_info "Hook $hook_count completed successfully" else local rc=$? From 5f976a02012326952290aab8a023b8db8c8ff2d9 Mon Sep 17 00:00:00 2001 From: Hans Elizaga Date: Sat, 4 Oct 2025 23:34:35 -0700 Subject: [PATCH 11/19] Enhance gtr script and config functions for improved functionality - Updated `cmd_list` in `gtr` to filter directory names, ensuring only numeric prefixes are listed and including worktree status in output. - Modified `cfg_get` and `cfg_get_all` in `config.sh` to support an 'auto' scope for retrieving configuration values, merging local, global, and system settings while deduplicating results. - Introduced `worktree_status` function in `core.sh` to determine the status of worktrees, enhancing the overall robustness of the script. --- bin/gtr | 12 +++++++----- lib/config.sh | 52 +++++++++++++++++++++++++++++++------------------- lib/core.sh | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 25 deletions(-) diff --git a/bin/gtr b/bin/gtr index 730f09c..43b40ae 100755 --- a/bin/gtr +++ b/bin/gtr @@ -456,7 +456,7 @@ cmd_list() { if [ -d "$base_dir" ]; then find "$base_dir" -maxdepth 1 -type d -name "${prefix}*" 2>/dev/null | while IFS= read -r dir; do basename "$dir" | sed "s/^${prefix}//" - done | sort -n + done | grep -E '^[0-9]+$' | sort -n fi return 0 fi @@ -464,21 +464,23 @@ cmd_list() { # Machine-readable output (porcelain) if [ "$porcelain" -eq 1 ]; then # Always include ID 1 (repo root) - local branch + local branch status # Try --show-current (Git 2.22+), fallback to rev-parse for older Git branch=$(git -C "$repo_root" branch --show-current 2>/dev/null) [ -z "$branch" ] && branch=$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null) [ -z "$branch" ] && branch="(detached)" - printf "%s\t%s\t%s\t%s\n" "1" "$repo_root" "$branch" "ok" + status=$(worktree_status "$repo_root") + printf "%s\t%s\t%s\t%s\n" "1" "$repo_root" "$branch" "$status" if [ -d "$base_dir" ]; then # Find all worktree directories and output: idpathbranchstatus find "$base_dir" -maxdepth 1 -type d -name "${prefix}*" 2>/dev/null | while IFS= read -r dir; do - local id branch + local id branch status id=$(basename "$dir" | sed "s/^${prefix}//") branch=$(current_branch "$dir") [ -z "$branch" ] && branch="(detached)" - printf "%s\t%s\t%s\t%s\n" "$id" "$dir" "$branch" "ok" + status=$(worktree_status "$dir") + printf "%s\t%s\t%s\t%s\n" "$id" "$dir" "$branch" "$status" done | LC_ALL=C sort -n -k1,1 fi return 0 diff --git a/lib/config.sh b/lib/config.sh index 699cf37..99ba0bc 100644 --- a/lib/config.sh +++ b/lib/config.sh @@ -4,35 +4,50 @@ # Get a single config value # Usage: cfg_get key [scope] -# scope: local (default), global, or system +# scope: auto (default), local, global, or system +# auto uses git's built-in precedence: local > global > system cfg_get() { local key="$1" - local scope="${2:-local}" + local scope="${2:-auto}" local flag="" case "$scope" in + local) flag="--local" ;; global) flag="--global" ;; system) flag="--system" ;; - local|*) flag="--local" ;; + auto|*) flag="" ;; esac - git config $flag "$key" 2>/dev/null || true + git config $flag --get "$key" 2>/dev/null || true } # Get all values for a multi-valued config key # Usage: cfg_get_all key [scope] +# scope: auto (default), local, global, or system +# auto merges local + global + system and deduplicates cfg_get_all() { local key="$1" - local scope="${2:-local}" - local flag="" + local scope="${2:-auto}" case "$scope" in - global) flag="--global" ;; - system) flag="--system" ;; - local|*) flag="--local" ;; + local) + git config --local --get-all "$key" 2>/dev/null || true + ;; + global) + git config --global --get-all "$key" 2>/dev/null || true + ;; + system) + git config --system --get-all "$key" 2>/dev/null || true + ;; + auto|*) + # Merge all levels and deduplicate while preserving order + { + git config --local --get-all "$key" 2>/dev/null || true + git config --global --get-all "$key" 2>/dev/null || true + git config --system --get-all "$key" 2>/dev/null || true + } | awk '!seen[$0]++' + ;; esac - - git config $flag --get-all "$key" 2>/dev/null || true } # Get a boolean config value @@ -111,24 +126,21 @@ cfg_unset() { # Get config value with environment variable fallback # Usage: cfg_default key env_name fallback_value +# Now uses auto scope by default (checks local, global, system) cfg_default() { local key="$1" local env_name="$2" local fallback="$3" local value - # Try git config first - value=$(cfg_get "$key") + # Try git config first (auto scope - checks local > global > system) + value=$(cfg_get "$key" auto) - # Fall back to environment variable + # Fall back to environment variable (POSIX-compliant indirect reference) if [ -z "$value" ] && [ -n "$env_name" ]; then - value=$(eval echo "\$$env_name") + eval "value=\${${env_name}:-}" fi # Use fallback if still empty - if [ -z "$value" ]; then - value="$fallback" - fi - - printf "%s" "$value" + printf "%s" "${value:-$fallback}" } diff --git a/lib/core.sh b/lib/core.sh index 01b36e2..2825644 100644 --- a/lib/core.sh +++ b/lib/core.sh @@ -107,6 +107,59 @@ current_branch() { printf "%s" "$branch" } +# Get the status of a worktree from git +# Usage: worktree_status worktree_path +# Returns: status (ok, detached, locked, prunable, or missing) +worktree_status() { + local target_path="$1" + local porcelain_output + local in_section=0 + local status="ok" + local found=0 + + # Parse git worktree list --porcelain line by line + porcelain_output=$(git worktree list --porcelain 2>/dev/null) + + while IFS= read -r line; do + # Check if this is the start of our target worktree + if [ "$line" = "worktree $target_path" ]; then + in_section=1 + found=1 + continue + fi + + # If we're in the target section, check for status lines + if [ "$in_section" -eq 1 ]; then + # Empty line marks end of section + if [ -z "$line" ]; then + break + fi + + # Check for status indicators (priority: locked > prunable > detached) + case "$line" in + locked*) + status="locked" + ;; + prunable*) + [ "$status" = "ok" ] && status="prunable" + ;; + detached) + [ "$status" = "ok" ] && status="detached" + ;; + esac + fi + done < Date: Sat, 4 Oct 2025 23:41:52 -0700 Subject: [PATCH 12/19] Refactor cmd_clean in gtr for improved empty directory removal - Updated the `cmd_clean` function to streamline the process of finding and removing empty directories by leveraging a variable to store results, enhancing readability and efficiency. - Added a check to ensure only non-empty directory paths are processed, improving robustness and preventing unnecessary operations. --- bin/gtr | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/bin/gtr b/bin/gtr index 43b40ae..6d6a8fa 100755 --- a/bin/gtr +++ b/bin/gtr @@ -536,14 +536,21 @@ cmd_clean() { # Find and remove empty directories local cleaned=0 - while IFS= read -r dir; do - if [ "$dir" != "$base_dir" ]; then - if rmdir "$dir" 2>/dev/null; then - cleaned=$((cleaned + 1)) - log_info "Removed empty directory: $(basename "$dir")" + local empty_dirs + empty_dirs=$(find "$base_dir" -maxdepth 1 -type d -empty 2>/dev/null | grep -v "^${base_dir}$" || true) + + if [ -n "$empty_dirs" ]; then + while IFS= read -r dir; do + if [ -n "$dir" ]; then + if rmdir "$dir" 2>/dev/null; then + cleaned=$((cleaned + 1)) + log_info "Removed empty directory: $(basename "$dir")" + fi fi - fi - done < <(find "$base_dir" -maxdepth 1 -type d -empty 2>/dev/null) + done < Date: Sat, 4 Oct 2025 23:59:14 -0700 Subject: [PATCH 13/19] Add editor adapters for various text editors - Introduced new adapter scripts for Atom, Emacs, IntelliJ IDEA, Nano, Neovim, PyCharm, Sublime Text, Vim, and WebStorm, enabling users to open directories in their preferred editors. - Updated the `gtr` script to include these editors in the available options, enhancing user flexibility and experience. - Each adapter checks for the editor's availability and provides appropriate error messages if not found. --- adapters/editor/atom.sh | 20 ++++++++++++++++++++ adapters/editor/emacs.sh | 21 +++++++++++++++++++++ adapters/editor/idea.sh | 20 ++++++++++++++++++++ adapters/editor/nano.sh | 22 ++++++++++++++++++++++ adapters/editor/nvim.sh | 21 +++++++++++++++++++++ adapters/editor/pycharm.sh | 20 ++++++++++++++++++++ adapters/editor/sublime.sh | 20 ++++++++++++++++++++ adapters/editor/vim.sh | 21 +++++++++++++++++++++ adapters/editor/webstorm.sh | 20 ++++++++++++++++++++ bin/gtr | 4 ++-- 10 files changed, 187 insertions(+), 2 deletions(-) create mode 100755 adapters/editor/atom.sh create mode 100755 adapters/editor/emacs.sh create mode 100755 adapters/editor/idea.sh create mode 100755 adapters/editor/nano.sh create mode 100755 adapters/editor/nvim.sh create mode 100755 adapters/editor/pycharm.sh create mode 100755 adapters/editor/sublime.sh create mode 100755 adapters/editor/vim.sh create mode 100755 adapters/editor/webstorm.sh diff --git a/adapters/editor/atom.sh b/adapters/editor/atom.sh new file mode 100755 index 0000000..4e0acea --- /dev/null +++ b/adapters/editor/atom.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Atom editor adapter + +# Check if Atom is available +editor_can_open() { + command -v atom >/dev/null 2>&1 +} + +# Open a directory in Atom +# Usage: editor_open path +editor_open() { + local path="$1" + + if ! editor_can_open; then + log_error "Atom not found. Install from https://atom.io" + return 1 + fi + + atom "$path" +} diff --git a/adapters/editor/emacs.sh b/adapters/editor/emacs.sh new file mode 100755 index 0000000..473a79f --- /dev/null +++ b/adapters/editor/emacs.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Emacs editor adapter + +# Check if Emacs is available +editor_can_open() { + command -v emacs >/dev/null 2>&1 +} + +# Open a directory in Emacs +# Usage: editor_open path +editor_open() { + local path="$1" + + if ! editor_can_open; then + log_error "Emacs not found. Install from https://www.gnu.org/software/emacs/" + return 1 + fi + + # Open emacs with the directory + emacs "$path" & +} diff --git a/adapters/editor/idea.sh b/adapters/editor/idea.sh new file mode 100755 index 0000000..c6dc4d4 --- /dev/null +++ b/adapters/editor/idea.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# IntelliJ IDEA editor adapter + +# Check if IntelliJ IDEA is available +editor_can_open() { + command -v idea >/dev/null 2>&1 +} + +# Open a directory in IntelliJ IDEA +# Usage: editor_open path +editor_open() { + local path="$1" + + if ! editor_can_open; then + log_error "IntelliJ IDEA 'idea' command not found. Enable shell launcher in Tools > Create Command-line Launcher" + return 1 + fi + + idea "$path" +} diff --git a/adapters/editor/nano.sh b/adapters/editor/nano.sh new file mode 100755 index 0000000..41016d6 --- /dev/null +++ b/adapters/editor/nano.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# Nano editor adapter + +# Check if Nano is available +editor_can_open() { + command -v nano >/dev/null 2>&1 +} + +# Open a directory in Nano +# Usage: editor_open path +editor_open() { + local path="$1" + + if ! editor_can_open; then + log_error "Nano not found. Usually pre-installed on Unix systems." + return 1 + fi + + # Open nano in the directory (just cd there, nano doesn't open directories) + log_info "Opening shell in $path (nano doesn't support directory mode)" + (cd "$path" && exec "$SHELL") +} diff --git a/adapters/editor/nvim.sh b/adapters/editor/nvim.sh new file mode 100755 index 0000000..d845cae --- /dev/null +++ b/adapters/editor/nvim.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Neovim editor adapter + +# Check if Neovim is available +editor_can_open() { + command -v nvim >/dev/null 2>&1 +} + +# Open a directory in Neovim +# Usage: editor_open path +editor_open() { + local path="$1" + + if ! editor_can_open; then + log_error "Neovim not found. Install from https://neovim.io" + return 1 + fi + + # Open neovim in the directory + (cd "$path" && nvim .) +} diff --git a/adapters/editor/pycharm.sh b/adapters/editor/pycharm.sh new file mode 100755 index 0000000..74c5499 --- /dev/null +++ b/adapters/editor/pycharm.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# PyCharm editor adapter + +# Check if PyCharm is available +editor_can_open() { + command -v pycharm >/dev/null 2>&1 +} + +# Open a directory in PyCharm +# Usage: editor_open path +editor_open() { + local path="$1" + + if ! editor_can_open; then + log_error "PyCharm 'pycharm' command not found. Enable shell launcher in Tools > Create Command-line Launcher" + return 1 + fi + + pycharm "$path" +} diff --git a/adapters/editor/sublime.sh b/adapters/editor/sublime.sh new file mode 100755 index 0000000..dff5c50 --- /dev/null +++ b/adapters/editor/sublime.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Sublime Text editor adapter + +# Check if Sublime Text is available +editor_can_open() { + command -v subl >/dev/null 2>&1 +} + +# Open a directory in Sublime Text +# Usage: editor_open path +editor_open() { + local path="$1" + + if ! editor_can_open; then + log_error "Sublime Text 'subl' command not found. Install from https://www.sublimetext.com" + return 1 + fi + + subl "$path" +} diff --git a/adapters/editor/vim.sh b/adapters/editor/vim.sh new file mode 100755 index 0000000..2f1b0e4 --- /dev/null +++ b/adapters/editor/vim.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Vim editor adapter + +# Check if Vim is available +editor_can_open() { + command -v vim >/dev/null 2>&1 +} + +# Open a directory in Vim +# Usage: editor_open path +editor_open() { + local path="$1" + + if ! editor_can_open; then + log_error "Vim not found. Install via your package manager." + return 1 + fi + + # Open vim in the directory + (cd "$path" && vim .) +} diff --git a/adapters/editor/webstorm.sh b/adapters/editor/webstorm.sh new file mode 100755 index 0000000..ea55c5c --- /dev/null +++ b/adapters/editor/webstorm.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# WebStorm editor adapter + +# Check if WebStorm is available +editor_can_open() { + command -v webstorm >/dev/null 2>&1 +} + +# Open a directory in WebStorm +# Usage: editor_open path +editor_open() { + local path="$1" + + if ! editor_can_open; then + log_error "WebStorm 'webstorm' command not found. Enable shell launcher in Tools > Create Command-line Launcher" + return 1 + fi + + webstorm "$path" +} diff --git a/bin/gtr b/bin/gtr index 6d6a8fa..2835e92 100755 --- a/bin/gtr +++ b/bin/gtr @@ -777,7 +777,7 @@ load_editor_adapter() { if [ ! -f "$adapter_file" ]; then log_error "Unknown editor: $editor" - log_info "Available editors: cursor, vscode, zed" + log_info "Available editors: cursor, vscode, zed, idea, pycharm, webstorm, vim, nvim, emacs, sublime, nano, atom" exit 1 fi @@ -889,7 +889,7 @@ CONFIGURATION: gtr.worktrees.prefix Worktree name prefix (default: wt-) gtr.worktrees.startId Starting ID (default: 2) gtr.defaultBranch Default branch (default: auto) - gtr.editor.default Default editor (cursor, vscode, zed, none) + gtr.editor.default Default editor (cursor, vscode, zed, idea, pycharm, webstorm, vim, nvim, emacs, sublime, nano, atom, none) gtr.ai.default Default AI tool (aider, claude, codex, cursor, continue, none) gtr.copy.include Files to copy (multi-valued) gtr.copy.exclude Files to exclude (multi-valued) From 85076c9cb2b2e8a100ca03594cb072ac1de65d0d Mon Sep 17 00:00:00 2001 From: Hans Elizaga Date: Sun, 5 Oct 2025 00:19:58 -0700 Subject: [PATCH 14/19] Update README and gtr script for clarity and consistency - Revised the "Requirements" section in README.md to specify Bash 3.2+ as the minimum version, aligning with macOS defaults and recommending 4.0+ for advanced features. - Removed emoji from output messages in the gtr script for a cleaner presentation, standardizing the format of log messages and user prompts. - Enhanced branch detection logic to normalize detached HEAD states in core.sh, improving user experience when working with Git worktrees. --- README.md | 2 +- bin/gtr | 88 +++++++++++++++++++++++++++-------------------------- lib/core.sh | 5 +++ lib/ui.sh | 10 +++--- 4 files changed, 56 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 588cd77..ef7b3a4 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ While `git worktree` is powerful, it requires remembering paths and manually set ## Requirements - **Git** 2.5+ (for `git worktree` support) -- **Bash** 4.0+ (standard on macOS, Linux, and Windows Git Bash) +- **Bash** 3.2+ (macOS ships 3.2; 4.0+ recommended for advanced features) ## Installation diff --git a/bin/gtr b/bin/gtr index 2835e92..11eae16 100755 --- a/bin/gtr +++ b/bin/gtr @@ -167,8 +167,8 @@ cmd_create() { worktree_path="$base_dir/${prefix}${worktree_id}" log_step "Creating worktree: ${prefix}${worktree_id}" - echo "📂 Location: $worktree_path" - echo "🌿 Branch: $branch_name" + echo "Location: $worktree_path" + echo "Branch: $branch_name" # Create the worktree if ! create_worktree "$base_dir" "$prefix" "$worktree_id" "$branch_name" "$from_ref" "$track_mode" "$skip_fetch"; then @@ -309,11 +309,11 @@ cmd_go() { # Human messages to stderr so stdout can be used in command substitution if [ "$worktree_id" = "1" ]; then - echo "📂 Repo root (id 1)" >&2 + echo "Repo root (id 1)" >&2 else - echo "📂 Worktree ${prefix}${worktree_id}" >&2 + echo "Worktree ${prefix}${worktree_id}" >&2 fi - echo "🌿 Branch: $branch" >&2 + echo "Branch: $branch" >&2 # Print path to stdout for shell integration: cd "$(gtr go 2)" printf "%s\n" "$worktree_path" @@ -359,14 +359,14 @@ cmd_open() { # AI command cmd_ai() { local identifier="" - local ai_args="" + local -a ai_args=() # Parse arguments while [ $# -gt 0 ]; do case "$1" in --) shift - ai_args="$*" + ai_args=("$@") break ;; -*) @@ -414,11 +414,10 @@ cmd_ai() { branch=$(echo "$target" | cut -f3) log_step "Starting $ai_tool in worktree: ${prefix}${worktree_id}" - echo "📂 Directory: $worktree_path" - echo "🌿 Branch: $branch" + echo "Directory: $worktree_path" + echo "Branch: $branch" - # shellcheck disable=SC2086 - ai_start "$worktree_path" $ai_args + ai_start "$worktree_path" "${ai_args[@]}" } # List command @@ -468,7 +467,7 @@ cmd_list() { # Try --show-current (Git 2.22+), fallback to rev-parse for older Git branch=$(git -C "$repo_root" branch --show-current 2>/dev/null) [ -z "$branch" ] && branch=$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null) - [ -z "$branch" ] && branch="(detached)" + [ -z "$branch" ] || [ "$branch" = "HEAD" ] && branch="(detached)" status=$(worktree_status "$repo_root") printf "%s\t%s\t%s\t%s\n" "1" "$repo_root" "$branch" "$status" @@ -487,7 +486,7 @@ cmd_list() { fi # Human-readable output - table format - echo "📋 Git Worktrees" + echo "Git Worktrees" echo "" printf "%-6s %-25s %s\n" "ID" "BRANCH" "PATH" printf "%-6s %-25s %s\n" "──" "──────" "────" @@ -497,7 +496,7 @@ cmd_list() { # Try --show-current (Git 2.22+), fallback to rev-parse for older Git branch=$(git -C "$repo_root" branch --show-current 2>/dev/null) [ -z "$branch" ] && branch=$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null) - [ -z "$branch" ] && branch="(detached)" + [ -z "$branch" ] || [ "$branch" = "HEAD" ] && branch="(detached)" printf "%-6s %-25s %s\n" "1" "$branch" "$repo_root" # Show worktrees @@ -512,7 +511,8 @@ cmd_list() { fi echo "" - echo "💡 Tip: Use 'gtr list --porcelain' for machine-readable output" + echo "" + echo "Tip: Use 'gtr list --porcelain' for machine-readable output" } # Clean command (remove prunable worktrees) @@ -553,15 +553,15 @@ EOF fi if [ "$cleaned" -gt 0 ]; then - log_info "✅ Cleanup complete ($cleaned director$([ "$cleaned" -eq 1 ] && echo 'y' || echo 'ies') removed)" + log_info "Cleanup complete ($cleaned director$([ "$cleaned" -eq 1 ] && echo 'y' || echo 'ies') removed)" else - log_info "✅ Cleanup complete (no empty directories found)" + log_info "Cleanup complete (no empty directories found)" fi } # Doctor command (health check) cmd_doctor() { - echo "🏥 Running gtr health check..." + echo "Running gtr health check..." echo "" local issues=0 @@ -570,16 +570,16 @@ cmd_doctor() { if command -v git >/dev/null 2>&1; then local git_version git_version=$(git --version) - echo "✅ Git: $git_version" + echo "[OK] Git: $git_version" else - echo "❌ Git: not found" + echo "[x] Git: not found" issues=$((issues + 1)) fi # Check repo local repo_root if repo_root=$(discover_repo_root 2>/dev/null); then - echo "✅ Repository: $repo_root" + echo "[OK] Repository: $repo_root" # Check worktree base dir local base_dir prefix @@ -589,12 +589,12 @@ cmd_doctor() { if [ -d "$base_dir" ]; then local count count=$(find "$base_dir" -maxdepth 1 -type d -name "${prefix}*" 2>/dev/null | wc -l | tr -d ' ') - echo "✅ Worktrees directory: $base_dir ($count worktrees)" + echo "[OK] Worktrees directory: $base_dir ($count worktrees)" else - echo "ℹ️ Worktrees directory: $base_dir (not created yet)" + echo "[i] Worktrees directory: $base_dir (not created yet)" fi else - echo "❌ Not in a git repository" + echo "[x] Not in a git repository" issues=$((issues + 1)) fi @@ -607,15 +607,15 @@ cmd_doctor() { if [ -f "$editor_adapter" ]; then . "$editor_adapter" if editor_can_open 2>/dev/null; then - echo "✅ Editor: $editor (found)" + echo "[OK] Editor: $editor (found)" else - echo "⚠️ Editor: $editor (configured but not found in PATH)" + echo "[!] Editor: $editor (configured but not found in PATH)" fi else - echo "⚠️ Editor: $editor (adapter not found)" + echo "[!] Editor: $editor (adapter not found)" fi else - echo "ℹ️ Editor: none configured" + echo "[i] Editor: none configured" fi # Check configured AI tool @@ -627,39 +627,39 @@ cmd_doctor() { if [ -f "$adapter_file" ]; then . "$adapter_file" if ai_can_start 2>/dev/null; then - echo "✅ AI tool: $ai_tool (found)" + echo "[OK] AI tool: $ai_tool (found)" else - echo "⚠️ AI tool: $ai_tool (configured but not found in PATH)" + echo "[!] AI tool: $ai_tool (configured but not found in PATH)" fi else - echo "⚠️ AI tool: $ai_tool (adapter not found)" + echo "[!] AI tool: $ai_tool (adapter not found)" fi else - echo "ℹ️ AI tool: none configured" + echo "[i] AI tool: none configured" fi # Check OS local os os=$(detect_os) - echo "✅ Platform: $os" + echo "[OK] Platform: $os" echo "" if [ "$issues" -eq 0 ]; then - echo "🎉 Everything looks good!" + echo "Everything looks good!" return 0 else - echo "⚠️ Found $issues issue(s)" + echo "[!] Found $issues issue(s)" return 1 fi } # Adapter command (list available adapters) cmd_adapter() { - echo "🔌 Available Adapters" + echo "Available Adapters" echo "" # Editor adapters - echo "📝 Editor Adapters:" + echo "Editor Adapters:" echo "" printf "%-15s %-10s %s\n" "NAME" "STATUS" "NOTES" printf "%-15s %-10s %s\n" "────" "──────" "─────" @@ -671,15 +671,16 @@ cmd_adapter() { . "$adapter_file" if editor_can_open 2>/dev/null; then - printf "%-15s %-10s %s\n" "$adapter_name" "✅ ready" "" + printf "%-15s %-10s %s\n" "$adapter_name" "[ready]" "" else - printf "%-15s %-10s %s\n" "$adapter_name" "⚠️ missing" "Not found in PATH" + printf "%-15s %-10s %s\n" "$adapter_name" "[missing]" "Not found in PATH" fi fi done echo "" - echo "🤖 AI Tool Adapters:" + echo "" + echo "AI Tool Adapters:" echo "" printf "%-15s %-10s %s\n" "NAME" "STATUS" "NOTES" printf "%-15s %-10s %s\n" "────" "──────" "─────" @@ -691,15 +692,16 @@ cmd_adapter() { . "$adapter_file" if ai_can_start 2>/dev/null; then - printf "%-15s %-10s %s\n" "$adapter_name" "✅ ready" "" + printf "%-15s %-10s %s\n" "$adapter_name" "[ready]" "" else - printf "%-15s %-10s %s\n" "$adapter_name" "⚠️ missing" "Not found in PATH" + printf "%-15s %-10s %s\n" "$adapter_name" "[missing]" "Not found in PATH" fi fi done echo "" - echo "💡 Tip: Set defaults with:" + echo "" + echo "Tip: Set defaults with:" echo " gtr config set gtr.editor.default " echo " gtr config set gtr.ai.default " } diff --git a/lib/core.sh b/lib/core.sh index 2825644..6fa87fc 100644 --- a/lib/core.sh +++ b/lib/core.sh @@ -104,6 +104,11 @@ current_branch() { branch=$(cd "$worktree_path" && git rev-parse --abbrev-ref HEAD 2>/dev/null) fi + # Normalize detached HEAD + if [ "$branch" = "HEAD" ]; then + branch="(detached)" + fi + printf "%s" "$branch" } diff --git a/lib/ui.sh b/lib/ui.sh index d33dbff..3a61db8 100644 --- a/lib/ui.sh +++ b/lib/ui.sh @@ -2,23 +2,23 @@ # UI utilities for logging and prompting log_info() { - printf "✅ %s\n" "$*" + printf "[OK] %s\n" "$*" } log_warn() { - printf "⚠️ %s\n" "$*" + printf "[!] %s\n" "$*" } log_error() { - printf "❌ %s\n" "$*" >&2 + printf "[x] %s\n" "$*" >&2 } log_step() { - printf "🚀 %s\n" "$*" + printf "==> %s\n" "$*" } log_question() { - printf "❓ %s" "$*" + printf "[?] %s" "$*" } # Prompt for yes/no confirmation From 804264499de816dbcb138874283d79f90b5aeb67 Mon Sep 17 00:00:00 2001 From: Hans Elizaga Date: Sun, 5 Oct 2025 03:02:35 -0700 Subject: [PATCH 15/19] Update gtr script to enhance output formatting for adapter listings - Adjusted column widths in the output of the `cmd_adapter` function to improve alignment and readability of the adapter status and notes. - Standardized header formatting for both editor and AI tool adapters, ensuring a consistent presentation across the script. --- bin/gtr | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/bin/gtr b/bin/gtr index 11eae16..edc6904 100755 --- a/bin/gtr +++ b/bin/gtr @@ -661,8 +661,8 @@ cmd_adapter() { # Editor adapters echo "Editor Adapters:" echo "" - printf "%-15s %-10s %s\n" "NAME" "STATUS" "NOTES" - printf "%-15s %-10s %s\n" "────" "──────" "─────" + printf "%-15s %-15s %s\n" "NAME" "STATUS" "NOTES" + printf "%-15s %-15s %s\n" "---------------" "---------------" "-----" for adapter_file in "$GTR_DIR"/adapters/editor/*.sh; do if [ -f "$adapter_file" ]; then @@ -671,9 +671,9 @@ cmd_adapter() { . "$adapter_file" if editor_can_open 2>/dev/null; then - printf "%-15s %-10s %s\n" "$adapter_name" "[ready]" "" + printf "%-15s %-15s %s\n" "$adapter_name" "[ready]" "" else - printf "%-15s %-10s %s\n" "$adapter_name" "[missing]" "Not found in PATH" + printf "%-15s %-15s %s\n" "$adapter_name" "[missing]" "Not found in PATH" fi fi done @@ -682,8 +682,8 @@ cmd_adapter() { echo "" echo "AI Tool Adapters:" echo "" - printf "%-15s %-10s %s\n" "NAME" "STATUS" "NOTES" - printf "%-15s %-10s %s\n" "────" "──────" "─────" + printf "%-15s %-15s %s\n" "NAME" "STATUS" "NOTES" + printf "%-15s %-15s %s\n" "---------------" "---------------" "-----" for adapter_file in "$GTR_DIR"/adapters/ai/*.sh; do if [ -f "$adapter_file" ]; then @@ -692,9 +692,9 @@ cmd_adapter() { . "$adapter_file" if ai_can_start 2>/dev/null; then - printf "%-15s %-10s %s\n" "$adapter_name" "[ready]" "" + printf "%-15s %-15s %s\n" "$adapter_name" "[ready]" "" else - printf "%-15s %-10s %s\n" "$adapter_name" "[missing]" "Not found in PATH" + printf "%-15s %-15s %s\n" "$adapter_name" "[missing]" "Not found in PATH" fi fi done From eb3093aac629f2167047a00753970e00a4aaab63 Mon Sep 17 00:00:00 2001 From: Hans Elizaga Date: Sun, 5 Oct 2025 03:16:39 -0700 Subject: [PATCH 16/19] Enhance gtr command handling with flag support for editor and AI tool - Updated `cmd_open` and `cmd_ai` functions to accept `--editor` and `--ai` flags, allowing users to specify their preferred tools directly in the command line. - Improved usage messages to reflect new flag options and ensure clarity for users. - Enhanced help documentation to provide a comprehensive overview of command usage and configuration options. --- bin/gtr | 167 +++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 116 insertions(+), 51 deletions(-) diff --git a/bin/gtr b/bin/gtr index edc6904..6afc2a2 100755 --- a/bin/gtr +++ b/bin/gtr @@ -321,16 +321,38 @@ cmd_go() { # Open command cmd_open() { - if [ $# -ne 1 ]; then - log_error "Usage: gtr open " + local identifier="" + local editor="" + + # Parse flags + while [ $# -gt 0 ]; do + case "$1" in + --editor) + editor="$2" + shift 2 + ;; + -*) + log_error "Unknown flag: $1" + exit 1 + ;; + *) + if [ -z "$identifier" ]; then + identifier="$1" + fi + shift + ;; + esac + done + + if [ -z "$identifier" ]; then + log_error "Usage: gtr open [--editor ]" exit 1 fi - local identifier="$1" - - # Get editor from config - local editor - editor=$(cfg_default gtr.editor.default GTR_EDITOR_DEFAULT "none") + # Get editor from flag or config + if [ -z "$editor" ]; then + editor=$(cfg_default gtr.editor.default GTR_EDITOR_DEFAULT "none") + fi local repo_root base_dir prefix repo_root=$(discover_repo_root) || exit 1 @@ -359,11 +381,16 @@ cmd_open() { # AI command cmd_ai() { local identifier="" + local ai_tool="" local -a ai_args=() # Parse arguments while [ $# -gt 0 ]; do case "$1" in + --ai) + ai_tool="$2" + shift 2 + ;; --) shift ai_args=("$@") @@ -383,13 +410,14 @@ cmd_ai() { done if [ -z "$identifier" ]; then - log_error "Usage: gtr ai [-- args...]" + log_error "Usage: gtr ai [--ai ] [-- args...]" exit 1 fi - # Get AI tool from config - local ai_tool - ai_tool=$(cfg_default gtr.ai.default GTR_AI_DEFAULT "none") + # Get AI tool from flag or config + if [ -z "$ai_tool" ]; then + ai_tool=$(cfg_default gtr.ai.default GTR_AI_DEFAULT "none") + fi # Check if AI tool is configured if [ "$ai_tool" = "none" ]; then @@ -805,13 +833,32 @@ cmd_help() { cat <<'EOF' gtr - Git worktree runner -Run from within a git repository. Each repo has independent worktrees and IDs. +PHILOSOPHY: Configuration over flags. Set defaults once, then use simple commands. + +──────────────────────────────────────────────────────────────────────────────── + +QUICK START: + cd ~/your-repo # Navigate to git repo first + gtr config set gtr.editor.default cursor # One-time setup + gtr config set gtr.ai.default claude # One-time setup + gtr new my-feature # Auto-assigns ID, uses defaults + gtr open my-feature # Opens in cursor + gtr ai my-feature # Starts claude + gtr rm my-feature # Remove when done + +──────────────────────────────────────────────────────────────────────────────── + +KEY CONCEPTS: + • Each git repo has independent worktrees and IDs + • Main repo is always ID 1 (accessible via: gtr go 1, gtr open 1) + • New worktrees auto-assign IDs starting at 2 (configurable) + • Commands accept EITHER ID numbers OR branch names + Examples: gtr open 2 OR gtr open my-feature -USAGE: - cd ~/your-repo # Navigate to git repo first - gtr [options] +──────────────────────────────────────────────────────────────────────────────── + +CORE COMMANDS (daily workflow): -COMMANDS: new [options] Create a new worktree (auto-assigns ID by default) --id : specify exact ID (rarely needed) @@ -821,15 +868,18 @@ COMMANDS: --no-fetch: skip git fetch --yes: non-interactive mode + open [--editor ] + Open worktree in editor (uses gtr.editor.default or --editor) + + ai [--ai ] [-- args...] + Start AI coding tool in worktree (uses gtr.ai.default or --ai) + go Navigate to worktree (prints path for: cd "$(gtr go 2)") - Accepts either numeric ID or branch name - - open - Open worktree in editor (uses gtr.editor.default) - ai [-- args...] - Start AI coding tool in worktree (uses gtr.ai.default) + list [--porcelain|--ids] + List all worktrees (ID 1 = repo root) + Aliases: ls rm [...] [options] Remove worktree(s) @@ -837,11 +887,12 @@ COMMANDS: --force: force removal (dirty worktree) --yes: skip confirmation - ls, list [--porcelain|--ids] - List all worktrees (ID 1 = repo root) +──────────────────────────────────────────────────────────────────────────────── - clean - Remove stale/prunable worktrees +SETUP & MAINTENANCE: + + config {get|set|unset} [value] [--global] + Manage configuration doctor Health check (verify git, editors, AI tools) @@ -849,56 +900,70 @@ COMMANDS: adapter List available editor & AI tool adapters - config {get|set|unset} [value] [--global] - Manage configuration + clean + Remove stale/prunable worktrees version Show version - help - Show this help +──────────────────────────────────────────────────────────────────────────────── -EXAMPLES: - # Navigate to your git repo first - cd ~/GitHub/my-project +WORKFLOW EXAMPLES: - # Configure defaults (one-time setup per repo) + # One-time repo setup + cd ~/GitHub/my-project gtr config set gtr.editor.default cursor gtr config set gtr.ai.default claude - # Typical workflow: create, open, start AI - gtr new my-feature - gtr open my-feature - gtr ai my-feature + # Daily workflow + gtr new feature/user-auth # Create worktree + gtr open feature/user-auth # Open in editor + gtr ai feature/user-auth # Start AI tool + + # Navigate to worktree directory + cd "$(gtr go feature/user-auth)" - # Or chain them together - gtr new my-feature && gtr open my-feature && gtr ai my-feature + # Use ID instead of branch name (same result) + gtr open 2 + gtr ai 2 - # Navigate to worktree - cd "$(gtr go my-feature)" + # Override defaults with flags + gtr open feature/user-auth --editor vscode + gtr ai feature/user-auth --ai aider - # List all worktrees - gtr list + # Chain commands together + gtr new hotfix && gtr open hotfix && gtr ai hotfix - # Remove when done - gtr rm my-feature --delete-branch + # When finished + gtr rm feature/user-auth --delete-branch - # Health check + # Check setup and available tools gtr doctor + gtr adapter + +──────────────────────────────────────────────────────────────────────────────── + +CONFIGURATION OPTIONS: -CONFIGURATION: gtr.worktrees.dir Worktrees base directory gtr.worktrees.prefix Worktree name prefix (default: wt-) gtr.worktrees.startId Starting ID (default: 2) gtr.defaultBranch Default branch (default: auto) - gtr.editor.default Default editor (cursor, vscode, zed, idea, pycharm, webstorm, vim, nvim, emacs, sublime, nano, atom, none) - gtr.ai.default Default AI tool (aider, claude, codex, cursor, continue, none) + gtr.editor.default Default editor + Options: cursor, vscode, zed, idea, pycharm, + webstorm, vim, nvim, emacs, sublime, nano, + atom, none + gtr.ai.default Default AI tool + Options: aider, claude, codex, cursor, + continue, none gtr.copy.include Files to copy (multi-valued) gtr.copy.exclude Files to exclude (multi-valued) gtr.hook.postCreate Post-create hooks (multi-valued) gtr.hook.postRemove Post-remove hooks (multi-valued) -See https://github.com/anthropics/git-worktree-runner for more info. +──────────────────────────────────────────────────────────────────────────────── + +MORE INFO: https://github.com/anthropics/git-worktree-runner EOF } From b5ff01f182db15aaacfd5167809366dc91d0ecc1 Mon Sep 17 00:00:00 2001 From: Hans Elizaga Date: Sun, 5 Oct 2025 03:18:55 -0700 Subject: [PATCH 17/19] Update project references in documentation and license - Changed all occurrences of "Anthropic PBC" to "CodeRabbit AI" in LICENSE and updated repository links in CHANGELOG.md, README.md, and gtr script to reflect the new GitHub organization. - Ensured consistency across project documentation and improved clarity for users regarding the new project ownership. --- CHANGELOG.md | 2 +- LICENSE | 2 +- README.md | 4 ++-- bin/gtr | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fedead8..9879496 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,4 +39,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Adapter pattern** - Pluggable editor and AI tool integrations - **Stream separation** - Data to stdout, messages to stderr for composability -[1.0.0]: https://github.com/anthropics/git-worktree-runner/releases/tag/v1.0.0 +[1.0.0]: https://github.com/coderabbitai/git-worktree-runner/releases/tag/v1.0.0 diff --git a/LICENSE b/LICENSE index 09b05f4..650b623 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2025 Anthropic PBC +Copyright (c) 2025 CodeRabbit AI Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index ef7b3a4..e162de1 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ While `git worktree` is powerful, it requires remembering paths and manually set ```bash # Clone the repository -git clone https://github.com/anthropics/git-worktree-runner.git +git clone https://github.com/coderabbitai/git-worktree-runner.git cd git-worktree-runner # Add to PATH (choose one) @@ -656,4 +656,4 @@ Built to streamline parallel development workflows with git worktrees. Inspired **Happy coding with worktrees! 🚀** -For questions or issues, please [open an issue](https://github.com/anthropics/git-worktree-runner/issues). +For questions or issues, please [open an issue](https://github.com/coderabbitai/git-worktree-runner/issues). diff --git a/bin/gtr b/bin/gtr index 6afc2a2..12bc3c7 100755 --- a/bin/gtr +++ b/bin/gtr @@ -963,7 +963,7 @@ CONFIGURATION OPTIONS: ──────────────────────────────────────────────────────────────────────────────── -MORE INFO: https://github.com/anthropics/git-worktree-runner +MORE INFO: https://github.com/coderabbitai/git-worktree-runner EOF } From a9e277b78ce23ada302b88e7b831e88b7379e409 Mon Sep 17 00:00:00 2001 From: Hans Elizaga Date: Sun, 5 Oct 2025 20:24:33 -0700 Subject: [PATCH 18/19] Update documentation and command handling for branch-based worktrees - Revised CHANGELOG.md to reflect new features including branch-based folder naming and improved command UX for worktree management. - Updated CONTRIBUTING.md to include new manual testing checklist items for branch name handling. - Enhanced README.md with clearer instructions on using branch names for worktree commands and streamlined the quick start guide. - Refactored gtr script to support branch names in commands, removing reliance on numeric IDs and improving user experience. - Adjusted completion scripts for Zsh and Bash to reflect changes in command usage, ensuring accurate suggestions for branch names. --- CHANGELOG.md | 14 +- CONTRIBUTING.md | 8 +- README.md | 315 ++++++++++++----------------------- bin/gtr | 173 ++++++++----------- completions/_gtr | 12 +- completions/gtr.bash | 11 +- completions/gtr.fish | 16 +- lib/core.sh | 130 +++++++-------- templates/gtr.config.example | 9 +- templates/setup-example.sh | 3 +- 10 files changed, 263 insertions(+), 428 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9879496..682e305 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- **Repository-scoped worktrees** - Each git repo has independent worktrees and IDs -- **Auto-ID allocation** - Worktrees auto-assigned starting from configurable `gtr.worktrees.startId` (default: 2) -- **Branch-name-first UX** - All commands accept either numeric IDs or branch names (`gtr open my-feature`) +- **Repository-scoped worktrees** - Each git repo has independent worktrees +- **Branch-based folder naming** - Worktree folders are named after their branch names +- **Branch-name UX** - All commands accept branch names to identify worktrees (`gtr open my-feature`) - **Explicit command design** - Each command does one thing (`new` creates, `open` opens, `ai` starts AI). No auto-behavior or override flags - **Config-based defaults** - Set `gtr.editor.default` and `gtr.ai.default` once, use everywhere without flags - **Editor adapters** - Support for Cursor, VS Code, and Zed @@ -22,10 +22,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Cross-platform support** - Works on macOS, Linux, and Windows (Git Bash/WSL) - **Utility commands**: - `gtr new ` - Create worktree with smart branch tracking - - `gtr go ` - Navigate to worktree (shell integration) - - `gtr open ` - Open in editor - - `gtr ai ` - Start AI coding tool - - `gtr rm ` - Remove worktree(s) + - `gtr go ` - Navigate to worktree (shell integration) + - `gtr open ` - Open in editor + - `gtr ai ` - Start AI coding tool + - `gtr rm ` - Remove worktree(s) - `gtr list` - List all worktrees with human/machine-readable output - `gtr clean` - Remove stale worktrees - `gtr doctor` - Health check for git, editors, and AI tools diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 99580be..f896a66 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -167,17 +167,19 @@ Currently, testing is manual. Please test your changes on: #### Manual Testing Checklist -- [ ] Create worktree with auto ID -- [ ] Create worktree with specific ID +- [ ] Create worktree with branch name +- [ ] Create worktree with branch containing slashes (e.g., feature/auth) - [ ] Create from remote branch - [ ] Create from local branch - [ ] Create new branch - [ ] Open in editor (if testing adapters) - [ ] Run AI tool (if testing adapters) -- [ ] Remove worktree +- [ ] Remove worktree by branch name - [ ] List worktrees - [ ] Test configuration commands - [ ] Test completions (tab completion works) +- [ ] Test `gtr go 1` for main repo +- [ ] Test `gtr go ` for worktrees ### Pull Request Process diff --git a/README.md b/README.md index e162de1..badf4b3 100644 --- a/README.md +++ b/README.md @@ -4,39 +4,22 @@ `gtr` makes it simple to create, manage, and work with [git worktrees](https://git-scm.com/docs/git-worktree), enabling you to work on multiple branches simultaneously without stashing or switching contexts. -## How It Works +## TL;DR -**gtr is repository-scoped** - each git repository has its own independent set of worktrees: - -- Run `gtr` commands from within any git repository -- Each repo has separate worktree IDs (starting at 2, ID 1 is the main repo) -- IDs are local to each repo - no conflicts across projects -- Switch repos with `cd`, then run `gtr` commands for that repo - -**Example - Working across multiple repos:** ```bash -cd ~/GitHub/frontend -gtr new auth-feature # Creates frontend worktree (ID 2) -gtr list # Shows only frontend worktrees - -cd ~/GitHub/backend -gtr new auth-api # Creates backend worktree (also ID 2 - different repo!) -gtr list # Shows only backend worktrees +cd ~/your-repo # Navigate to git repo +gtr config set gtr.editor.default cursor # One-time setup +gtr new my-feature # Create worktree +gtr open my-feature # Open in editor +gtr ai my-feature # Start AI tool +gtr rm my-feature # Remove when done ``` -## Why Git Worktrees? +## Why gtr? -Git worktrees let you check out multiple branches at once in separate directories. This is invaluable when you need to: +Git worktrees let you check out multiple branches at once in separate directories - perfect for reviewing PRs while developing, running tests on main, or comparing implementations side-by-side. -- Review a PR while working on a feature -- Run tests on `main` while developing -- Quickly switch between branches without stashing -- Compare implementations side-by-side -- Run multiple development servers simultaneously - -## Why not just `git worktree`? - -While `git worktree` is powerful, it requires remembering paths and manually setting up each worktree. `gtr` adds: +While `git worktree` is powerful, it's verbose and manual. `gtr` adds quality-of-life features for modern development: | Task | With `git worktree` | With `gtr` | | ------------------ | ------------------------------------------ | ---------------------------------- | @@ -45,7 +28,7 @@ While `git worktree` is powerful, it requires remembering paths and manually set | Start AI tool | `cd ../repo-feature && aider` | `gtr ai feature` | | Copy config files | Manual copy/paste | Auto-copy via `gtr.copy.include` | | Run build steps | Manual `npm install && npm run build` | Auto-run via `gtr.hook.postCreate` | -| List worktrees | `git worktree list` (shows paths) | `gtr list` (shows IDs + status) | +| List worktrees | `git worktree list` (shows paths) | `gtr list` (shows branches + status) | | Switch to worktree | `cd ../repo-feature` | `cd "$(gtr go feature)"` | | Clean up | `git worktree remove ../repo-feature` | `gtr rm feature` | @@ -54,15 +37,40 @@ While `git worktree` is powerful, it requires remembering paths and manually set ## Features - 🚀 **Simple commands** - Create and manage worktrees with intuitive CLI -- 📁 **Repository-scoped** - Each repo has independent worktrees and IDs -- 🔧 **Configurable** - Git-config based settings, no YAML/TOML parsers needed -- 🎨 **Editor integration** - Open worktrees in Cursor, VS Code, or Zed -- 🤖 **AI tool support** - Launch Aider or other AI coding tools +- 📁 **Repository-scoped** - Each repo has independent worktrees +- 🔧 **Configuration over flags** - Set defaults once, use simple commands +- 🎨 **Editor integration** - Open worktrees in Cursor, VS Code, Zed, and more +- 🤖 **AI tool support** - Launch Aider, Claude Code, or other AI coding tools - 📋 **Smart file copying** - Selectively copy configs/env files to new worktrees - 🪝 **Hooks system** - Run custom commands after create/remove - 🌍 **Cross-platform** - Works on macOS, Linux, and Windows (Git Bash) - 🎯 **Shell completions** - Tab completion for Bash, Zsh, and Fish +## Quick Start + +```bash +# Navigate to your git repo +cd ~/GitHub/my-project + +# One-time setup (per repository) +gtr config set gtr.editor.default cursor +gtr config set gtr.ai.default claude + +# Daily workflow +gtr new my-feature # Create worktree folder: my-feature +gtr open my-feature # Open in cursor +gtr ai my-feature # Start claude + +# Navigate to worktree +cd "$(gtr go my-feature)" + +# List all worktrees +gtr list + +# Remove when done +gtr rm my-feature +``` + ## Requirements - **Git** 2.5+ (for `git worktree` support) @@ -115,201 +123,84 @@ echo 'source /path/to/git-worktree-runner/completions/_gtr' >> ~/.zshrc ln -s /path/to/git-worktree-runner/completions/gtr.fish ~/.config/fish/completions/ ``` -## Quick Start - -**Prerequisites:** `cd` into a git repository first. - -**Basic workflow:** -```bash -# Navigate to your git repo -cd ~/GitHub/my-project - -# One-time setup (per repository) -gtr config set gtr.editor.default cursor -gtr config set gtr.ai.default claude - -# Daily workflow - explicit commands -gtr new my-feature # Create worktree -gtr open my-feature # Open in cursor (from config) -gtr ai my-feature # Start claude (from config) - -# Or chain them together -gtr new my-feature && gtr open my-feature && gtr ai my-feature - -# Navigate to worktree -cd "$(gtr go my-feature)" - -# List all worktrees -gtr list - -# Remove when done -gtr rm my-feature -``` - -**Advanced:** -```bash -# Create from specific ref -gtr new hotfix --from v1.2.3 --id 99 - -# Remove with branch deletion -gtr rm my-feature --delete-branch --force -``` - ## Commands -### `gtr new` +Commands accept branch names to identify worktrees. Use `1` to reference the main repo. +Run `gtr help` for full documentation. -Create a new git worktree. IDs are auto-assigned by default. +### `gtr new [options]` -```bash -gtr new [options] - -Options (all optional): - --id Specific worktree ID (rarely needed) - --from Create from specific ref (default: main/master) - --track Track mode: auto|remote|local|none - --no-copy Skip file copying - --no-fetch Skip git fetch - --yes Non-interactive mode -``` - -**Examples:** +Create a new git worktree. Folder is named after the branch. ```bash -# Create worktree (auto-assigns ID) -gtr new my-feature - -# Create from specific ref -gtr new hotfix --from v1.2.3 - -# Then open and start AI -gtr open hotfix -gtr ai hotfix +gtr new my-feature # Creates folder: my-feature +gtr new hotfix --from v1.2.3 # Create from specific ref +gtr new feature/auth # Creates folder: feature-auth ``` -### `gtr open` +**Options:** `--from `, `--track `, `--no-copy`, `--no-fetch`, `--yes` -Open a worktree in an editor. Uses `gtr.editor.default` from config. +### `gtr open [--editor ]` -```bash -gtr open -``` - -**Examples:** +Open worktree in editor (uses `gtr.editor.default` or `--editor` flag). ```bash -# Open by ID (uses gtr.editor.default) -gtr open 2 - -# Open by branch name -gtr open my-feature +gtr open my-feature # Uses configured editor +gtr open my-feature --editor vscode # Override with vscode ``` -### `gtr go` - -Navigate to a worktree directory. Prints path to stdout for shell integration. +### `gtr ai [--ai ] [-- args...]` -```bash -gtr go -``` - -**Examples:** +Start AI coding tool (uses `gtr.ai.default` or `--ai` flag). ```bash -# Change to worktree by ID -cd "$(gtr go 2)" - -# Change to worktree by branch name -cd "$(gtr go my-feature)" +gtr ai my-feature # Uses configured AI tool +gtr ai my-feature --ai aider # Override with aider +gtr ai my-feature -- --model gpt-4 # Pass arguments to tool +gtr ai 1 # Use AI in main repo ``` -### `gtr ai` - -Start an AI coding tool in a worktree. Uses `gtr.ai.default` from config. +### `gtr go ` -```bash -gtr ai [-- args...] -``` - -**Examples:** +Print worktree path for shell navigation. ```bash -# Start AI tool by ID (uses gtr.ai.default) -gtr ai 2 - -# Start by branch name -gtr ai my-feature - -# Pass arguments to the AI tool -gtr ai my-feature -- --model gpt-4 +cd "$(gtr go my-feature)" # Navigate by branch name +cd "$(gtr go 1)" # Navigate to main repo ``` -### `gtr rm` +### `gtr rm ... [options]` -Remove worktree(s). Accepts either ID or branch name. +Remove worktree(s) by branch name. ```bash -gtr rm [...] [options] - -Options: - --delete-branch Also delete the branch - --force Force removal even with uncommitted changes - --yes Non-interactive mode -``` - -**Examples:** - -```bash -# Remove by branch name -gtr rm my-feature - -# Remove by ID -gtr rm 2 - -# Remove and delete branch -gtr rm my-feature --delete-branch - -# Remove multiple worktrees -gtr rm feature-a feature-b hotfix --yes - -# Force remove with uncommitted changes -gtr rm my-feature --force +gtr rm my-feature # Remove one +gtr rm feature-a feature-b # Remove multiple +gtr rm my-feature --delete-branch --force # Delete branch and force ``` -### `gtr list` +**Options:** `--delete-branch`, `--force`, `--yes` -List all git worktrees. +### `gtr list [--porcelain]` -```bash -gtr list [--porcelain|--ids] - -Options: - --porcelain Machine-readable output (tab-separated) - --ids Output only worktree IDs (for scripting) -``` +List all worktrees. Use `--porcelain` for machine-readable output. -### `gtr config` +### `gtr config {get|set|unset} [value] [--global]` -Manage gtr configuration via git config. +Manage configuration via git config. ```bash -gtr config get [--global] -gtr config set [--global] -gtr config unset [--global] +gtr config set gtr.editor.default cursor # Set locally +gtr config set gtr.ai.default claude --global # Set globally +gtr config get gtr.editor.default # Get value ``` -**Examples:** - -```bash -# Set default editor locally -gtr config set gtr.editor.default cursor +### Other Commands -# Set global worktree prefix -gtr config set gtr.worktrees.prefix "wt-" --global - -# Get current value -gtr config get gtr.editor.default -``` +- `gtr doctor` - Health check (verify git, editors, AI tools) +- `gtr adapter` - List available editor & AI adapters +- `gtr clean` - Remove stale worktrees +- `gtr version` - Show version ## Configuration @@ -321,12 +212,9 @@ All configuration is stored via `git config`, making it easy to manage per-repos # Base directory (default: -worktrees) gtr.worktrees.dir = /path/to/worktrees -# Name prefix (default: wt-) +# Folder prefix (default: "") gtr.worktrees.prefix = dev- -# Starting ID (default: 2) -gtr.worktrees.startId = 1 - # Default branch (default: auto-detect) gtr.defaultBranch = main ``` @@ -446,7 +334,6 @@ git config --local gtr.defaultBranch "main" ```bash # Worktree settings git config --local gtr.worktrees.prefix "wt-" -git config --local gtr.worktrees.startId 2 # Editor git config --local gtr.editor.default cursor @@ -467,59 +354,67 @@ git config --local --add gtr.hook.postCreate "pnpm run build" # Set global preferences git config --global gtr.editor.default cursor git config --global gtr.ai.default claude -git config --global gtr.worktrees.startId 2 ``` ## Advanced Usage +### How It Works: Repository Scoping + +**gtr is repository-scoped** - each git repository has its own independent set of worktrees: + +- Run `gtr` commands from within any git repository +- Worktree folders are named after their branch names +- Each repo manages its own worktrees independently +- Switch repos with `cd`, then run `gtr` commands for that repo + ### Working with Multiple Branches ```bash # Terminal 1: Work on feature -gtr new feature-a --id 2 +gtr new feature-a gtr open feature-a # Terminal 2: Review PR -gtr new pr/123 --id 3 +gtr new pr/123 gtr open pr/123 # Terminal 3: Navigate to main branch (repo root) -cd "$(gtr go 1)" # ID 1 is always the repo root +cd "$(gtr go 1)" # Special ID '1' = main repo ``` ### Working with Multiple Repositories -Each repository has its own independent set of worktrees and IDs. Switch repos with `cd`: +Each repository has its own independent set of worktrees. Switch repos with `cd`: ```bash # Frontend repo cd ~/GitHub/frontend gtr list -# ID BRANCH PATH -# 1 main ~/GitHub/frontend -# 2 auth-feature ~/GitHub/frontend-worktrees/wt-2 -# 3 nav-redesign ~/GitHub/frontend-worktrees/wt-3 +# BRANCH PATH +# main [main] ~/GitHub/frontend +# auth-feature ~/GitHub/frontend-worktrees/auth-feature +# nav-redesign ~/GitHub/frontend-worktrees/nav-redesign gtr open auth-feature # Open frontend auth work gtr ai nav-redesign # AI on frontend nav work -# Backend repo (separate worktrees, separate IDs) +# Backend repo (separate worktrees) cd ~/GitHub/backend gtr list -# ID BRANCH PATH -# 1 main ~/GitHub/backend -# 2 api-auth ~/GitHub/backend-worktrees/wt-2 # Different ID 2! -# 5 websockets ~/GitHub/backend-worktrees/wt-5 +# BRANCH PATH +# main [main] ~/GitHub/backend +# api-auth ~/GitHub/backend-worktrees/api-auth +# websockets ~/GitHub/backend-worktrees/websockets gtr open api-auth # Open backend auth work gtr ai websockets # AI on backend websockets # Switch back to frontend cd ~/GitHub/frontend -gtr open auth-feature # Opens frontend auth (use branch names!) +gtr open auth-feature # Opens frontend auth ``` -**Key point:** IDs are per-repository, not global. ID 2 in frontend ≠ ID 2 in backend. +**Key point:** Each repository has its own worktrees. Use branch names to identify worktrees. ### Custom Workflows with Hooks diff --git a/bin/gtr b/bin/gtr index 12bc3c7..ff9c9ef 100755 --- a/bin/gtr +++ b/bin/gtr @@ -80,11 +80,9 @@ main() { # Create command cmd_create() { - local worktree_id="" local branch_name="" local from_ref="" local track_mode="auto" - local explicit_id=0 local skip_copy=0 local skip_fetch=0 local yes_mode=0 @@ -92,11 +90,6 @@ cmd_create() { # Parse flags and arguments while [ $# -gt 0 ]; do case "$1" in - --id) - worktree_id="$2" - explicit_id=1 - shift 2 - ;; --from) from_ref="$2" shift 2 @@ -135,15 +128,9 @@ cmd_create() { local repo_root repo_root=$(discover_repo_root) || exit 1 - local base_dir prefix start_id + local base_dir prefix base_dir=$(resolve_base_dir "$repo_root") - prefix=$(cfg_default gtr.worktrees.prefix GTR_WORKTREES_PREFIX "wt-") - start_id=$(cfg_default gtr.worktrees.startId GTR_WORKTREES_STARTID "2") - - # Auto-assign ID if not explicitly provided (NEW DEFAULT BEHAVIOR) - if [ "$explicit_id" -eq 0 ]; then - worktree_id=$(next_available_id "$base_dir" "$prefix" "$start_id") - fi + prefix=$(cfg_default gtr.worktrees.prefix GTR_WORKTREES_PREFIX "") # Get branch name if not provided if [ -z "$branch_name" ]; then @@ -151,7 +138,7 @@ cmd_create() { log_error "Branch name required in non-interactive mode" exit 1 fi - branch_name=$(prompt_input "Enter branch name for worktree ${prefix}${worktree_id}:") + branch_name=$(prompt_input "Enter branch name:") if [ -z "$branch_name" ]; then log_error "Branch name required" exit 1 @@ -163,15 +150,17 @@ cmd_create() { from_ref=$(resolve_default_branch "$repo_root") fi - local worktree_path - worktree_path="$base_dir/${prefix}${worktree_id}" + # Construct worktree path using sanitized branch name + local sanitized_name worktree_path + sanitized_name=$(sanitize_branch_name "$branch_name") + worktree_path="$base_dir/${prefix}${sanitized_name}" - log_step "Creating worktree: ${prefix}${worktree_id}" + log_step "Creating worktree: $sanitized_name" echo "Location: $worktree_path" echo "Branch: $branch_name" # Create the worktree - if ! create_worktree "$base_dir" "$prefix" "$worktree_id" "$branch_name" "$from_ref" "$track_mode" "$skip_fetch"; then + if ! create_worktree "$base_dir" "$prefix" "$branch_name" "$from_ref" "$track_mode" "$skip_fetch"; then exit 1 fi @@ -243,23 +232,23 @@ cmd_remove() { local repo_root base_dir prefix repo_root=$(discover_repo_root) || exit 1 base_dir=$(resolve_base_dir "$repo_root") - prefix=$(cfg_default gtr.worktrees.prefix GTR_WORKTREES_PREFIX "wt-") + prefix=$(cfg_default gtr.worktrees.prefix GTR_WORKTREES_PREFIX "") for identifier in $identifiers; do - # Resolve target (supports both ID and branch name) - local target worktree_id worktree_path branch_name + # Resolve target branch + local target is_main worktree_path branch_name target=$(resolve_target "$identifier" "$repo_root" "$base_dir" "$prefix") || continue - worktree_id=$(echo "$target" | cut -f1) + is_main=$(echo "$target" | cut -f1) worktree_path=$(echo "$target" | cut -f2) branch_name=$(echo "$target" | cut -f3) - # Cannot remove ID 1 (main repository) - if [ "$worktree_id" = "1" ]; then - log_error "Cannot remove main repository (ID 1)" + # Cannot remove main repository + if [ "$is_main" = "1" ]; then + log_error "Cannot remove main repository" continue fi - log_step "Removing worktree: ${prefix}${worktree_id} (branch: $branch_name)" + log_step "Removing worktree: $branch_name" # Remove the worktree if ! remove_worktree "$worktree_path" "$force"; then @@ -298,24 +287,24 @@ cmd_go() { local repo_root base_dir prefix repo_root=$(discover_repo_root) || exit 1 base_dir=$(resolve_base_dir "$repo_root") - prefix=$(cfg_default gtr.worktrees.prefix GTR_WORKTREES_PREFIX "wt-") + prefix=$(cfg_default gtr.worktrees.prefix GTR_WORKTREES_PREFIX "") - # Resolve target (supports both ID and branch name) - local target worktree_id worktree_path branch + # Resolve target branch + local target is_main worktree_path branch target=$(resolve_target "$identifier" "$repo_root" "$base_dir" "$prefix") || exit 1 - worktree_id=$(echo "$target" | cut -f1) + is_main=$(echo "$target" | cut -f1) worktree_path=$(echo "$target" | cut -f2) branch=$(echo "$target" | cut -f3) # Human messages to stderr so stdout can be used in command substitution - if [ "$worktree_id" = "1" ]; then - echo "Repo root (id 1)" >&2 + if [ "$is_main" = "1" ]; then + echo "Main repo" >&2 else - echo "Worktree ${prefix}${worktree_id}" >&2 + echo "Worktree: $branch" >&2 fi echo "Branch: $branch" >&2 - # Print path to stdout for shell integration: cd "$(gtr go 2)" + # Print path to stdout for shell integration: cd "$(gtr go my-feature)" printf "%s\n" "$worktree_path" } @@ -357,12 +346,11 @@ cmd_open() { local repo_root base_dir prefix repo_root=$(discover_repo_root) || exit 1 base_dir=$(resolve_base_dir "$repo_root") - prefix=$(cfg_default gtr.worktrees.prefix GTR_WORKTREES_PREFIX "wt-") + prefix=$(cfg_default gtr.worktrees.prefix GTR_WORKTREES_PREFIX "") - # Resolve target (supports both ID and branch name) - local target worktree_id worktree_path branch + # Resolve target branch + local target worktree_path branch target=$(resolve_target "$identifier" "$repo_root" "$base_dir" "$prefix") || exit 1 - worktree_id=$(echo "$target" | cut -f1) worktree_path=$(echo "$target" | cut -f2) branch=$(echo "$target" | cut -f3) @@ -432,16 +420,15 @@ cmd_ai() { local repo_root base_dir prefix repo_root=$(discover_repo_root) || exit 1 base_dir=$(resolve_base_dir "$repo_root") - prefix=$(cfg_default gtr.worktrees.prefix GTR_WORKTREES_PREFIX "wt-") + prefix=$(cfg_default gtr.worktrees.prefix GTR_WORKTREES_PREFIX "") - # Resolve target (supports both ID and branch name) - local target worktree_id worktree_path branch + # Resolve target branch + local target worktree_path branch target=$(resolve_target "$identifier" "$repo_root" "$base_dir" "$prefix") || exit 1 - worktree_id=$(echo "$target" | cut -f1) worktree_path=$(echo "$target" | cut -f2) branch=$(echo "$target" | cut -f3) - log_step "Starting $ai_tool in worktree: ${prefix}${worktree_id}" + log_step "Starting $ai_tool for: $branch" echo "Directory: $worktree_path" echo "Branch: $branch" @@ -451,7 +438,6 @@ cmd_ai() { # List command cmd_list() { local porcelain=0 - local ids_only=0 # Parse flags while [ $# -gt 0 ]; do @@ -460,10 +446,6 @@ cmd_list() { porcelain=1 shift ;; - --ids) - ids_only=1 - shift - ;; *) shift ;; @@ -473,42 +455,28 @@ cmd_list() { local repo_root base_dir prefix repo_root=$(discover_repo_root) 2>/dev/null || return 0 base_dir=$(resolve_base_dir "$repo_root") - prefix=$(cfg_default gtr.worktrees.prefix GTR_WORKTREES_PREFIX "wt-") - - # IDs only (for completions) - if [ "$ids_only" -eq 1 ]; then - # Always include ID 1 (repo root) - echo "1" - - if [ -d "$base_dir" ]; then - find "$base_dir" -maxdepth 1 -type d -name "${prefix}*" 2>/dev/null | while IFS= read -r dir; do - basename "$dir" | sed "s/^${prefix}//" - done | grep -E '^[0-9]+$' | sort -n - fi - return 0 - fi + prefix=$(cfg_default gtr.worktrees.prefix GTR_WORKTREES_PREFIX "") # Machine-readable output (porcelain) if [ "$porcelain" -eq 1 ]; then - # Always include ID 1 (repo root) + # Output: pathbranchstatus local branch status # Try --show-current (Git 2.22+), fallback to rev-parse for older Git branch=$(git -C "$repo_root" branch --show-current 2>/dev/null) [ -z "$branch" ] && branch=$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null) [ -z "$branch" ] || [ "$branch" = "HEAD" ] && branch="(detached)" status=$(worktree_status "$repo_root") - printf "%s\t%s\t%s\t%s\n" "1" "$repo_root" "$branch" "$status" + printf "%s\t%s\t%s\n" "$repo_root" "$branch" "$status" if [ -d "$base_dir" ]; then - # Find all worktree directories and output: idpathbranchstatus + # Find all worktree directories and output: pathbranchstatus find "$base_dir" -maxdepth 1 -type d -name "${prefix}*" 2>/dev/null | while IFS= read -r dir; do - local id branch status - id=$(basename "$dir" | sed "s/^${prefix}//") + local branch status branch=$(current_branch "$dir") [ -z "$branch" ] && branch="(detached)" status=$(worktree_status "$dir") - printf "%s\t%s\t%s\t%s\n" "$id" "$dir" "$branch" "$status" - done | LC_ALL=C sort -n -k1,1 + printf "%s\t%s\t%s\n" "$dir" "$branch" "$status" + done | LC_ALL=C sort -k2,2 fi return 0 fi @@ -516,26 +484,25 @@ cmd_list() { # Human-readable output - table format echo "Git Worktrees" echo "" - printf "%-6s %-25s %s\n" "ID" "BRANCH" "PATH" - printf "%-6s %-25s %s\n" "──" "──────" "────" + printf "%-30s %s\n" "BRANCH" "PATH" + printf "%-30s %s\n" "------" "----" - # Always show repo root as ID 1 + # Always show repo root first local branch # Try --show-current (Git 2.22+), fallback to rev-parse for older Git branch=$(git -C "$repo_root" branch --show-current 2>/dev/null) [ -z "$branch" ] && branch=$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null) [ -z "$branch" ] || [ "$branch" = "HEAD" ] && branch="(detached)" - printf "%-6s %-25s %s\n" "1" "$branch" "$repo_root" + printf "%-30s %s\n" "$branch [main repo]" "$repo_root" - # Show worktrees + # Show worktrees sorted by branch name if [ -d "$base_dir" ]; then find "$base_dir" -maxdepth 1 -type d -name "${prefix}*" 2>/dev/null | while IFS= read -r dir; do - local id branch - id=$(basename "$dir" | sed "s/^${prefix}//") + local branch branch=$(current_branch "$dir") [ -z "$branch" ] && branch="(detached)" - printf "%-6s %-25s %s\n" "$id" "$branch" "$dir" - done | LC_ALL=C sort -n -k1,1 + printf "%-30s %s\n" "$branch" "$dir" + done | LC_ALL=C sort -k1,1 fi echo "" @@ -555,7 +522,7 @@ cmd_clean() { local repo_root base_dir prefix repo_root=$(discover_repo_root) || exit 1 base_dir=$(resolve_base_dir "$repo_root") - prefix=$(cfg_default gtr.worktrees.prefix GTR_WORKTREES_PREFIX "wt-") + prefix=$(cfg_default gtr.worktrees.prefix GTR_WORKTREES_PREFIX "") if [ ! -d "$base_dir" ]; then log_info "No worktrees directory to clean" @@ -612,7 +579,7 @@ cmd_doctor() { # Check worktree base dir local base_dir prefix base_dir=$(resolve_base_dir "$repo_root") - prefix=$(cfg_default gtr.worktrees.prefix GTR_WORKTREES_PREFIX "wt-") + prefix=$(cfg_default gtr.worktrees.prefix GTR_WORKTREES_PREFIX "") if [ -d "$base_dir" ]; then local count @@ -841,7 +808,7 @@ QUICK START: cd ~/your-repo # Navigate to git repo first gtr config set gtr.editor.default cursor # One-time setup gtr config set gtr.ai.default claude # One-time setup - gtr new my-feature # Auto-assigns ID, uses defaults + gtr new my-feature # Creates worktree in folder "my-feature" gtr open my-feature # Opens in cursor gtr ai my-feature # Starts claude gtr rm my-feature # Remove when done @@ -849,40 +816,41 @@ QUICK START: ──────────────────────────────────────────────────────────────────────────────── KEY CONCEPTS: - • Each git repo has independent worktrees and IDs - • Main repo is always ID 1 (accessible via: gtr go 1, gtr open 1) - • New worktrees auto-assign IDs starting at 2 (configurable) - • Commands accept EITHER ID numbers OR branch names - Examples: gtr open 2 OR gtr open my-feature + • Worktree folders are named after the branch name + • Main repo is accessible via special ID '1' (e.g., gtr go 1, gtr open 1) + • Commands accept branch names to identify worktrees + Example: gtr open my-feature, gtr go feature/user-auth ──────────────────────────────────────────────────────────────────────────────── CORE COMMANDS (daily workflow): new [options] - Create a new worktree (auto-assigns ID by default) - --id : specify exact ID (rarely needed) + Create a new worktree (folder named after branch) --from : create from specific ref --track : tracking mode (auto|remote|local|none) --no-copy: skip file copying --no-fetch: skip git fetch --yes: non-interactive mode - open [--editor ] + open [--editor ] Open worktree in editor (uses gtr.editor.default or --editor) + Special: use '1' to open repo root - ai [--ai ] [-- args...] + ai [--ai ] [-- args...] Start AI coding tool in worktree (uses gtr.ai.default or --ai) + Special: use '1' to open repo root - go - Navigate to worktree (prints path for: cd "$(gtr go 2)") + go + Navigate to worktree (prints path for: cd "$(gtr go my-feature)") + Special: use '1' for repo root - list [--porcelain|--ids] - List all worktrees (ID 1 = repo root) + list [--porcelain] + List all worktrees Aliases: ls - rm [...] [options] - Remove worktree(s) + rm [...] [options] + Remove worktree(s) by branch name --delete-branch: also delete the branch --force: force removal (dirty worktree) --yes: skip confirmation @@ -916,17 +884,13 @@ WORKFLOW EXAMPLES: gtr config set gtr.ai.default claude # Daily workflow - gtr new feature/user-auth # Create worktree + gtr new feature/user-auth # Create worktree (folder: feature-user-auth) gtr open feature/user-auth # Open in editor gtr ai feature/user-auth # Start AI tool # Navigate to worktree directory cd "$(gtr go feature/user-auth)" - # Use ID instead of branch name (same result) - gtr open 2 - gtr ai 2 - # Override defaults with flags gtr open feature/user-auth --editor vscode gtr ai feature/user-auth --ai aider @@ -946,8 +910,7 @@ WORKFLOW EXAMPLES: CONFIGURATION OPTIONS: gtr.worktrees.dir Worktrees base directory - gtr.worktrees.prefix Worktree name prefix (default: wt-) - gtr.worktrees.startId Starting ID (default: 2) + gtr.worktrees.prefix Worktree folder prefix (default: "") gtr.defaultBranch Default branch (default: auto) gtr.editor.default Default editor Options: cursor, vscode, zed, idea, pycharm, diff --git a/completions/_gtr b/completions/_gtr index 73921ad..5e046b3 100644 --- a/completions/_gtr +++ b/completions/_gtr @@ -19,24 +19,21 @@ _gtr() { 'help:Show help' ) - local -a worktree_ids branches all_options - # Use gtr list --ids for config-aware completion - worktree_ids=(${(f)"$(command gtr list --ids 2>/dev/null)"}) + local -a branches all_options # Get branch names branches=(${(f)"$(git branch --format='%(refname:short)' 2>/dev/null)"}) - # Combine IDs and branches - all_options=("${worktree_ids[@]}" "${branches[@]}") + # Add special ID '1' for main repo + all_options=("1" "${branches[@]}") if (( CURRENT == 2 )); then _describe 'commands' commands elif (( CURRENT == 3 )); then case "$words[2]" in go|open|ai|rm) - _describe 'worktree IDs or branches' all_options + _describe 'branch names' all_options ;; new) _arguments \ - '--id[Worktree ID]:id:' \ '--from[Base ref]:ref:' \ '--track[Track mode]:mode:(auto remote local none)' \ '--no-copy[Skip file copying]' \ @@ -61,7 +58,6 @@ _gtr() { _values 'config key' \ 'gtr.worktrees.dir' \ 'gtr.worktrees.prefix' \ - 'gtr.worktrees.startId' \ 'gtr.defaultBranch' \ 'gtr.editor.default' \ 'gtr.ai.default' \ diff --git a/completions/gtr.bash b/completions/gtr.bash index 4f8c590..700b6f1 100644 --- a/completions/gtr.bash +++ b/completions/gtr.bash @@ -13,15 +13,14 @@ _gtr_completion() { return 0 fi - # Commands that take worktree IDs or branch names + # Commands that take branch names or '1' for main repo case "$cmd" in go|open|ai|rm) if [ "$cword" -eq 2 ]; then - # Complete with both IDs and branch names - local ids branches all_options - ids=$(command gtr list --ids 2>/dev/null || true) + # Complete with branch names and special ID '1' for main repo + local branches all_options branches=$(git branch --format='%(refname:short)' 2>/dev/null || true) - all_options="$ids $branches" + all_options="1 $branches" COMPREPLY=($(compgen -W "$all_options" -- "$cur")) elif [[ "$cur" == -* ]]; then case "$cmd" in @@ -43,7 +42,7 @@ _gtr_completion() { if [ "$cword" -eq 2 ]; then COMPREPLY=($(compgen -W "get set unset" -- "$cur")) elif [ "$cword" -eq 3 ]; then - COMPREPLY=($(compgen -W "gtr.worktrees.dir gtr.worktrees.prefix gtr.worktrees.startId gtr.defaultBranch gtr.editor.default gtr.ai.default gtr.copy.include gtr.copy.exclude gtr.hook.postCreate gtr.hook.postRemove" -- "$cur")) + COMPREPLY=($(compgen -W "gtr.worktrees.dir gtr.worktrees.prefix gtr.defaultBranch gtr.editor.default gtr.ai.default gtr.copy.include gtr.copy.exclude gtr.hook.postCreate gtr.hook.postRemove" -- "$cur")) fi ;; esac diff --git a/completions/gtr.fish b/completions/gtr.fish index 56bf4a3..f18ed61 100644 --- a/completions/gtr.fish +++ b/completions/gtr.fish @@ -16,7 +16,6 @@ complete -c gtr -f -n "__fish_use_subcommand" -a "version" -d "Show version" complete -c gtr -f -n "__fish_use_subcommand" -a "help" -d "Show help" # New command options -complete -c gtr -n "__fish_seen_subcommand_from new" -l id -d "Worktree ID (rarely needed)" -r complete -c gtr -n "__fish_seen_subcommand_from new" -l from -d "Base ref" -r complete -c gtr -n "__fish_seen_subcommand_from new" -l track -d "Track mode" -r -a "auto remote local none" complete -c gtr -n "__fish_seen_subcommand_from new" -l no-copy -d "Skip file copying" @@ -32,8 +31,7 @@ complete -c gtr -n "__fish_seen_subcommand_from rm" -l yes -d "Non-interactive m complete -c gtr -n "__fish_seen_subcommand_from config" -f -a "get set unset" complete -c gtr -n "__fish_seen_subcommand_from config; and __fish_seen_subcommand_from get set unset" -f -a "\ gtr.worktrees.dir\t'Worktrees base directory' - gtr.worktrees.prefix\t'Worktree name prefix' - gtr.worktrees.startId\t'Starting ID' + gtr.worktrees.prefix\t'Worktree folder prefix' gtr.defaultBranch\t'Default branch' gtr.editor.default\t'Default editor' gtr.ai.default\t'Default AI tool' @@ -43,13 +41,13 @@ complete -c gtr -n "__fish_seen_subcommand_from config; and __fish_seen_subcomma gtr.hook.postRemove\t'Post-remove hook' " -# Helper function to get worktree IDs and branch names -function __gtr_worktree_ids_and_branches - # Get worktree IDs - command gtr list --ids 2>/dev/null +# Helper function to get branch names and special '1' for main repo +function __gtr_worktree_branches + # Special ID for main repo + echo "1" # Get branch names git branch --format='%(refname:short)' 2>/dev/null end -# Complete worktree IDs and branch names for commands that need them -complete -c gtr -n "__fish_seen_subcommand_from go open ai rm" -f -a "(__gtr_worktree_ids_and_branches)" +# Complete branch names for commands that need them +complete -c gtr -n "__fish_seen_subcommand_from go open ai rm" -f -a "(__gtr_worktree_branches)" diff --git a/lib/core.sh b/lib/core.sh index 6fa87fc..95a62d8 100644 --- a/lib/core.sh +++ b/lib/core.sh @@ -16,6 +16,17 @@ discover_repo_root() { printf "%s" "$root" } +# Sanitize branch name for use as directory name +# Usage: sanitize_branch_name branch_name +# Converts special characters to hyphens for valid folder names +sanitize_branch_name() { + local branch="$1" + + # Replace slashes, spaces, and other problematic chars with hyphens + # Remove any leading/trailing hyphens + printf "%s" "$branch" | sed -e 's/[\/\\ :*?"<>|]/-/g' -e 's/^-*//' -e 's/-*$//' +} + # Resolve the base directory for worktrees # Usage: resolve_base_dir repo_root resolve_base_dir() { @@ -73,21 +84,6 @@ resolve_default_branch() { fi } -# Find the next available worktree ID -# Usage: next_available_id base_dir prefix [start_id] -next_available_id() { - local base_dir="$1" - local prefix="$2" - local start_id="${3:-2}" - local id="$start_id" - - while [ -d "$base_dir/${prefix}${id}" ]; do - id=$((id + 1)) - done - - printf "%s" "$id" -} - # Get the current branch of a worktree # Usage: current_branch worktree_path current_branch() { @@ -165,87 +161,79 @@ EOF printf "%s" "$status" } -# Resolve a worktree target from ID or branch name +# Resolve a worktree target from branch name or special ID '1' for main repo # Usage: resolve_target identifier repo_root base_dir prefix -# Returns: tab-separated "id\tpath\tbranch" on success +# Returns: tab-separated "is_main\tpath\tbranch" on success (is_main: 1 for main repo, 0 for worktrees) # Exit code: 0 on success, 1 if not found resolve_target() { local identifier="$1" local repo_root="$2" local base_dir="$3" local prefix="$4" - local id path branch - - # Check if identifier is numeric (ID) or a branch name - if echo "$identifier" | grep -qE '^[0-9]+$'; then - # Numeric ID - id="$identifier" - - if [ "$id" = "1" ]; then - # ID 1 is always the repo root - path="$repo_root" - # Try --show-current (Git 2.22+), fallback to rev-parse for older Git - branch=$(git -C "$repo_root" branch --show-current 2>/dev/null) - [ -z "$branch" ] && branch=$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null) - printf "%s\t%s\t%s\n" "$id" "$path" "$branch" - return 0 - fi + local id path branch sanitized_name - # Other IDs map to worktree directories - path="$base_dir/${prefix}${id}" - if [ ! -d "$path" ]; then - log_error "Worktree not found: ${prefix}${id}" - return 1 - fi - branch=$(current_branch "$path") - printf "%s\t%s\t%s\n" "$id" "$path" "$branch" - return 0 - else - # Branch name - search for matching worktree - # First check if it's the current branch in repo root + # Special case: ID 1 is always the repo root + if [ "$identifier" = "1" ]; then + path="$repo_root" # Try --show-current (Git 2.22+), fallback to rev-parse for older Git branch=$(git -C "$repo_root" branch --show-current 2>/dev/null) [ -z "$branch" ] && branch=$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null) - if [ "$branch" = "$identifier" ]; then - printf "1\t%s\t%s\n" "$repo_root" "$identifier" - return 0 - fi + printf "1\t%s\t%s\n" "$path" "$branch" + return 0 + fi - # Search worktree directories for matching branch - if [ -d "$base_dir" ]; then - for dir in "$base_dir/${prefix}"*; do - [ -d "$dir" ] || continue - branch=$(current_branch "$dir") - if [ "$branch" = "$identifier" ]; then - id=$(basename "$dir" | sed "s/^${prefix}//") - printf "%s\t%s\t%s\n" "$id" "$dir" "$branch" - return 0 - fi - done - fi + # For all other identifiers, treat as branch name + # First check if it's the current branch in repo root (if not ID 1) + branch=$(git -C "$repo_root" branch --show-current 2>/dev/null) + [ -z "$branch" ] && branch=$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null) + if [ "$branch" = "$identifier" ]; then + printf "1\t%s\t%s\n" "$repo_root" "$identifier" + return 0 + fi - log_error "Worktree not found for branch: $identifier" - return 1 + # Try direct path match with sanitized branch name + sanitized_name=$(sanitize_branch_name "$identifier") + path="$base_dir/${prefix}${sanitized_name}" + if [ -d "$path" ]; then + branch=$(current_branch "$path") + printf "0\t%s\t%s\n" "$path" "$branch" + return 0 fi + + # Search worktree directories for matching branch (fallback) + if [ -d "$base_dir" ]; then + for dir in "$base_dir/${prefix}"*; do + [ -d "$dir" ] || continue + branch=$(current_branch "$dir") + if [ "$branch" = "$identifier" ]; then + printf "0\t%s\t%s\n" "$dir" "$branch" + return 0 + fi + done + fi + + log_error "Worktree not found for branch: $identifier" + return 1 } # Create a new git worktree -# Usage: create_worktree base_dir prefix id branch_name from_ref track_mode [skip_fetch] +# Usage: create_worktree base_dir prefix branch_name from_ref track_mode [skip_fetch] # track_mode: auto, remote, local, or none # skip_fetch: 0 (default, fetch) or 1 (skip) create_worktree() { local base_dir="$1" local prefix="$2" - local id="$3" - local branch_name="$4" - local from_ref="$5" - local track_mode="${6:-auto}" - local skip_fetch="${7:-0}" - local worktree_path="$base_dir/${prefix}${id}" + local branch_name="$3" + local from_ref="$4" + local track_mode="${5:-auto}" + local skip_fetch="${6:-0}" + local sanitized_name + sanitized_name=$(sanitize_branch_name "$branch_name") + local worktree_path="$base_dir/${prefix}${sanitized_name}" # Check if worktree already exists if [ -d "$worktree_path" ]; then - log_error "Worktree ${prefix}${id} already exists at $worktree_path" + log_error "Worktree $sanitized_name already exists at $worktree_path" return 1 fi diff --git a/templates/gtr.config.example b/templates/gtr.config.example index f4e8380..89e9d35 100644 --- a/templates/gtr.config.example +++ b/templates/gtr.config.example @@ -7,14 +7,10 @@ # Default: -worktrees # gtr.worktrees.dir = my-worktrees -# Prefix for worktree directory names -# Default: wt- +# Prefix for worktree directory names (folders named: {prefix}{branch-name}) +# Default: "" (empty, folders named after branch) # gtr.worktrees.prefix = dev- -# Starting ID for auto-assigned worktrees -# Default: 2 -# gtr.worktrees.startId = 1 - # Default branch to create new branches from # Options: auto (detect from origin/HEAD), main, master, or any branch name # Default: auto @@ -64,7 +60,6 @@ # git config --local --add gtr.hook.postCreate "pnpm install" # For all repositories (global): -# git config --global gtr.worktrees.startId 2 # git config --global gtr.editor.default vscode # Or use gtr's config command: diff --git a/templates/setup-example.sh b/templates/setup-example.sh index c5841e0..fe4d754 100755 --- a/templates/setup-example.sh +++ b/templates/setup-example.sh @@ -7,8 +7,7 @@ set -e echo "🔧 Configuring gtr for this repository..." # Worktree settings -git config --local gtr.worktrees.prefix "wt-" -git config --local gtr.worktrees.startId 2 +git config --local gtr.worktrees.prefix "" git config --local gtr.defaultBranch "auto" # Editor (change to your preference: cursor, vscode, zed) From 9927d083966277104c578d4f473452f78bc59c74 Mon Sep 17 00:00:00 2001 From: Hans Elizaga Date: Mon, 6 Oct 2025 00:55:31 -0700 Subject: [PATCH 19/19] Remove CHANGELOG.md and update README.md for clarity - Deleted CHANGELOG.md as it was no longer needed for project documentation. - Revised README.md to enhance clarity in the comparison table and AI tools section, ensuring consistent formatting and improved readability. --- CHANGELOG.md | 42 ------------------------------------------ README.md | 32 ++++++++++++++++---------------- 2 files changed, 16 insertions(+), 58 deletions(-) delete mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 682e305..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,42 +0,0 @@ -# Changelog - -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [1.0.0] - 2025-01-XX - -### Added - -- **Repository-scoped worktrees** - Each git repo has independent worktrees -- **Branch-based folder naming** - Worktree folders are named after their branch names -- **Branch-name UX** - All commands accept branch names to identify worktrees (`gtr open my-feature`) -- **Explicit command design** - Each command does one thing (`new` creates, `open` opens, `ai` starts AI). No auto-behavior or override flags -- **Config-based defaults** - Set `gtr.editor.default` and `gtr.ai.default` once, use everywhere without flags -- **Editor adapters** - Support for Cursor, VS Code, and Zed -- **AI tool adapters** - Support for Aider, Claude Code, Continue, Codex, and Cursor AI -- **Smart file copying** - Selectively copy files to new worktrees via `gtr.copy.include` and `gtr.copy.exclude` globs -- **Hooks system** - Run custom commands after worktree creation (`gtr.hook.postCreate`) and removal (`gtr.hook.postRemove`) -- **Shell completions** - Tab completion for Bash, Zsh, and Fish -- **Cross-platform support** - Works on macOS, Linux, and Windows (Git Bash/WSL) -- **Utility commands**: - - `gtr new ` - Create worktree with smart branch tracking - - `gtr go ` - Navigate to worktree (shell integration) - - `gtr open ` - Open in editor - - `gtr ai ` - Start AI coding tool - - `gtr rm ` - Remove worktree(s) - - `gtr list` - List all worktrees with human/machine-readable output - - `gtr clean` - Remove stale worktrees - - `gtr doctor` - Health check for git, editors, and AI tools - - `gtr adapter` - List available editor/AI adapters - - `gtr config` - Manage git-config based settings - -### Technical - -- **POSIX-sh compliance** - Pure shell script with zero external dependencies beyond git -- **Modular architecture** - Clean separation of core, config, platform, UI, copy, and hooks logic -- **Adapter pattern** - Pluggable editor and AI tool integrations -- **Stream separation** - Data to stdout, messages to stderr for composability - -[1.0.0]: https://github.com/coderabbitai/git-worktree-runner/releases/tag/v1.0.0 diff --git a/README.md b/README.md index badf4b3..083a722 100644 --- a/README.md +++ b/README.md @@ -21,16 +21,16 @@ Git worktrees let you check out multiple branches at once in separate directorie While `git worktree` is powerful, it's verbose and manual. `gtr` adds quality-of-life features for modern development: -| Task | With `git worktree` | With `gtr` | -| ------------------ | ------------------------------------------ | ---------------------------------- | -| Create worktree | `git worktree add ../repo-feature feature` | `gtr new feature` | -| Open in editor | `cd ../repo-feature && cursor .` | `gtr open feature` | -| Start AI tool | `cd ../repo-feature && aider` | `gtr ai feature` | -| Copy config files | Manual copy/paste | Auto-copy via `gtr.copy.include` | -| Run build steps | Manual `npm install && npm run build` | Auto-run via `gtr.hook.postCreate` | -| List worktrees | `git worktree list` (shows paths) | `gtr list` (shows branches + status) | -| Switch to worktree | `cd ../repo-feature` | `cd "$(gtr go feature)"` | -| Clean up | `git worktree remove ../repo-feature` | `gtr rm feature` | +| Task | With `git worktree` | With `gtr` | +| ------------------ | ------------------------------------------ | ------------------------------------ | +| Create worktree | `git worktree add ../repo-feature feature` | `gtr new feature` | +| Open in editor | `cd ../repo-feature && cursor .` | `gtr open feature` | +| Start AI tool | `cd ../repo-feature && aider` | `gtr ai feature` | +| Copy config files | Manual copy/paste | Auto-copy via `gtr.copy.include` | +| Run build steps | Manual `npm install && npm run build` | Auto-run via `gtr.hook.postCreate` | +| List worktrees | `git worktree list` (shows paths) | `gtr list` (shows branches + status) | +| Switch to worktree | `cd ../repo-feature` | `cd "$(gtr go feature)"` | +| Clean up | `git worktree remove ../repo-feature` | `gtr rm feature` | **TL;DR:** `gtr` wraps `git worktree` with quality-of-life features for modern development workflows (AI tools, editors, automation). @@ -241,12 +241,12 @@ gtr.ai.default = none **Supported AI Tools:** -| Tool | Install | Use Case | Set as Default | -| ------------------------------------------------- | ------------------------------------------------- | ------------------------------------ | ------------------------------------- | -| **[Aider](https://aider.chat)** | `pip install aider-chat` | Pair programming, edit files with AI | `gtr config set gtr.ai.default aider` | -| **[Claude Code](https://claude.com/claude-code)** | Install from claude.com | Terminal-native coding agent | `gtr config set gtr.ai.default claude` | -| **[Codex CLI](https://github.com/openai/codex)** | `npm install -g @openai/codex` | OpenAI coding assistant | `gtr config set gtr.ai.default codex` | -| **[Cursor](https://cursor.com)** | Install from cursor.com | AI-powered editor with CLI agent | `gtr config set gtr.ai.default cursor` | +| Tool | Install | Use Case | Set as Default | +| ------------------------------------------------- | ------------------------------------------------- | ------------------------------------ | ---------------------------------------- | +| **[Aider](https://aider.chat)** | `pip install aider-chat` | Pair programming, edit files with AI | `gtr config set gtr.ai.default aider` | +| **[Claude Code](https://claude.com/claude-code)** | Install from claude.com | Terminal-native coding agent | `gtr config set gtr.ai.default claude` | +| **[Codex CLI](https://github.com/openai/codex)** | `npm install -g @openai/codex` | OpenAI coding assistant | `gtr config set gtr.ai.default codex` | +| **[Cursor](https://cursor.com)** | Install from cursor.com | AI-powered editor with CLI agent | `gtr config set gtr.ai.default cursor` | | **[Continue](https://continue.dev)** | See [docs](https://docs.continue.dev/cli/install) | Open-source coding agent | `gtr config set gtr.ai.default continue` | **Examples:**