sprout-cli 0.4.0__tar.gz → 0.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sprout_cli-0.4.0 → sprout_cli-0.6.0}/PKG-INFO +21 -6
- {sprout_cli-0.4.0 → sprout_cli-0.6.0}/README.md +20 -5
- {sprout_cli-0.4.0 → sprout_cli-0.6.0}/src/sprout/__init__.py +1 -1
- {sprout_cli-0.4.0 → sprout_cli-0.6.0}/src/sprout/commands/create.py +68 -57
- {sprout_cli-0.4.0 → sprout_cli-0.6.0}/src/sprout/utils.py +11 -2
- {sprout_cli-0.4.0 → sprout_cli-0.6.0}/tests/test_commands.py +46 -7
- {sprout_cli-0.4.0 → sprout_cli-0.6.0}/tests/test_integration.py +45 -3
- {sprout_cli-0.4.0 → sprout_cli-0.6.0}/tests/test_utils.py +41 -0
- {sprout_cli-0.4.0 → sprout_cli-0.6.0}/.gitignore +0 -0
- {sprout_cli-0.4.0 → sprout_cli-0.6.0}/LICENSE +0 -0
- {sprout_cli-0.4.0 → sprout_cli-0.6.0}/pyproject.toml +0 -0
- {sprout_cli-0.4.0 → sprout_cli-0.6.0}/src/sprout/__main__.py +0 -0
- {sprout_cli-0.4.0 → sprout_cli-0.6.0}/src/sprout/cli.py +0 -0
- {sprout_cli-0.4.0 → sprout_cli-0.6.0}/src/sprout/commands/__init__.py +0 -0
- {sprout_cli-0.4.0 → sprout_cli-0.6.0}/src/sprout/commands/ls.py +0 -0
- {sprout_cli-0.4.0 → sprout_cli-0.6.0}/src/sprout/commands/path.py +0 -0
- {sprout_cli-0.4.0 → sprout_cli-0.6.0}/src/sprout/commands/rm.py +0 -0
- {sprout_cli-0.4.0 → sprout_cli-0.6.0}/src/sprout/exceptions.py +0 -0
- {sprout_cli-0.4.0 → sprout_cli-0.6.0}/src/sprout/types.py +0 -0
- {sprout_cli-0.4.0 → sprout_cli-0.6.0}/tests/__init__.py +0 -0
- {sprout_cli-0.4.0 → sprout_cli-0.6.0}/tests/test_multi_env.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sprout-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: CLI tool to automate git worktree and Docker Compose development workflows
|
|
5
5
|
Author: SecDevLab Inc.
|
|
6
6
|
License: MIT
|
|
@@ -41,7 +41,7 @@ A CLI tool to automate git worktree and Docker Compose development workflows.
|
|
|
41
41
|
## Installation
|
|
42
42
|
|
|
43
43
|
```bash
|
|
44
|
-
pip install sprout
|
|
44
|
+
pip install sprout-cli
|
|
45
45
|
```
|
|
46
46
|
|
|
47
47
|
For development:
|
|
@@ -56,7 +56,9 @@ pip install -e ".[dev]"
|
|
|
56
56
|
|
|
57
57
|
## Quick Start
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
**Note**: Sprout works in any git repository. `.env.example` files are optional - if you don't have them, sprout will simply create worktrees without `.env` generation.
|
|
60
|
+
|
|
61
|
+
1. (Optional) Create a `.env.example` template in your project root (and optionally in subdirectories) for automatic `.env` generation:
|
|
60
62
|
```env
|
|
61
63
|
# API Configuration
|
|
62
64
|
API_KEY={{ API_KEY }}
|
|
@@ -66,6 +68,10 @@ API_PORT={{ auto_port() }}
|
|
|
66
68
|
DB_HOST=localhost
|
|
67
69
|
DB_PORT={{ auto_port() }}
|
|
68
70
|
|
|
71
|
+
# Branch-specific Configuration
|
|
72
|
+
SERVICE_NAME=myapp-{{ branch() }}
|
|
73
|
+
DEPLOYMENT_ENV={{ branch() }}
|
|
74
|
+
|
|
69
75
|
# Example: Docker Compose variables (preserved as-is)
|
|
70
76
|
# sprout will NOT process ${...} syntax - it's passed through unchanged
|
|
71
77
|
# DB_NAME=${DB_NAME}
|
|
@@ -86,9 +92,13 @@ repo/
|
|
|
86
92
|
cd $(sprout create feature-branch --path)
|
|
87
93
|
```
|
|
88
94
|
|
|
95
|
+
**What happens when you run `sprout create`:**
|
|
96
|
+
- If `.env.example` files exist: Sprout will generate corresponding `.env` files with populated variables and unique port assignments
|
|
97
|
+
- If no `.env.example` files exist: Sprout will show a warning and create the worktree without `.env` generation
|
|
98
|
+
|
|
89
99
|
This single command:
|
|
90
100
|
- Creates a new git worktree for `feature-branch`
|
|
91
|
-
- Generates
|
|
101
|
+
- Generates `.env` files from your templates (if `.env.example` files exist)
|
|
92
102
|
- Outputs the path to the new environment
|
|
93
103
|
- Changes to that directory when wrapped in `cd $(...)`
|
|
94
104
|
|
|
@@ -174,7 +184,7 @@ Show the version of sprout.
|
|
|
174
184
|
|
|
175
185
|
## Template Syntax
|
|
176
186
|
|
|
177
|
-
sprout supports
|
|
187
|
+
sprout supports three types of placeholders in `.env.example`:
|
|
178
188
|
|
|
179
189
|
1. **Variable Placeholders**: `{{ VARIABLE_NAME }}`
|
|
180
190
|
- **First**: Checks if the variable exists in your environment (e.g., `export API_KEY=xxx`)
|
|
@@ -187,7 +197,12 @@ sprout supports two types of placeholders in `.env.example`:
|
|
|
187
197
|
- Checks system port availability
|
|
188
198
|
- Ensures global uniqueness even in monorepo setups
|
|
189
199
|
|
|
190
|
-
3. **
|
|
200
|
+
3. **Branch Name**: `{{ branch() }}`
|
|
201
|
+
- Replaced with the current branch/subtree name
|
|
202
|
+
- Useful for branch-specific configurations
|
|
203
|
+
- Example: `SERVICE_NAME=myapp-{{ branch() }}` becomes `SERVICE_NAME=myapp-feature-auth`
|
|
204
|
+
|
|
205
|
+
4. **Docker Compose Syntax (Preserved)**: `${VARIABLE}`
|
|
191
206
|
- NOT processed by sprout - passed through as-is
|
|
192
207
|
- Useful for Docker Compose variable substitution
|
|
193
208
|
- Example: `${DB_NAME:-default}` remains unchanged in generated `.env`
|
|
@@ -13,7 +13,7 @@ A CLI tool to automate git worktree and Docker Compose development workflows.
|
|
|
13
13
|
## Installation
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
pip install sprout
|
|
16
|
+
pip install sprout-cli
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
For development:
|
|
@@ -28,7 +28,9 @@ pip install -e ".[dev]"
|
|
|
28
28
|
|
|
29
29
|
## Quick Start
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
**Note**: Sprout works in any git repository. `.env.example` files are optional - if you don't have them, sprout will simply create worktrees without `.env` generation.
|
|
32
|
+
|
|
33
|
+
1. (Optional) Create a `.env.example` template in your project root (and optionally in subdirectories) for automatic `.env` generation:
|
|
32
34
|
```env
|
|
33
35
|
# API Configuration
|
|
34
36
|
API_KEY={{ API_KEY }}
|
|
@@ -38,6 +40,10 @@ API_PORT={{ auto_port() }}
|
|
|
38
40
|
DB_HOST=localhost
|
|
39
41
|
DB_PORT={{ auto_port() }}
|
|
40
42
|
|
|
43
|
+
# Branch-specific Configuration
|
|
44
|
+
SERVICE_NAME=myapp-{{ branch() }}
|
|
45
|
+
DEPLOYMENT_ENV={{ branch() }}
|
|
46
|
+
|
|
41
47
|
# Example: Docker Compose variables (preserved as-is)
|
|
42
48
|
# sprout will NOT process ${...} syntax - it's passed through unchanged
|
|
43
49
|
# DB_NAME=${DB_NAME}
|
|
@@ -58,9 +64,13 @@ repo/
|
|
|
58
64
|
cd $(sprout create feature-branch --path)
|
|
59
65
|
```
|
|
60
66
|
|
|
67
|
+
**What happens when you run `sprout create`:**
|
|
68
|
+
- If `.env.example` files exist: Sprout will generate corresponding `.env` files with populated variables and unique port assignments
|
|
69
|
+
- If no `.env.example` files exist: Sprout will show a warning and create the worktree without `.env` generation
|
|
70
|
+
|
|
61
71
|
This single command:
|
|
62
72
|
- Creates a new git worktree for `feature-branch`
|
|
63
|
-
- Generates
|
|
73
|
+
- Generates `.env` files from your templates (if `.env.example` files exist)
|
|
64
74
|
- Outputs the path to the new environment
|
|
65
75
|
- Changes to that directory when wrapped in `cd $(...)`
|
|
66
76
|
|
|
@@ -146,7 +156,7 @@ Show the version of sprout.
|
|
|
146
156
|
|
|
147
157
|
## Template Syntax
|
|
148
158
|
|
|
149
|
-
sprout supports
|
|
159
|
+
sprout supports three types of placeholders in `.env.example`:
|
|
150
160
|
|
|
151
161
|
1. **Variable Placeholders**: `{{ VARIABLE_NAME }}`
|
|
152
162
|
- **First**: Checks if the variable exists in your environment (e.g., `export API_KEY=xxx`)
|
|
@@ -159,7 +169,12 @@ sprout supports two types of placeholders in `.env.example`:
|
|
|
159
169
|
- Checks system port availability
|
|
160
170
|
- Ensures global uniqueness even in monorepo setups
|
|
161
171
|
|
|
162
|
-
3. **
|
|
172
|
+
3. **Branch Name**: `{{ branch() }}`
|
|
173
|
+
- Replaced with the current branch/subtree name
|
|
174
|
+
- Useful for branch-specific configurations
|
|
175
|
+
- Example: `SERVICE_NAME=myapp-{{ branch() }}` becomes `SERVICE_NAME=myapp-feature-auth`
|
|
176
|
+
|
|
177
|
+
4. **Docker Compose Syntax (Preserved)**: `${VARIABLE}`
|
|
163
178
|
- NOT processed by sprout - passed through as-is
|
|
164
179
|
- Useful for Docker Compose variable substitution
|
|
165
180
|
- Example: `${DB_NAME:-default}` remains unchanged in generated `.env`
|
|
@@ -47,11 +47,9 @@ def create_worktree(branch_name: BranchName, path_only: bool = False) -> Never:
|
|
|
47
47
|
|
|
48
48
|
if not env_examples:
|
|
49
49
|
if not path_only:
|
|
50
|
-
console.print("[
|
|
51
|
-
console.print(f"
|
|
52
|
-
|
|
53
|
-
typer.echo(f"Error: No .env.example files found in {git_root}", err=True)
|
|
54
|
-
raise typer.Exit(1)
|
|
50
|
+
console.print("[yellow]Warning: No .env.example files found[/yellow]")
|
|
51
|
+
console.print(f"Proceeding without .env generation in: {git_root}")
|
|
52
|
+
# Continue execution without exiting
|
|
55
53
|
|
|
56
54
|
# Check if worktree already exists
|
|
57
55
|
if worktree_exists(branch_name):
|
|
@@ -87,58 +85,62 @@ def create_worktree(branch_name: BranchName, path_only: bool = False) -> Never:
|
|
|
87
85
|
typer.echo(f"Error creating worktree: {e}", err=True)
|
|
88
86
|
raise typer.Exit(1) from e
|
|
89
87
|
|
|
90
|
-
# Generate .env files
|
|
91
|
-
if
|
|
92
|
-
console.print(f"Generating .env files from {len(env_examples)} template(s)...")
|
|
93
|
-
|
|
94
|
-
# Get all currently used ports to avoid conflicts
|
|
95
|
-
all_used_ports = get_used_ports()
|
|
96
|
-
session_ports: set[int] = set()
|
|
97
|
-
|
|
98
|
-
try:
|
|
99
|
-
for env_example in env_examples:
|
|
100
|
-
# Calculate relative path from git root
|
|
101
|
-
relative_dir = env_example.parent.relative_to(git_root)
|
|
102
|
-
|
|
103
|
-
# Create target directory in worktree if needed
|
|
104
|
-
if relative_dir != Path("."):
|
|
105
|
-
target_dir = worktree_path / relative_dir
|
|
106
|
-
target_dir.mkdir(parents=True, exist_ok=True)
|
|
107
|
-
env_file = target_dir / ".env"
|
|
108
|
-
else:
|
|
109
|
-
env_file = worktree_path / ".env"
|
|
110
|
-
|
|
111
|
-
# Parse template with combined used ports
|
|
112
|
-
env_content = parse_env_template(
|
|
113
|
-
env_example, silent=path_only, used_ports=all_used_ports | session_ports
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
# Extract ports from generated content and add to session_ports
|
|
117
|
-
port_matches = re.findall(r"=(\d{4,5})\b", env_content)
|
|
118
|
-
for port_str in port_matches:
|
|
119
|
-
port = int(port_str)
|
|
120
|
-
if 1024 <= port <= 65535:
|
|
121
|
-
session_ports.add(port)
|
|
122
|
-
|
|
123
|
-
# Write the .env file
|
|
124
|
-
env_file.write_text(env_content)
|
|
125
|
-
|
|
126
|
-
except SproutError as e:
|
|
127
|
-
if not path_only:
|
|
128
|
-
console.print(f"[red]Error generating .env file: {e}[/red]")
|
|
129
|
-
else:
|
|
130
|
-
typer.echo(f"Error generating .env file: {e}", err=True)
|
|
131
|
-
# Clean up worktree on failure
|
|
132
|
-
run_command(["git", "worktree", "remove", str(worktree_path)], check=False)
|
|
133
|
-
raise typer.Exit(1) from e
|
|
134
|
-
except KeyboardInterrupt:
|
|
88
|
+
# Generate .env files only if .env.example files exist
|
|
89
|
+
if env_examples:
|
|
135
90
|
if not path_only:
|
|
136
|
-
console.print("
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
91
|
+
console.print(f"Generating .env files from {len(env_examples)} template(s)...")
|
|
92
|
+
|
|
93
|
+
# Get all currently used ports to avoid conflicts
|
|
94
|
+
all_used_ports = get_used_ports()
|
|
95
|
+
session_ports: set[int] = set()
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
for env_example in env_examples:
|
|
99
|
+
# Calculate relative path from git root
|
|
100
|
+
relative_dir = env_example.parent.relative_to(git_root)
|
|
101
|
+
|
|
102
|
+
# Create target directory in worktree if needed
|
|
103
|
+
if relative_dir != Path("."):
|
|
104
|
+
target_dir = worktree_path / relative_dir
|
|
105
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
106
|
+
env_file = target_dir / ".env"
|
|
107
|
+
else:
|
|
108
|
+
env_file = worktree_path / ".env"
|
|
109
|
+
|
|
110
|
+
# Parse template with combined used ports
|
|
111
|
+
env_content = parse_env_template(
|
|
112
|
+
env_example,
|
|
113
|
+
silent=path_only,
|
|
114
|
+
used_ports=all_used_ports | session_ports,
|
|
115
|
+
branch_name=branch_name,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Extract ports from generated content and add to session_ports
|
|
119
|
+
port_matches = re.findall(r"=(\d{4,5})\b", env_content)
|
|
120
|
+
for port_str in port_matches:
|
|
121
|
+
port = int(port_str)
|
|
122
|
+
if 1024 <= port <= 65535:
|
|
123
|
+
session_ports.add(port)
|
|
124
|
+
|
|
125
|
+
# Write the .env file
|
|
126
|
+
env_file.write_text(env_content)
|
|
127
|
+
|
|
128
|
+
except SproutError as e:
|
|
129
|
+
if not path_only:
|
|
130
|
+
console.print(f"[red]Error generating .env file: {e}[/red]")
|
|
131
|
+
else:
|
|
132
|
+
typer.echo(f"Error generating .env file: {e}", err=True)
|
|
133
|
+
# Clean up worktree on failure
|
|
134
|
+
run_command(["git", "worktree", "remove", str(worktree_path)], check=False)
|
|
135
|
+
raise typer.Exit(1) from e
|
|
136
|
+
except KeyboardInterrupt:
|
|
137
|
+
if not path_only:
|
|
138
|
+
console.print("\n[yellow]Cancelled by user[/yellow]")
|
|
139
|
+
else:
|
|
140
|
+
typer.echo("Cancelled by user", err=True)
|
|
141
|
+
# Clean up worktree on cancellation
|
|
142
|
+
run_command(["git", "worktree", "remove", str(worktree_path)], check=False)
|
|
143
|
+
raise typer.Exit(130) from None
|
|
142
144
|
|
|
143
145
|
# Success message or path output
|
|
144
146
|
if path_only:
|
|
@@ -146,8 +148,17 @@ def create_worktree(branch_name: BranchName, path_only: bool = False) -> Never:
|
|
|
146
148
|
print(str(worktree_path))
|
|
147
149
|
else:
|
|
148
150
|
console.print(f"\n[green]✅ Workspace '{branch_name}' created successfully![/green]\n")
|
|
151
|
+
if env_examples:
|
|
152
|
+
console.print(f"Generated .env files from {len(env_examples)} template(s)")
|
|
153
|
+
else:
|
|
154
|
+
console.print("No .env files generated (no .env.example templates found)")
|
|
149
155
|
console.print("Navigate to your new environment with:")
|
|
150
|
-
|
|
156
|
+
try:
|
|
157
|
+
relative_path = worktree_path.relative_to(Path.cwd())
|
|
158
|
+
console.print(f" [cyan]cd {relative_path}[/cyan]")
|
|
159
|
+
except ValueError:
|
|
160
|
+
# If worktree_path is not relative to current directory, show absolute path
|
|
161
|
+
console.print(f" [cyan]cd {worktree_path}[/cyan]")
|
|
151
162
|
|
|
152
163
|
# Exit successfully
|
|
153
164
|
raise typer.Exit(0)
|
|
@@ -126,7 +126,10 @@ def find_available_port() -> PortNumber:
|
|
|
126
126
|
|
|
127
127
|
|
|
128
128
|
def parse_env_template(
|
|
129
|
-
template_path: Path,
|
|
129
|
+
template_path: Path,
|
|
130
|
+
silent: bool = False,
|
|
131
|
+
used_ports: PortSet | None = None,
|
|
132
|
+
branch_name: str | None = None,
|
|
130
133
|
) -> str:
|
|
131
134
|
"""Parse .env.example template and process placeholders.
|
|
132
135
|
|
|
@@ -134,6 +137,7 @@ def parse_env_template(
|
|
|
134
137
|
template_path: Path to the .env.example template file
|
|
135
138
|
silent: If True, use stderr for prompts to keep stdout clean
|
|
136
139
|
used_ports: Set of ports already in use (in addition to system-wide used ports)
|
|
140
|
+
branch_name: Branch name to use for {{ branch() }} placeholders
|
|
137
141
|
"""
|
|
138
142
|
if not template_path.exists():
|
|
139
143
|
raise SproutError(f".env.example file not found at {template_path}")
|
|
@@ -161,6 +165,10 @@ def parse_env_template(
|
|
|
161
165
|
|
|
162
166
|
line = re.sub(r"{{\s*auto_port\(\)\s*}}", replace_auto_port, line)
|
|
163
167
|
|
|
168
|
+
# Process {{ branch() }} placeholders
|
|
169
|
+
if branch_name:
|
|
170
|
+
line = re.sub(r"{{\s*branch\(\)\s*}}", branch_name, line)
|
|
171
|
+
|
|
164
172
|
# Process {{ VARIABLE }} placeholders
|
|
165
173
|
def replace_variable(match: re.Match[str]) -> str:
|
|
166
174
|
var_name = match.group(1).strip()
|
|
@@ -187,7 +195,8 @@ def parse_env_template(
|
|
|
187
195
|
value = console.input(prompt)
|
|
188
196
|
return value
|
|
189
197
|
|
|
190
|
-
|
|
198
|
+
# Only match variables that don't look like function calls (no parentheses)
|
|
199
|
+
line = re.sub(r"{{\s*([^}()]+)\s*}}", replace_variable, line)
|
|
191
200
|
|
|
192
201
|
lines.append(line)
|
|
193
202
|
|
|
@@ -126,20 +126,37 @@ class TestCreateCommand:
|
|
|
126
126
|
# stdout should be empty
|
|
127
127
|
assert result.stdout == ""
|
|
128
128
|
|
|
129
|
-
def test_create_no_env_example(self, mocker):
|
|
130
|
-
"""Test
|
|
129
|
+
def test_create_no_env_example(self, mocker, tmp_path):
|
|
130
|
+
"""Test success when .env.example doesn't exist."""
|
|
131
131
|
mocker.patch("sprout.commands.create.is_git_repository", return_value=True)
|
|
132
|
-
mock_git_root =
|
|
132
|
+
mock_git_root = tmp_path / "project"
|
|
133
|
+
mock_git_root.mkdir()
|
|
133
134
|
mocker.patch("sprout.commands.create.get_git_root", return_value=mock_git_root)
|
|
135
|
+
mocker.patch("sprout.commands.create.worktree_exists", return_value=False)
|
|
136
|
+
sprout_dir = tmp_path / ".sprout"
|
|
137
|
+
sprout_dir.mkdir()
|
|
138
|
+
mocker.patch("sprout.commands.create.ensure_sprout_dir", return_value=sprout_dir)
|
|
139
|
+
mocker.patch("sprout.commands.create.branch_exists", return_value=False)
|
|
134
140
|
|
|
135
|
-
# Mock git ls-files to return empty list
|
|
141
|
+
# Mock git ls-files to return empty list (no .env.example files)
|
|
136
142
|
mock_run = mocker.patch("sprout.commands.create.run_command")
|
|
137
143
|
mock_run.return_value = Mock(stdout="", returncode=0)
|
|
138
144
|
|
|
139
|
-
|
|
145
|
+
# Change to project directory to make relative path calculation work
|
|
146
|
+
import os
|
|
140
147
|
|
|
141
|
-
|
|
142
|
-
|
|
148
|
+
old_cwd = os.getcwd()
|
|
149
|
+
os.chdir(str(mock_git_root))
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
result = runner.invoke(app, ["create", "feature-branch"])
|
|
153
|
+
|
|
154
|
+
assert result.exit_code == 0
|
|
155
|
+
assert "Warning: No .env.example files found" in result.stdout
|
|
156
|
+
assert "Workspace 'feature-branch' created successfully!" in result.stdout
|
|
157
|
+
assert "No .env files generated (no .env.example templates found)" in result.stdout
|
|
158
|
+
finally:
|
|
159
|
+
os.chdir(old_cwd)
|
|
143
160
|
|
|
144
161
|
def test_create_worktree_exists(self, mocker):
|
|
145
162
|
"""Test error when worktree already exists."""
|
|
@@ -161,6 +178,28 @@ class TestCreateCommand:
|
|
|
161
178
|
assert result.exit_code == 1
|
|
162
179
|
assert "Worktree for branch 'feature-branch' already exists" in result.stdout
|
|
163
180
|
|
|
181
|
+
def test_create_without_env_example_path_mode(self, mocker, tmp_path):
|
|
182
|
+
"""Test path mode with no .env.example files."""
|
|
183
|
+
mocker.patch("sprout.commands.create.is_git_repository", return_value=True)
|
|
184
|
+
mock_git_root = tmp_path / "project"
|
|
185
|
+
mock_git_root.mkdir()
|
|
186
|
+
mocker.patch("sprout.commands.create.get_git_root", return_value=mock_git_root)
|
|
187
|
+
mocker.patch("sprout.commands.create.worktree_exists", return_value=False)
|
|
188
|
+
sprout_dir = tmp_path / ".sprout"
|
|
189
|
+
sprout_dir.mkdir()
|
|
190
|
+
mocker.patch("sprout.commands.create.ensure_sprout_dir", return_value=sprout_dir)
|
|
191
|
+
mocker.patch("sprout.commands.create.branch_exists", return_value=False)
|
|
192
|
+
|
|
193
|
+
# Mock git ls-files to return empty list (no .env.example files)
|
|
194
|
+
mock_run = mocker.patch("sprout.commands.create.run_command")
|
|
195
|
+
mock_run.return_value = Mock(stdout="", returncode=0)
|
|
196
|
+
|
|
197
|
+
result = runner.invoke(app, ["create", "feature-branch", "--path"])
|
|
198
|
+
|
|
199
|
+
assert result.exit_code == 0
|
|
200
|
+
# In path mode, only the path should be printed to stdout
|
|
201
|
+
assert result.stdout.strip() == str(sprout_dir / "feature-branch")
|
|
202
|
+
|
|
164
203
|
|
|
165
204
|
class TestLsCommand:
|
|
166
205
|
"""Test sprout ls command."""
|
|
@@ -198,6 +198,47 @@ class TestIntegrationWorkflow:
|
|
|
198
198
|
worktree_path = git_repo / ".sprout" / "existing-branch"
|
|
199
199
|
assert worktree_path.exists()
|
|
200
200
|
|
|
201
|
+
def test_branch_placeholder(self, git_repo, monkeypatch):
|
|
202
|
+
"""Test that {{ branch() }} placeholder is replaced with branch name."""
|
|
203
|
+
git_repo, default_branch = git_repo
|
|
204
|
+
monkeypatch.chdir(git_repo)
|
|
205
|
+
monkeypatch.setenv("API_KEY", "test_key")
|
|
206
|
+
|
|
207
|
+
# Create .env.example with branch() placeholder
|
|
208
|
+
env_example = git_repo / ".env.example"
|
|
209
|
+
env_example.write_text(
|
|
210
|
+
"# Branch-specific configuration\n"
|
|
211
|
+
"BRANCH_NAME={{ branch() }}\n"
|
|
212
|
+
"SERVICE_NAME=myapp-{{ branch() }}\n"
|
|
213
|
+
"API_KEY={{ API_KEY }}\n"
|
|
214
|
+
"PORT={{ auto_port() }}\n"
|
|
215
|
+
"COMPOSE_VAR=${BRANCH_NAME}\n"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# Add to git
|
|
219
|
+
subprocess.run(["git", "add", ".env.example"], cwd=git_repo, check=True)
|
|
220
|
+
subprocess.run(
|
|
221
|
+
["git", "commit", "-m", "Add .env.example with branch placeholder"],
|
|
222
|
+
cwd=git_repo,
|
|
223
|
+
check=True,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
# Create worktree
|
|
227
|
+
branch_name = "feature-auth"
|
|
228
|
+
result = runner.invoke(app, ["create", branch_name])
|
|
229
|
+
assert result.exit_code == 0
|
|
230
|
+
|
|
231
|
+
# Verify .env was created with correct branch name substitution
|
|
232
|
+
env_file = git_repo / ".sprout" / branch_name / ".env"
|
|
233
|
+
env_content = env_file.read_text()
|
|
234
|
+
|
|
235
|
+
# Check branch name substitutions
|
|
236
|
+
assert f"BRANCH_NAME={branch_name}" in env_content
|
|
237
|
+
assert f"SERVICE_NAME=myapp-{branch_name}" in env_content
|
|
238
|
+
assert "API_KEY=test_key" in env_content
|
|
239
|
+
assert "PORT=" in env_content # Should have a port number
|
|
240
|
+
assert "COMPOSE_VAR=${BRANCH_NAME}" in env_content # Docker syntax preserved
|
|
241
|
+
|
|
201
242
|
def test_error_cases(self, git_repo, monkeypatch, tmp_path):
|
|
202
243
|
"""Test various error conditions."""
|
|
203
244
|
git_repo, default_branch = git_repo
|
|
@@ -223,11 +264,12 @@ class TestIntegrationWorkflow:
|
|
|
223
264
|
result = runner.invoke(app, ["path", "nonexistent"])
|
|
224
265
|
assert result.exit_code == 1
|
|
225
266
|
|
|
226
|
-
# Remove .env.example and try to create
|
|
267
|
+
# Remove .env.example and try to create (should succeed now)
|
|
227
268
|
(git_repo / ".env.example").unlink()
|
|
228
269
|
result = runner.invoke(app, ["create", "another-branch"])
|
|
229
|
-
assert result.exit_code ==
|
|
230
|
-
assert "No .env.example files found" in result.stdout
|
|
270
|
+
assert result.exit_code == 0
|
|
271
|
+
assert "Warning: No .env.example files found" in result.stdout
|
|
272
|
+
assert "Workspace 'another-branch' created successfully!" in result.stdout
|
|
231
273
|
|
|
232
274
|
# Test outside git repo using a separate temp directory
|
|
233
275
|
import tempfile
|
|
@@ -179,6 +179,47 @@ class TestEnvTemplateParser:
|
|
|
179
179
|
result = parse_env_template(template)
|
|
180
180
|
assert result == "COMPOSE_VAR=${COMPOSE_VAR:-default}"
|
|
181
181
|
|
|
182
|
+
def test_parse_env_template_branch_placeholder(self, tmp_path):
|
|
183
|
+
"""Test parsing {{ branch() }} placeholders."""
|
|
184
|
+
template = tmp_path / ".env.example"
|
|
185
|
+
template.write_text("BRANCH_NAME={{ branch() }}\nFEATURE={{ branch() }}_feature")
|
|
186
|
+
|
|
187
|
+
result = parse_env_template(template, branch_name="feature-auth")
|
|
188
|
+
assert result == "BRANCH_NAME=feature-auth\nFEATURE=feature-auth_feature"
|
|
189
|
+
|
|
190
|
+
def test_parse_env_template_branch_placeholder_none(self, tmp_path):
|
|
191
|
+
"""Test {{ branch() }} placeholder when branch_name is None."""
|
|
192
|
+
template = tmp_path / ".env.example"
|
|
193
|
+
template.write_text("BRANCH={{ branch() }}")
|
|
194
|
+
|
|
195
|
+
# When branch_name is None, placeholder should remain unchanged
|
|
196
|
+
result = parse_env_template(template, branch_name=None)
|
|
197
|
+
assert result == "BRANCH={{ branch() }}"
|
|
198
|
+
|
|
199
|
+
def test_parse_env_template_mixed_placeholders(self, tmp_path, mocker):
|
|
200
|
+
"""Test parsing mixed placeholders in one template."""
|
|
201
|
+
mocker.patch("sprout.utils.find_available_port", return_value=8080)
|
|
202
|
+
mocker.patch.dict(os.environ, {"API_KEY": "secret"})
|
|
203
|
+
mocker.patch("sprout.utils.console.input", return_value="password123")
|
|
204
|
+
|
|
205
|
+
template = tmp_path / ".env.example"
|
|
206
|
+
template.write_text(
|
|
207
|
+
"API_KEY={{ API_KEY }}\n"
|
|
208
|
+
"PORT={{ auto_port() }}\n"
|
|
209
|
+
"BRANCH={{ branch() }}\n"
|
|
210
|
+
"DB_PASS={{ DB_PASS }}\n"
|
|
211
|
+
"COMPOSE_VAR=${COMPOSE_VAR}"
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
result = parse_env_template(template, branch_name="feature-xyz")
|
|
215
|
+
assert result == (
|
|
216
|
+
"API_KEY=secret\n"
|
|
217
|
+
"PORT=8080\n"
|
|
218
|
+
"BRANCH=feature-xyz\n"
|
|
219
|
+
"DB_PASS=password123\n"
|
|
220
|
+
"COMPOSE_VAR=${COMPOSE_VAR}"
|
|
221
|
+
)
|
|
222
|
+
|
|
182
223
|
def test_parse_env_template_file_not_found(self, tmp_path):
|
|
183
224
|
"""Test error when template file doesn't exist."""
|
|
184
225
|
template = tmp_path / "nonexistent.env"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|