diff --git a/README.md b/README.md index 4b8638d..5ef8d25 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,15 @@ A lightweight Python CLI tool to simplify Git worktree management. ## Overview -`wt` makes working with Git worktrees effortless by providing an intuitive command-line interface for creating, listing, and deleting worktrees. It automatically generates consistent paths and can optionally open new worktrees in your IDE or terminal. +`wt` makes working with Git worktrees effortless by providing an intuitive command-line interface for adding, listing, and removing worktrees. It automatically generates consistent paths and can optionally open new worktrees in your IDE or terminal. ## Features -- **Simple Worktree Creation**: Create worktrees with automatic path generation +- **Simple Worktree Creation**: Add worktrees with automatic path generation - **Smart Path Management**: Auto-generates paths as `../_` - **IDE Integration**: Open worktrees directly in your favorite IDE (VS Code, PyCharm, Cursor, etc.) - **Terminal Integration**: Launch new iTerm2 tabs on macOS pointing to your worktree -- **Easy Management**: List and delete worktrees with simple commands +- **Easy Management**: List and remove worktrees with simple commands - **Branch Handling**: Automatically creates new branches or checks out existing ones - **Cross-Platform**: Works on any system with Python 3.12+ and Git @@ -63,23 +63,23 @@ wt --version ## Usage -### Create Worktree +### Add Worktree -Create a new worktree for a branch: +Add a new worktree for a branch: ```bash -# Basic usage - creates worktree only -wt create feature-x +# Basic usage - adds worktree only +wt add feature-x # Creates: ../git-worktree-cli_feature-x -# Create and open in terminal (iTerm2 on macOS) -wt create feature-y --mode terminal +# Add and open in VS Code +wt add feature-y --ide code -# Create and open in VS Code -wt create feature-z --mode ide --ide code +# Add and start Claude session +wt add feature-z --claude -# Create and open in default IDE (auto-detects: code, cursor, pycharm, subl, atom) -wt create feature-w --mode ide +# Add and open in default IDE (auto-detects: code, cursor, pycharm, subl, atom) +wt add feature-w --ide ``` **Path Generation**: Worktrees are created at `../_` @@ -92,6 +92,8 @@ Display all worktrees in the repository: ```bash wt list +# Or use the alias: +wt ls ``` Example output: @@ -102,84 +104,88 @@ PATH BRANCH /Users/user/projects/myproject_feature-x feature-x def5678 ``` -### Delete Worktree +### Remove Worktree Remove a worktree: ```bash -# Delete a worktree -wt delete /path/to/worktree +# Remove a worktree +wt remove /path/to/worktree -# Force delete (even with uncommitted changes) -wt delete /path/to/worktree --force +# Or use the alias: +wt rm /path/to/worktree + +# Force remove (even with uncommitted changes) +wt remove /path/to/worktree --force ``` -## Modes +## Post-Creation Actions -The `create` command supports three modes via the `--mode` option: +The `add` command supports optional flags to perform actions after creating the worktree: -### `none` (default) -Creates the worktree without any additional action. +### Default (no flags) +Adds the worktree without any additional action. ```bash -wt create feature-x +wt add feature-x ``` -### `terminal` -Creates the worktree and opens a new terminal tab at that location. - -**Supported platforms:** -- macOS: Opens new iTerm2 tab +### `--ide` +Adds the worktree and opens it in an IDE. ```bash -ezl create feature-x --mode terminal +# Specify IDE explicitly +wt add feature-x --ide code # VS Code +wt add feature-x --ide cursor # Cursor +wt add feature-x --ide pycharm # PyCharm + +# Auto-detect IDE (tries: code, cursor, pycharm, subl, atom) +wt add feature-x --ide ``` -### `ide` -Creates the worktree and opens it in an IDE. +### `--claude` +Adds the worktree and starts a Claude Code session. ```bash -# Specify IDE explicitly -wt create feature-x --mode ide --ide code # VS Code -wt create feature-x --mode ide --ide cursor # Cursor -wt create feature-x --mode ide --ide pycharm # PyCharm - -# Auto-detect IDE (tries: code, cursor, pycharm, subl, atom) -wt create feature-x --mode ide +wt add feature-x --claude ``` +**Note**: `--ide` and `--claude` are mutually exclusive. + ## Examples ### Working on a new feature ```bash -# Create a new worktree for a feature branch and open in VS Code -wt create feature/auth-system --mode ide --ide code +# Add a new worktree for a feature branch and open in VS Code +wt add feature/auth-system --ide code # Work on the feature... cd ../myproject_feature/auth-system -# When done, delete the worktree -wt delete /path/to/myproject_feature/auth-system +# When done, remove the worktree +wt rm /path/to/myproject_feature/auth-system ``` ### Quick bug fix ```bash -# Create worktree for hotfix -wt create hotfix/urgent-bug +# Add worktree for hotfix +wt add hotfix/urgent-bug # Work on the fix in the new location cd ../myproject_hotfix/urgent-bug # After merging, clean up -wt delete ../myproject_hotfix/urgent-bug +wt remove ../myproject_hotfix/urgent-bug ``` ### Review all active worktrees ```bash wt list +# Or use the alias: +wt ls ``` ## Requirements @@ -266,7 +272,7 @@ Contributions are welcome! Please feel free to submit a Pull Request. ## License -[Add your license here] +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. ## Acknowledgments diff --git a/tests/test_cli.py b/tests/test_cli.py index ba3a22a..9c9541a 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -29,50 +29,48 @@ def test_version_option(self): assert "git-worktree-cli version" in result.stdout -class TestCLICreate: - """Tests for create command.""" +class TestCLIAdd: + """Tests for add command.""" - def test_create_worktree_default(self, mocker): - """Test creating worktree without any flags.""" + def test_add_worktree_default(self, mocker): + """Test adding worktree without any flags.""" mock_create = mocker.patch( "wt.cli.create_worktree", return_value=Path("/path/to/worktree") ) mock_print = mocker.patch("builtins.print") - result = runner.invoke(app, ["create", "feature-x"]) + result = runner.invoke(app, ["add", "feature-x"]) assert result.exit_code == 0 mock_create.assert_called_once_with("feature-x") mock_print.assert_called_once_with("Worktree created at: /path/to/worktree") - def test_create_worktree_with_ide(self, mocker): - """Test creating worktree with --ide flag.""" + def test_add_worktree_with_ide(self, mocker): + """Test adding worktree with --ide flag.""" mocker.patch("wt.cli.create_worktree", return_value=Path("/path/to/worktree")) mock_launch_ide = mocker.patch("wt.cli.launch_ide") - result = runner.invoke(app, ["create", "feature-x", "--ide", "code"]) + result = runner.invoke(app, ["add", "feature-x", "--ide", "code"]) assert result.exit_code == 0 mock_launch_ide.assert_called_once_with(Path("/path/to/worktree"), "code") - def test_create_worktree_with_claude(self, mocker): - """Test creating worktree with --claude flag.""" + def test_add_worktree_with_claude(self, mocker): + """Test adding worktree with --claude flag.""" mocker.patch("wt.cli.create_worktree", return_value=Path("/path/to/worktree")) mock_launch_claude = mocker.patch("wt.cli.launch_claude") - result = runner.invoke(app, ["create", "feature-x", "--claude"]) + result = runner.invoke(app, ["add", "feature-x", "--claude"]) assert result.exit_code == 0 mock_launch_claude.assert_called_once_with(Path("/path/to/worktree")) - def test_create_worktree_ide_and_claude_exclusive(self, mocker): + def test_add_worktree_ide_and_claude_exclusive(self, mocker): """Test that --ide and --claude are mutually exclusive.""" mock_echo = mocker.patch("typer.echo") mocker.patch("wt.cli.create_worktree", return_value=Path("/path/to/worktree")) - result = runner.invoke( - app, ["create", "feature-x", "--ide", "code", "--claude"] - ) + result = runner.invoke(app, ["add", "feature-x", "--ide", "code", "--claude"]) assert result.exit_code == 1 # Verify error message was echoed @@ -84,12 +82,12 @@ def test_create_worktree_ide_and_claude_exclusive(self, mocker): ] assert len(error_call) > 0 - def test_create_worktree_error(self, mocker): - """Test creating worktree when WorktreeError occurs.""" + def test_add_worktree_error(self, mocker): + """Test adding worktree when WorktreeError occurs.""" mock_echo = mocker.patch("typer.echo") mocker.patch("wt.cli.create_worktree", side_effect=WorktreeError("Test error")) - result = runner.invoke(app, ["create", "feature-x"]) + result = runner.invoke(app, ["add", "feature-x"]) assert result.exit_code == 1 # Verify error message was echoed @@ -101,13 +99,13 @@ def test_create_worktree_error(self, mocker): ] assert len(error_call) > 0 - def test_create_worktree_launcher_error(self, mocker): - """Test creating worktree when LauncherError occurs.""" + def test_add_worktree_launcher_error(self, mocker): + """Test adding worktree when LauncherError occurs.""" mock_echo = mocker.patch("typer.echo") mocker.patch("wt.cli.create_worktree", return_value=Path("/path/to/worktree")) mocker.patch("wt.cli.launch_ide", side_effect=LauncherError("Launcher error")) - result = runner.invoke(app, ["create", "feature-x", "--ide", "code"]) + result = runner.invoke(app, ["add", "feature-x", "--ide", "code"]) assert result.exit_code == 1 # Verify error message was echoed @@ -173,36 +171,36 @@ def test_list_worktrees_error(self, mocker): assert len(error_call) > 0 -class TestCLIDelete: - """Tests for delete command.""" +class TestCLIRemove: + """Tests for remove command.""" - def test_delete_worktree(self, mocker): - """Test deleting worktree.""" + def test_remove_worktree(self, mocker): + """Test removing worktree.""" mock_delete = mocker.patch("wt.cli.delete_worktree") - result = runner.invoke(app, ["delete", "/path/to/worktree"]) + result = runner.invoke(app, ["remove", "/path/to/worktree"]) assert result.exit_code == 0 mock_delete.assert_called_once_with("/path/to/worktree", False) - assert "Worktree deleted: /path/to/worktree" in result.stdout + assert "Worktree removed: /path/to/worktree" in result.stdout - def test_delete_worktree_force(self, mocker): - """Test deleting worktree with force flag.""" + def test_remove_worktree_force(self, mocker): + """Test removing worktree with force flag.""" mock_delete = mocker.patch("wt.cli.delete_worktree") - result = runner.invoke(app, ["delete", "/path/to/worktree", "--force"]) + result = runner.invoke(app, ["remove", "/path/to/worktree", "--force"]) assert result.exit_code == 0 mock_delete.assert_called_once_with("/path/to/worktree", True) - def test_delete_worktree_error(self, mocker): - """Test deleting worktree when error occurs.""" + def test_remove_worktree_error(self, mocker): + """Test removing worktree when error occurs.""" mock_echo = mocker.patch("typer.echo") mocker.patch( - "wt.cli.delete_worktree", side_effect=WorktreeError("Delete error") + "wt.cli.delete_worktree", side_effect=WorktreeError("Remove error") ) - result = runner.invoke(app, ["delete", "/path/to/worktree"]) + result = runner.invoke(app, ["remove", "/path/to/worktree"]) assert result.exit_code == 1 # Verify error message was echoed @@ -210,10 +208,20 @@ def test_delete_worktree_error(self, mocker): error_call = [ call for call in mock_echo.call_args_list - if call.args and "Error: Delete error" in call.args[0] + if call.args and "Error: Remove error" in call.args[0] ] assert len(error_call) > 0 + def test_rm_alias(self, mocker): + """Test rm alias for remove command.""" + mock_delete = mocker.patch("wt.cli.delete_worktree") + + result = runner.invoke(app, ["rm", "/path/to/worktree"]) + + assert result.exit_code == 0 + mock_delete.assert_called_once_with("/path/to/worktree", False) + assert "Worktree removed: /path/to/worktree" in result.stdout + class TestCLIHelp: """Tests for help command.""" @@ -224,17 +232,17 @@ def test_main_help(self): assert result.exit_code == 0 assert "git-worktree-cli" in result.stdout - assert "create" in result.stdout + assert "add" in result.stdout assert "list" in result.stdout - assert "delete" in result.stdout + assert "remove" in result.stdout - def test_create_help(self): - """Test create command help.""" - result = runner.invoke(app, ["create", "--help"]) + def test_add_help(self): + """Test add command help.""" + result = runner.invoke(app, ["add", "--help"]) output = strip_ansi(result.stdout) assert result.exit_code == 0 - assert "Create a new git worktree" in output + assert "Add a new git worktree" in output assert "--ide" in output assert "--claude" in output @@ -245,11 +253,17 @@ def test_list_help(self): assert result.exit_code == 0 assert "List all git worktrees" in result.stdout - def test_delete_help(self): - """Test delete command help.""" - result = runner.invoke(app, ["delete", "--help"]) + def test_remove_help(self): + """Test remove command help.""" + result = runner.invoke(app, ["remove", "--help"]) output = strip_ansi(result.stdout) assert result.exit_code == 0 - assert "Delete a git worktree" in output + assert "Remove a git worktree" in output assert "--force" in output + + def test_ls_alias(self): + """Test ls alias for list command.""" + result = runner.invoke(app, ["ls"]) + + assert result.exit_code == 0 diff --git a/wt/cli.py b/wt/cli.py index 2eb7ec8..fbe07f7 100644 --- a/wt/cli.py +++ b/wt/cli.py @@ -39,9 +39,9 @@ def main( """git-worktree-cli: A lightweight Python CLI tool to simplify Git worktree management.""" -@app.command() -def create( - branch: Annotated[str, typer.Argument(help="Branch name to create worktree for")], +@app.command(name="add") +def add( + branch: Annotated[str, typer.Argument(help="Branch name to add worktree for")], ide: Annotated[ Optional[str], typer.Option( @@ -55,27 +55,27 @@ def create( ), ] = False, ): - """Create a new git worktree for BRANCH. + """Add a new git worktree for BRANCH. The worktree will be created at: ../_ Examples: \b - # Create worktree only - wt create feature-x + # Add worktree only + wt add feature-x \b - # Create and open in VS Code - wt create feature-x --ide code + # Add and open in VS Code + wt add feature-x --ide code \b - # Create and start Claude session - wt create feature-x --claude + # Add and start Claude session + wt add feature-x --claude \b - # Create and open in default IDE - wt create feature-x --ide + # Add and open in default IDE + wt add feature-x --ide """ # Check for mutually exclusive options if ide and claude: @@ -124,35 +124,57 @@ def list_cmd(): raise typer.Exit(code=1) -@app.command() -def delete( - path: Annotated[str, typer.Argument(help="Path to the worktree to delete")], +# Alias: ls -> list +@app.command(name="ls", hidden=True) +def ls(): + """Alias for list command.""" + list_cmd() + + +@app.command(name="remove") +def remove( + path: Annotated[str, typer.Argument(help="Path to the worktree to remove")], force: Annotated[ bool, typer.Option( - "--force", "-f", help="Force deletion even with uncommitted changes" + "--force", "-f", help="Force removal even with uncommitted changes" ), ] = False, ): - """Delete a git worktree at PATH. + """Remove a git worktree at PATH. Examples: \b - # Delete a worktree - wt delete ../myproject_feature-x + # Remove a worktree + wt remove ../myproject_feature-x \b - # Force delete worktree with uncommitted changes - wt delete ../myproject_feature-x --force + # Force remove worktree with uncommitted changes + wt remove ../myproject_feature-x --force """ try: delete_worktree(path, force) - typer.echo(f"Worktree deleted: {path}") + typer.echo(f"Worktree removed: {path}") except WorktreeError as e: typer.echo(f"Error: {e}", err=True) raise typer.Exit(code=1) +# Alias: rm -> remove +@app.command(name="rm", hidden=True) +def rm( + path: Annotated[str, typer.Argument(help="Path to the worktree to remove")], + force: Annotated[ + bool, + typer.Option( + "--force", "-f", help="Force removal even with uncommitted changes" + ), + ] = False, +): + """Alias for remove command.""" + remove(path, force) + + if __name__ == "__main__": app()