sprout-cli 0.5.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sprout-cli
3
- Version: 0.5.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
@@ -68,6 +68,10 @@ API_PORT={{ auto_port() }}
68
68
  DB_HOST=localhost
69
69
  DB_PORT={{ auto_port() }}
70
70
 
71
+ # Branch-specific Configuration
72
+ SERVICE_NAME=myapp-{{ branch() }}
73
+ DEPLOYMENT_ENV={{ branch() }}
74
+
71
75
  # Example: Docker Compose variables (preserved as-is)
72
76
  # sprout will NOT process ${...} syntax - it's passed through unchanged
73
77
  # DB_NAME=${DB_NAME}
@@ -180,7 +184,7 @@ Show the version of sprout.
180
184
 
181
185
  ## Template Syntax
182
186
 
183
- sprout supports two types of placeholders in `.env.example`:
187
+ sprout supports three types of placeholders in `.env.example`:
184
188
 
185
189
  1. **Variable Placeholders**: `{{ VARIABLE_NAME }}`
186
190
  - **First**: Checks if the variable exists in your environment (e.g., `export API_KEY=xxx`)
@@ -193,7 +197,12 @@ sprout supports two types of placeholders in `.env.example`:
193
197
  - Checks system port availability
194
198
  - Ensures global uniqueness even in monorepo setups
195
199
 
196
- 3. **Docker Compose Syntax (Preserved)**: `${VARIABLE}`
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}`
197
206
  - NOT processed by sprout - passed through as-is
198
207
  - Useful for Docker Compose variable substitution
199
208
  - Example: `${DB_NAME:-default}` remains unchanged in generated `.env`
@@ -40,6 +40,10 @@ API_PORT={{ auto_port() }}
40
40
  DB_HOST=localhost
41
41
  DB_PORT={{ auto_port() }}
42
42
 
43
+ # Branch-specific Configuration
44
+ SERVICE_NAME=myapp-{{ branch() }}
45
+ DEPLOYMENT_ENV={{ branch() }}
46
+
43
47
  # Example: Docker Compose variables (preserved as-is)
44
48
  # sprout will NOT process ${...} syntax - it's passed through unchanged
45
49
  # DB_NAME=${DB_NAME}
@@ -152,7 +156,7 @@ Show the version of sprout.
152
156
 
153
157
  ## Template Syntax
154
158
 
155
- sprout supports two types of placeholders in `.env.example`:
159
+ sprout supports three types of placeholders in `.env.example`:
156
160
 
157
161
  1. **Variable Placeholders**: `{{ VARIABLE_NAME }}`
158
162
  - **First**: Checks if the variable exists in your environment (e.g., `export API_KEY=xxx`)
@@ -165,7 +169,12 @@ sprout supports two types of placeholders in `.env.example`:
165
169
  - Checks system port availability
166
170
  - Ensures global uniqueness even in monorepo setups
167
171
 
168
- 3. **Docker Compose Syntax (Preserved)**: `${VARIABLE}`
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}`
169
178
  - NOT processed by sprout - passed through as-is
170
179
  - Useful for Docker Compose variable substitution
171
180
  - Example: `${DB_NAME:-default}` remains unchanged in generated `.env`
@@ -1,3 +1,3 @@
1
1
  """sprout - CLI tool to automate git worktree and Docker Compose development workflows."""
2
2
 
3
- __version__ = "0.5.0"
3
+ __version__ = "0.6.0"
@@ -109,7 +109,10 @@ def create_worktree(branch_name: BranchName, path_only: bool = False) -> Never:
109
109
 
110
110
  # Parse template with combined used ports
111
111
  env_content = parse_env_template(
112
- env_example, silent=path_only, used_ports=all_used_ports | session_ports
112
+ env_example,
113
+ silent=path_only,
114
+ used_ports=all_used_ports | session_ports,
115
+ branch_name=branch_name,
113
116
  )
114
117
 
115
118
  # Extract ports from generated content and add to session_ports
@@ -126,7 +126,10 @@ def find_available_port() -> PortNumber:
126
126
 
127
127
 
128
128
  def parse_env_template(
129
- template_path: Path, silent: bool = False, used_ports: PortSet | None = None
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
- line = re.sub(r"{{\s*([^}]+)\s*}}", replace_variable, line)
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
 
@@ -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
@@ -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