From a89f420796b6dcac5d7898974afe942ba36b9922 Mon Sep 17 00:00:00 2001 From: Sho Nakatani Date: Fri, 27 Jun 2025 21:53:32 +0900 Subject: [PATCH 1/3] feat: Add index-based functionality for worktree commands --- CHANGELOG.md | 1 + pyproject.toml | 8 +- src/sprout/cli.py | 12 +-- src/sprout/commands/ls.py | 65 ++---------- src/sprout/commands/path.py | 25 ++++- src/sprout/commands/rm.py | 19 +++- src/sprout/utils.py | 90 ++++++++++++++++- tests/test_commands.py | 195 +++++++++++++++++++++++++++++++++++- tests/test_utils.py | 84 ++++++++++++++++ 9 files changed, 425 insertions(+), 74 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67f6f9f..a2a9e8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added ### Changed +- `sprout path` command now accepts index numbers in addition to branch names ### Deprecated diff --git a/pyproject.toml b/pyproject.toml index cefce97..87d1400 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -122,4 +122,10 @@ ignore_missing_imports = true [[tool.mypy.overrides]] module = "tests.*" disallow_untyped_defs = false -disallow_incomplete_defs = false \ No newline at end of file +disallow_incomplete_defs = false + +[dependency-groups] +dev = [ + "pytest-mock>=3.14.1", + "ruff>=0.12.1", +] diff --git a/src/sprout/cli.py b/src/sprout/cli.py index 29b66ef..bfdc3a5 100644 --- a/src/sprout/cli.py +++ b/src/sprout/cli.py @@ -59,24 +59,24 @@ def ls() -> None: @app.command() def rm( - branch_name: BranchName = typer.Argument( + identifier: str = typer.Argument( ..., - help="Name of the branch to remove", + help="Branch name or index number to remove", ), ) -> None: """Remove a development environment.""" - remove_worktree(branch_name) + remove_worktree(identifier) @app.command() def path( - branch_name: BranchName = typer.Argument( + identifier: str = typer.Argument( ..., - help="Name of the branch to get path for", + help="Branch name or index number to get path for", ), ) -> None: """Get the path of a development environment.""" - get_worktree_path(branch_name) + get_worktree_path(identifier) if __name__ == "__main__": diff --git a/src/sprout/commands/ls.py b/src/sprout/commands/ls.py index 8ba69f3..7bb9e9e 100644 --- a/src/sprout/commands/ls.py +++ b/src/sprout/commands/ls.py @@ -1,15 +1,10 @@ """Implementation of the ls command.""" -from datetime import datetime -from pathlib import Path - import typer from rich.console import Console from rich.table import Table -from sprout.exceptions import SproutError -from sprout.types import WorktreeInfo -from sprout.utils import get_sprout_dir, is_git_repository, run_command +from sprout.utils import get_indexed_worktrees, is_git_repository console = Console() @@ -20,74 +15,34 @@ def list_worktrees() -> None: console.print("[red]Error: Not in a git repository[/red]") raise typer.Exit(1) - sprout_dir = get_sprout_dir() - - # Get worktree list from git try: - result = run_command(["git", "worktree", "list", "--porcelain"]) - except SproutError as e: + sprout_worktrees = get_indexed_worktrees() + except Exception as e: console.print(f"[red]Error listing worktrees: {e}[/red]") raise typer.Exit(1) from e - # Parse worktree output - worktrees: list[WorktreeInfo] = [] - current_worktree: WorktreeInfo = {} - - for line in result.stdout.strip().split("\n"): - if not line: - if current_worktree: - worktrees.append(current_worktree) - current_worktree = {} - continue - - if line.startswith("worktree "): - current_worktree["path"] = Path(line[9:]) - elif line.startswith("branch "): - current_worktree["branch"] = line[7:] - elif line.startswith("HEAD "): - current_worktree["head"] = line[5:] - - if current_worktree: - worktrees.append(current_worktree) - - # Filter for sprout-managed worktrees - sprout_worktrees: list[WorktreeInfo] = [] - current_path = Path.cwd().resolve() - - for wt in worktrees: - wt_path = wt["path"].resolve() - if wt_path.parent == sprout_dir: - # Check if we're currently in this worktree - wt["is_current"] = current_path == wt_path or current_path.is_relative_to(wt_path) - - # Get last modified time - if wt_path.exists(): - stat = wt_path.stat() - wt["modified"] = datetime.fromtimestamp(stat.st_mtime) - else: - wt["modified"] = None - - sprout_worktrees.append(wt) - if not sprout_worktrees: console.print("[yellow]No sprout-managed worktrees found.[/yellow]") console.print("Use 'sprout create ' to create one.") return None - # Create table + # Create table with index column table = Table(title="Sprout Worktrees", show_lines=True) + table.add_column("No.", style="bright_white", no_wrap=True, width=4) table.add_column("Branch", style="cyan", no_wrap=True) table.add_column("Path", style="blue") table.add_column("Status", style="green") table.add_column("Last Modified", style="yellow") - for wt in sprout_worktrees: + from pathlib import Path + + for idx, wt in enumerate(sprout_worktrees, 1): branch = wt.get("branch", wt.get("head", "detached")) path = str(wt["path"].relative_to(Path.cwd())) - status = "[green]โ— current[/green]" if wt["is_current"] else "" + status = "[green]โ— current[/green]" if wt.get("is_current", False) else "" modified_dt = wt.get("modified") modified = modified_dt.strftime("%Y-%m-%d %H:%M") if modified_dt else "N/A" - table.add_row(branch, path, status, modified) + table.add_row(str(idx), branch, path, status, modified) console.print(table) diff --git a/src/sprout/commands/path.py b/src/sprout/commands/path.py index db2d21e..c829f6e 100644 --- a/src/sprout/commands/path.py +++ b/src/sprout/commands/path.py @@ -4,18 +4,35 @@ import typer -from sprout.types import BranchName -from sprout.utils import get_sprout_dir, is_git_repository, worktree_exists +from sprout.utils import ( + get_sprout_dir, + is_git_repository, + resolve_branch_identifier, + worktree_exists, +) console: TextIO = typer.get_text_stream("stdout") -def get_worktree_path(branch_name: BranchName) -> Never: - """Get the path of a development environment.""" +def get_worktree_path(identifier: str) -> Never: + """Get the path of a development environment by branch name or index.""" if not is_git_repository(): typer.echo("Error: Not in a git repository", err=True) raise typer.Exit(1) + # Resolve identifier to branch name + branch_name = resolve_branch_identifier(identifier) + + if branch_name is None: + if identifier.isdigit(): + typer.echo( + f"Error: Invalid index '{identifier}'. Use 'sprout ls' to see valid indices.", + err=True, + ) + else: + typer.echo(f"Error: Worktree for branch '{identifier}' does not exist", err=True) + raise typer.Exit(1) + # Check if worktree exists if not worktree_exists(branch_name): typer.echo(f"Error: Worktree for branch '{branch_name}' does not exist", err=True) diff --git a/src/sprout/commands/rm.py b/src/sprout/commands/rm.py index d45e720..893915f 100644 --- a/src/sprout/commands/rm.py +++ b/src/sprout/commands/rm.py @@ -4,10 +4,10 @@ from rich.console import Console from sprout.exceptions import SproutError -from sprout.types import BranchName from sprout.utils import ( get_sprout_dir, is_git_repository, + resolve_branch_identifier, run_command, worktree_exists, ) @@ -15,12 +15,25 @@ console = Console() -def remove_worktree(branch_name: BranchName) -> None: - """Remove a development environment.""" +def remove_worktree(identifier: str) -> None: + """Remove a development environment by branch name or index.""" if not is_git_repository(): console.print("[red]Error: Not in a git repository[/red]") raise typer.Exit(1) + # Resolve identifier to branch name + branch_name = resolve_branch_identifier(identifier) + + if branch_name is None: + if identifier.isdigit(): + console.print( + f"[red]Error: Invalid index '{identifier}'. " + "Use 'sprout ls' to see valid indices.[/red]" + ) + else: + console.print(f"[red]Error: Worktree for branch '{identifier}' does not exist[/red]") + raise typer.Exit(1) + # Check if worktree exists if not worktree_exists(branch_name): console.print(f"[red]Error: Worktree for branch '{branch_name}' does not exist[/red]") diff --git a/src/sprout/utils.py b/src/sprout/utils.py index c96148a..198c4b9 100644 --- a/src/sprout/utils.py +++ b/src/sprout/utils.py @@ -5,13 +5,14 @@ import re import socket import subprocess +from datetime import datetime from pathlib import Path from typing import TypeAlias from rich.console import Console from sprout.exceptions import SproutError -from sprout.types import BranchName +from sprout.types import BranchName, WorktreeInfo # Type aliases PortNumber: TypeAlias = int @@ -175,3 +176,90 @@ def branch_exists(branch_name: BranchName) -> bool: """Check if a git branch exists.""" result = run_command(["git", "rev-parse", "--verify", f"refs/heads/{branch_name}"], check=False) return result.returncode == 0 + + +def get_indexed_worktrees() -> list[WorktreeInfo]: + """Get a list of sprout-managed worktrees with consistent ordering. + + Returns: + List of WorktreeInfo dicts, sorted by branch name for consistent indexing. + """ + if not is_git_repository(): + raise SproutError("Not in a git repository") + + sprout_dir = get_sprout_dir() + + # Get worktree list from git + result = run_command(["git", "worktree", "list", "--porcelain"]) + + # Parse worktree output + worktrees: list[WorktreeInfo] = [] + current_worktree: WorktreeInfo = {} + + for line in result.stdout.strip().split("\n"): + if not line: + if current_worktree: + worktrees.append(current_worktree) + current_worktree = {} + continue + + if line.startswith("worktree "): + current_worktree["path"] = Path(line[9:]) + elif line.startswith("branch "): + branch_ref = line[7:] + # Strip refs/heads/ prefix if present + if branch_ref.startswith("refs/heads/"): + current_worktree["branch"] = branch_ref[11:] + else: + current_worktree["branch"] = branch_ref + elif line.startswith("HEAD "): + current_worktree["head"] = line[5:] + + if current_worktree: + worktrees.append(current_worktree) + + # Filter for sprout-managed worktrees + sprout_worktrees: list[WorktreeInfo] = [] + current_path = Path.cwd().resolve() + + for wt in worktrees: + wt_path = wt["path"].resolve() + if wt_path.parent == sprout_dir: + # Check if we're currently in this worktree + wt["is_current"] = current_path == wt_path or current_path.is_relative_to(wt_path) + + # Get last modified time + if wt_path.exists(): + stat = wt_path.stat() + wt["modified"] = datetime.fromtimestamp(stat.st_mtime) + else: + wt["modified"] = None + + sprout_worktrees.append(wt) + + # Sort by branch name for consistent indexing + sprout_worktrees.sort(key=lambda wt: wt.get("branch", wt.get("head", ""))) + + return sprout_worktrees + + +def resolve_branch_identifier(identifier: str) -> BranchName | None: + """Resolve a branch identifier (name or index) to a branch name. + + Args: + identifier: Either a branch name or a 1-based index number + + Returns: + Branch name if found, None otherwise + """ + # Check if identifier is a number + if identifier.isdigit(): + index = int(identifier) + worktrees = get_indexed_worktrees() + + if 1 <= index <= len(worktrees): + return worktrees[index - 1].get("branch", worktrees[index - 1].get("head", "")) + return None + + # Otherwise treat as branch name + return identifier diff --git a/tests/test_commands.py b/tests/test_commands.py index 817bf78..1482996 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -109,7 +109,8 @@ def test_ls_with_worktrees(self, mocker, tmp_path): # Mock prerequisites mocker.patch("sprout.commands.ls.is_git_repository", return_value=True) - mocker.patch("sprout.commands.ls.get_sprout_dir", return_value=sprout_dir) + mocker.patch("sprout.utils.is_git_repository", return_value=True) + mocker.patch("sprout.utils.get_sprout_dir", return_value=sprout_dir) mocker.patch("pathlib.Path.cwd", return_value=project_dir) # Mock git worktree list output @@ -122,7 +123,7 @@ def test_ls_with_worktrees(self, mocker, tmp_path): HEAD def456 branch refs/heads/feature2 """ - mocker.patch("sprout.commands.ls.run_command", return_value=mock_result) + mocker.patch("sprout.utils.run_command", return_value=mock_result) result = runner.invoke(app, ["ls"]) @@ -134,11 +135,12 @@ def test_ls_with_worktrees(self, mocker, tmp_path): def test_ls_no_worktrees(self, mocker): """Test listing when no worktrees exist.""" mocker.patch("sprout.commands.ls.is_git_repository", return_value=True) - mocker.patch("sprout.commands.ls.get_sprout_dir", return_value=Path("/project/.sprout")) + mocker.patch("sprout.utils.is_git_repository", return_value=True) + mocker.patch("sprout.utils.get_sprout_dir", return_value=Path("/project/.sprout")) mock_result = Mock() mock_result.stdout = "" - mocker.patch("sprout.commands.ls.run_command", return_value=mock_result) + mocker.patch("sprout.utils.run_command", return_value=mock_result) result = runner.invoke(app, ["ls"]) @@ -165,6 +167,9 @@ class TestPathCommand: def test_path_success(self, mocker): """Test getting worktree path.""" mocker.patch("sprout.commands.path.is_git_repository", return_value=True) + mocker.patch( + "sprout.commands.path.resolve_branch_identifier", return_value="feature-branch" + ) mocker.patch("sprout.commands.path.worktree_exists", return_value=True) mocker.patch("sprout.commands.path.get_sprout_dir", return_value=Path("/project/.sprout")) @@ -176,6 +181,9 @@ def test_path_success(self, mocker): def test_path_worktree_not_exists(self, mocker): """Test error when worktree doesn't exist.""" mocker.patch("sprout.commands.path.is_git_repository", return_value=True) + mocker.patch( + "sprout.commands.path.resolve_branch_identifier", return_value="feature-branch" + ) mocker.patch("sprout.commands.path.worktree_exists", return_value=False) result = runner.invoke(app, ["path", "feature-branch"]) @@ -184,6 +192,64 @@ def test_path_worktree_not_exists(self, mocker): # Error goes to stderr, not stdout in path command assert "Error: Worktree for branch 'feature-branch' does not exist" in result.output + def test_path_with_index(self, mocker, tmp_path): + """Test getting worktree path using index.""" + # Set up test directory structure + project_dir = tmp_path / "project" + project_dir.mkdir() + sprout_dir = project_dir / ".sprout" + sprout_dir.mkdir() + + # Create worktree directories + feature_a_dir = sprout_dir / "feature-a" + feature_a_dir.mkdir() + feature_b_dir = sprout_dir / "feature-b" + feature_b_dir.mkdir() + + # Mock prerequisites + mocker.patch("sprout.commands.path.is_git_repository", return_value=True) + mocker.patch("sprout.utils.is_git_repository", return_value=True) + mocker.patch("sprout.utils.get_sprout_dir", return_value=sprout_dir) + mocker.patch("sprout.commands.path.get_sprout_dir", return_value=sprout_dir) + mocker.patch("sprout.commands.path.worktree_exists", return_value=True) + mocker.patch("pathlib.Path.cwd", return_value=project_dir) + + # Mock git worktree list output for index resolution + mock_result = Mock() + mock_result.stdout = f"""worktree {feature_a_dir} +HEAD abc123 +branch refs/heads/feature-a + +worktree {feature_b_dir} +HEAD def456 +branch refs/heads/feature-b +""" + mocker.patch("sprout.utils.run_command", return_value=mock_result) + + # Test with index "2" which should resolve to "feature-b" + result = runner.invoke(app, ["path", "2"]) + + assert result.exit_code == 0 + assert result.stdout.strip() == str(sprout_dir / "feature-b") + + def test_path_with_invalid_index(self, mocker): + """Test error when using invalid index.""" + # Mock prerequisites + mocker.patch("sprout.commands.path.is_git_repository", return_value=True) + mocker.patch("sprout.utils.is_git_repository", return_value=True) + mocker.patch("sprout.utils.get_sprout_dir", return_value=Path("/project/.sprout")) + + # Mock empty worktree list + mock_result = Mock() + mock_result.stdout = "" + mocker.patch("sprout.utils.run_command", return_value=mock_result) + + result = runner.invoke(app, ["path", "99"]) + + assert result.exit_code == 1 + assert "Invalid index '99'" in result.output + assert "sprout ls" in result.output + class TestVersion: """Test version display.""" @@ -194,3 +260,124 @@ def test_version_flag(self): assert result.exit_code == 0 assert "sprout version" in result.stdout + + +class TestIndexedOperations: + """Test index-based functionality.""" + + def test_ls_with_indices(self, mocker, tmp_path): + """Test that ls command shows index numbers.""" + # Set up test directory structure + project_dir = tmp_path / "project" + project_dir.mkdir() + sprout_dir = project_dir / ".sprout" + sprout_dir.mkdir() + + # Create worktree directories + feature_a_dir = sprout_dir / "feature-a" + feature_a_dir.mkdir() + feature_b_dir = sprout_dir / "feature-b" + feature_b_dir.mkdir() + feature_c_dir = sprout_dir / "feature-c" + feature_c_dir.mkdir() + + # Mock prerequisites + mocker.patch("sprout.utils.is_git_repository", return_value=True) + mocker.patch("sprout.utils.get_sprout_dir", return_value=sprout_dir) + mocker.patch("pathlib.Path.cwd", return_value=project_dir) + + # Mock git worktree list output + mock_result = Mock() + mock_result.stdout = f"""worktree {feature_a_dir} +HEAD abc123 +branch refs/heads/feature-a + +worktree {feature_b_dir} +HEAD def456 +branch refs/heads/feature-b + +worktree {feature_c_dir} +HEAD ghi789 +branch refs/heads/feature-c +""" + mocker.patch("sprout.utils.run_command", return_value=mock_result) + + result = runner.invoke(app, ["ls"]) + + assert result.exit_code == 0 + assert "No." in result.stdout # Check for index column header + assert "1" in result.stdout + assert "2" in result.stdout + assert "3" in result.stdout + # Check that branches are sorted alphabetically + lines = result.stdout.split("\n") + for line in lines: + if "1" in line and "feature-a" in line: + assert True + break + else: + raise AssertionError("Index 1 should correspond to feature-a") + + def test_rm_with_index(self, mocker, tmp_path): + """Test removing worktree by index.""" + # Set up test directory structure + project_dir = tmp_path / "project" + project_dir.mkdir() + sprout_dir = project_dir / ".sprout" + sprout_dir.mkdir() + + # Create worktree directories + feature_a_dir = sprout_dir / "feature-a" + feature_a_dir.mkdir() + feature_b_dir = sprout_dir / "feature-b" + feature_b_dir.mkdir() + + # Mock prerequisites + mocker.patch("sprout.commands.rm.is_git_repository", return_value=True) + mocker.patch("sprout.utils.is_git_repository", return_value=True) + mocker.patch("sprout.utils.get_sprout_dir", return_value=sprout_dir) + mocker.patch("sprout.commands.rm.get_sprout_dir", return_value=sprout_dir) + mocker.patch("sprout.utils.worktree_exists", return_value=True) + mocker.patch("sprout.commands.rm.worktree_exists", return_value=True) + mocker.patch("pathlib.Path.cwd", return_value=project_dir) + + # Mock git worktree list output for index resolution + mock_result = Mock() + mock_result.stdout = f"""worktree {feature_a_dir} +HEAD abc123 +branch refs/heads/feature-a + +worktree {feature_b_dir} +HEAD def456 +branch refs/heads/feature-b +""" + mocker.patch("sprout.utils.run_command", return_value=mock_result) + + # Mock the actual removal command + mock_rm_result = Mock() + mock_rm_result.returncode = 0 + mocker.patch("sprout.commands.rm.run_command", return_value=mock_rm_result) + + # Test with index "2" which should resolve to "feature-b" + result = runner.invoke(app, ["rm", "2"], input="n\n") # Say no to confirmation + + assert result.exit_code == 0 + assert "feature-b" in result.stdout # Should show the resolved branch name + + def test_rm_with_invalid_index(self, mocker): + """Test error when using invalid index.""" + # Mock prerequisites + mocker.patch("sprout.commands.rm.is_git_repository", return_value=True) + mocker.patch("sprout.utils.is_git_repository", return_value=True) + mocker.patch("sprout.utils.get_sprout_dir", return_value=Path("/project/.sprout")) + + # Mock empty worktree list + mock_result = Mock() + mock_result.stdout = "" + mocker.patch("sprout.utils.run_command", return_value=mock_result) + + result = runner.invoke(app, ["rm", "1"]) + + assert result.exit_code == 1 + assert "Invalid index" in result.stdout + assert "sprout ls" in result.stdout # Should suggest using ls command diff --git a/tests/test_utils.py b/tests/test_utils.py index 885936a..9e393e5 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -228,3 +228,87 @@ def test_run_command_failure(self, mocker): with pytest.raises(SproutError, match="Command failed"): run_command(["git", "status"]) + + +class TestIndexedWorktrees: + """Test indexed worktree functionality.""" + + def test_get_indexed_worktrees(self, mocker, tmp_path): + """Test get_indexed_worktrees returns sorted list.""" + sprout_dir = tmp_path / ".sprout" + sprout_dir.mkdir() + + # Create test directories + feature_b_dir = sprout_dir / "feature-b" + feature_b_dir.mkdir() + feature_a_dir = sprout_dir / "feature-a" + feature_a_dir.mkdir() + + mocker.patch("sprout.utils.is_git_repository", return_value=True) + mocker.patch("sprout.utils.get_sprout_dir", return_value=sprout_dir) + mocker.patch("pathlib.Path.cwd", return_value=tmp_path) + + # Mock git worktree list output (unsorted) + mock_result = Mock() + mock_result.stdout = f"""worktree {feature_b_dir} +HEAD def456 +branch refs/heads/feature-b + +worktree {feature_a_dir} +HEAD abc123 +branch refs/heads/feature-a +""" + mocker.patch("sprout.utils.run_command", return_value=mock_result) + + from sprout.utils import get_indexed_worktrees + + worktrees = get_indexed_worktrees() + + assert len(worktrees) == 2 + # Check that they're sorted by branch name + assert worktrees[0]["branch"] == "feature-a" + assert worktrees[1]["branch"] == "feature-b" + + def test_resolve_branch_identifier_with_name(self): + """Test resolve_branch_identifier with branch name.""" + from sprout.utils import resolve_branch_identifier + + result = resolve_branch_identifier("feature-branch") + assert result == "feature-branch" + + def test_resolve_branch_identifier_with_valid_index(self, mocker, tmp_path): + """Test resolve_branch_identifier with valid index.""" + sprout_dir = tmp_path / ".sprout" + sprout_dir.mkdir() + feature_dir = sprout_dir / "feature-a" + feature_dir.mkdir() + + mocker.patch("sprout.utils.is_git_repository", return_value=True) + mocker.patch("sprout.utils.get_sprout_dir", return_value=sprout_dir) + mocker.patch("pathlib.Path.cwd", return_value=tmp_path) + + mock_result = Mock() + mock_result.stdout = f"""worktree {feature_dir} +HEAD abc123 +branch refs/heads/feature-a +""" + mocker.patch("sprout.utils.run_command", return_value=mock_result) + + from sprout.utils import resolve_branch_identifier + + result = resolve_branch_identifier("1") + assert result == "feature-a" + + def test_resolve_branch_identifier_with_invalid_index(self, mocker): + """Test resolve_branch_identifier with invalid index.""" + mocker.patch("sprout.utils.is_git_repository", return_value=True) + mocker.patch("sprout.utils.get_sprout_dir", return_value=Path("/project/.sprout")) + + mock_result = Mock() + mock_result.stdout = "" # No worktrees + mocker.patch("sprout.utils.run_command", return_value=mock_result) + + from sprout.utils import resolve_branch_identifier + + result = resolve_branch_identifier("99") + assert result is None From 0dfcb16d521f686e5f8e9f442e9e6cc549da32fd Mon Sep 17 00:00:00 2001 From: Sho Nakatani Date: Fri, 27 Jun 2025 21:59:35 +0900 Subject: [PATCH 2/3] docs: Update README and CHANGELOG with index functionality details MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Document index numbers in sprout ls output - Show examples of using indices with rm and path commands - Improve CHANGELOG entry to explain the user benefit clearly ๐Ÿค– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- CHANGELOG.md | 2 +- README.md | 39 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3e527f..0e60168 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `--path` flag for `sprout create` command to output only the worktree path, enabling one-liner usage like `cd $(sprout create feature --path)` ### Changed -- `sprout path` command now accepts index numbers in addition to branch names +- Enhanced `sprout path` and `sprout rm` commands to accept index numbers from `sprout ls` output, enabling faster navigation without typing full branch names (e.g., `cd $(sprout path 2)` instead of `cd $(sprout path feature-long-branch-name)`) ### Deprecated diff --git a/README.md b/README.md index 11c3d02..a88cb75 100644 --- a/README.md +++ b/README.md @@ -90,12 +90,47 @@ cd $(sprout create feature-xyz --path) ### `sprout ls` List all managed development environments with their status. -### `sprout rm ` +The output includes index numbers that can be used with other commands: +```bash +sprout ls +# Output: +# โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”“ +# โ”ƒ No. โ”ƒ Branch โ”ƒ Path โ”ƒ Status โ”ƒ Last Modified โ”ƒ +# โ”กโ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ฉ +# โ”‚ 1 โ”‚ feature-auth โ”‚ .sprout/feat... โ”‚ โ”‚ 2025-06-27 14:30 โ”‚ +# โ”‚ 2 โ”‚ bugfix-api โ”‚ .sprout/bugf... โ”‚ โ”‚ 2025-06-27 15:45 โ”‚ +# โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### `sprout rm ` Remove a development environment (with confirmation prompts). -### `sprout path ` +You can use either the branch name or the index number from `sprout ls`: +```bash +# Remove by branch name +sprout rm feature-auth + +# Remove by index number +sprout rm 1 +``` + +### `sprout path ` Get the filesystem path of a development environment. +You can use either the branch name or the index number from `sprout ls`: +```bash +# Get path by branch name +sprout path feature-auth +# Output: /path/to/project/.sprout/feature-auth + +# Get path by index number +sprout path 1 +# Output: /path/to/project/.sprout/feature-auth + +# Use with cd command +cd $(sprout path 2) +``` + ### `sprout --version` Show the version of sprout. From ac0b2ee3f2c66dd7b250cd7905c6b5d83dd34512 Mon Sep 17 00:00:00 2001 From: Sho Nakatani Date: Fri, 27 Jun 2025 22:04:21 +0900 Subject: [PATCH 3/3] fix: Fix mypy type error in worktree sorting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changed sort key to use 'or' operator instead of nested get() calls to ensure the lambda always returns a string, never None. ๐Ÿค– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/sprout/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sprout/utils.py b/src/sprout/utils.py index 34dcd38..885b317 100644 --- a/src/sprout/utils.py +++ b/src/sprout/utils.py @@ -244,7 +244,7 @@ def get_indexed_worktrees() -> list[WorktreeInfo]: sprout_worktrees.append(wt) # Sort by branch name for consistent indexing - sprout_worktrees.sort(key=lambda wt: wt.get("branch", wt.get("head", ""))) + sprout_worktrees.sort(key=lambda wt: wt.get("branch") or wt.get("head") or "") return sprout_worktrees