rhiza 0.8.8__py3-none-any.whl → 0.9.1__py3-none-any.whl

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.
rhiza/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """Rhiza — Manage reusable configuration templates for Python projects.
2
2
 
3
- Rhiza is a commandline interface (CLI) that helps you maintain consistent
3
+ Rhiza is a command-line interface (CLI) that helps you maintain consistent
4
4
  configuration across multiple Python projects using templates stored in a
5
5
  central repository. It can initialize projects with standard configuration,
6
6
  materialize (inject) template files into a target repository, and validate the
@@ -11,8 +11,8 @@ template configuration.
11
11
  - Template initialization for new or existing projects.
12
12
  - Template materialization with selective include/exclude support.
13
13
  - Configuration validation (syntax and basic semantics).
14
- - Multihost support (GitHub and GitLab).
15
- - Nondestructive updates by default, with an explicit `--force` flag.
14
+ - Multi-host support (GitHub and GitLab).
15
+ - Non-destructive updates by default, with an explicit `--force` flag.
16
16
 
17
17
  ## Quick start
18
18
 
@@ -29,7 +29,7 @@ Validate your configuration:
29
29
  rhiza validate
30
30
  ```
31
31
 
32
- Customize `.github/rhiza/template.yml`, then materialize templates into your project:
32
+ Customize `.rhiza/template.yml`, then materialize templates into your project:
33
33
 
34
34
  ```bash
35
35
  rhiza materialize
rhiza/__main__.py CHANGED
@@ -11,7 +11,7 @@ import typer
11
11
  from rhiza.cli import app
12
12
 
13
13
 
14
- def load_plugins(app: typer.Typer):
14
+ def load_plugins(app: typer.Typer) -> None:
15
15
  """Load plugins from entry points."""
16
16
  # 'rhiza.plugins' matches the group we defined in rhiza-tools
17
17
  plugin_entries = entry_points(group="rhiza.plugins")
rhiza/cli.py CHANGED
@@ -5,6 +5,7 @@ Commands are thin wrappers around implementations in `rhiza.commands.*`.
5
5
  """
6
6
 
7
7
  from pathlib import Path
8
+ from typing import Annotated
8
9
 
9
10
  import typer
10
11
 
@@ -13,6 +14,7 @@ from rhiza.commands import init as init_cmd
13
14
  from rhiza.commands import materialize as materialize_cmd
14
15
  from rhiza.commands import validate as validate_cmd
15
16
  from rhiza.commands.migrate import migrate as migrate_cmd
17
+ from rhiza.commands.summarise import summarise as summarise_cmd
16
18
  from rhiza.commands.uninstall import uninstall as uninstall_cmd
17
19
  from rhiza.commands.welcome import welcome as welcome_cmd
18
20
 
@@ -21,14 +23,14 @@ app = typer.Typer(
21
23
  """
22
24
  Rhiza - Manage reusable configuration templates for Python projects
23
25
 
24
- \x1b]8;;https://jebel-quant.github.io/rhiza-cli/\x1b\\https://jebel-quant.github.io/rhiza-cli/\x1b]8;;\x1b\\
26
+ https://jebel-quant.github.io/rhiza-cli/
25
27
  """
26
28
  ),
27
29
  add_completion=True,
28
30
  )
29
31
 
30
32
 
31
- def version_callback(value: bool):
33
+ def version_callback(value: bool) -> None:
32
34
  """Print version information and exit.
33
35
 
34
36
  Args:
@@ -52,7 +54,7 @@ def main(
52
54
  callback=version_callback,
53
55
  is_eager=True,
54
56
  ),
55
- ):
57
+ ) -> None:
56
58
  """Rhiza CLI main callback.
57
59
 
58
60
  This callback is executed before any command. It handles global options
@@ -65,13 +67,15 @@ def main(
65
67
 
66
68
  @app.command()
67
69
  def init(
68
- target: Path = typer.Argument(
69
- default=Path("."), # default to current directory
70
- exists=True,
71
- file_okay=False,
72
- dir_okay=True,
73
- help="Target directory (defaults to current directory)",
74
- ),
70
+ target: Annotated[
71
+ Path,
72
+ typer.Argument(
73
+ exists=True,
74
+ file_okay=False,
75
+ dir_okay=True,
76
+ help="Target directory (defaults to current directory)",
77
+ ),
78
+ ] = Path("."),
75
79
  project_name: str = typer.Option(
76
80
  None,
77
81
  "--project-name",
@@ -93,10 +97,20 @@ def init(
93
97
  help="Target Git hosting platform (github or gitlab). Determines which CI/CD files to include. "
94
98
  "If not provided, will prompt interactively.",
95
99
  ),
96
- ):
97
- r"""Initialize or validate .github/rhiza/template.yml.
100
+ template_repository: str = typer.Option(
101
+ None,
102
+ "--template-repository",
103
+ help="Custom template repository (format: owner/repo). Defaults to 'jebel-quant/rhiza'.",
104
+ ),
105
+ template_branch: str = typer.Option(
106
+ None,
107
+ "--template-branch",
108
+ help="Custom template branch. Defaults to 'main'.",
109
+ ),
110
+ ) -> None:
111
+ r"""Initialize or validate .rhiza/template.yml.
98
112
 
99
- Creates a default `.github/rhiza/template.yml` configuration file if one
113
+ Creates a default `.rhiza/template.yml` configuration file if one
100
114
  doesn't exist, or validates the existing configuration.
101
115
 
102
116
  The default template includes common Python project files.
@@ -108,6 +122,8 @@ def init(
108
122
  rhiza init
109
123
  rhiza init --git-host github
110
124
  rhiza init --git-host gitlab
125
+ rhiza init --template-repository myorg/my-templates
126
+ rhiza init --template-repository myorg/my-templates --template-branch develop
111
127
  rhiza init /path/to/project
112
128
  rhiza init ..
113
129
  """
@@ -117,18 +133,22 @@ def init(
117
133
  package_name=package_name,
118
134
  with_dev_dependencies=with_dev_dependencies,
119
135
  git_host=git_host,
136
+ template_repository=template_repository,
137
+ template_branch=template_branch,
120
138
  )
121
139
 
122
140
 
123
141
  @app.command()
124
142
  def materialize(
125
- target: Path = typer.Argument(
126
- default=Path("."), # default to current directory
127
- exists=True,
128
- file_okay=False,
129
- dir_okay=True,
130
- help="Target git repository (defaults to current directory)",
131
- ),
143
+ target: Annotated[
144
+ Path,
145
+ typer.Argument(
146
+ exists=True,
147
+ file_okay=False,
148
+ dir_okay=True,
149
+ help="Target git repository (defaults to current directory)",
150
+ ),
151
+ ] = Path("."),
132
152
  branch: str = typer.Option("main", "--branch", "-b", help="Rhiza branch to use"),
133
153
  target_branch: str = typer.Option(
134
154
  None,
@@ -137,13 +157,13 @@ def materialize(
137
157
  help="Create and checkout a new branch in the target repository for changes",
138
158
  ),
139
159
  force: bool = typer.Option(False, "--force", "-y", help="Overwrite existing files"),
140
- ):
160
+ ) -> None:
141
161
  r"""Inject Rhiza configuration templates into a target repository.
142
162
 
143
163
  Materializes configuration files from the template repository specified
144
- in .github/rhiza/template.yml into your project. This command:
164
+ in .rhiza/template.yml into your project. This command:
145
165
 
146
- - Reads .github/rhiza/template.yml configuration
166
+ - Reads .rhiza/template.yml configuration
147
167
  - Performs a sparse clone of the template repository
148
168
  - Copies specified files/directories to your project
149
169
  - Respects exclusion patterns defined in the configuration
@@ -161,17 +181,19 @@ def materialize(
161
181
 
162
182
  @app.command()
163
183
  def validate(
164
- target: Path = typer.Argument(
165
- default=Path("."), # default to current directory
166
- exists=True,
167
- file_okay=False,
168
- dir_okay=True,
169
- help="Target git repository (defaults to current directory)",
170
- ),
171
- ):
184
+ target: Annotated[
185
+ Path,
186
+ typer.Argument(
187
+ exists=True,
188
+ file_okay=False,
189
+ dir_okay=True,
190
+ help="Target git repository (defaults to current directory)",
191
+ ),
192
+ ] = Path("."),
193
+ ) -> None:
172
194
  r"""Validate Rhiza template configuration.
173
195
 
174
- Validates the .github/rhiza/template.yml file to ensure it is syntactically
196
+ Validates the .rhiza/template.yml file to ensure it is syntactically
175
197
  correct and semantically valid.
176
198
 
177
199
  Performs comprehensive validation:
@@ -196,14 +218,16 @@ def validate(
196
218
 
197
219
  @app.command()
198
220
  def migrate(
199
- target: Path = typer.Argument(
200
- default=Path("."), # default to current directory
201
- exists=True,
202
- file_okay=False,
203
- dir_okay=True,
204
- help="Target git repository (defaults to current directory)",
205
- ),
206
- ):
221
+ target: Annotated[
222
+ Path,
223
+ typer.Argument(
224
+ exists=True,
225
+ file_okay=False,
226
+ dir_okay=True,
227
+ help="Target git repository (defaults to current directory)",
228
+ ),
229
+ ] = Path("."),
230
+ ) -> None:
207
231
  r"""Migrate project to the new .rhiza folder structure.
208
232
 
209
233
  This command helps transition projects to use the new `.rhiza/` folder
@@ -229,7 +253,7 @@ def migrate(
229
253
 
230
254
 
231
255
  @app.command()
232
- def welcome():
256
+ def welcome() -> None:
233
257
  r"""Display a friendly welcome message and explain what Rhiza is.
234
258
 
235
259
  Shows a welcome message, explains Rhiza's purpose, key features,
@@ -243,20 +267,22 @@ def welcome():
243
267
 
244
268
  @app.command()
245
269
  def uninstall(
246
- target: Path = typer.Argument(
247
- default=Path("."), # default to current directory
248
- exists=True,
249
- file_okay=False,
250
- dir_okay=True,
251
- help="Target git repository (defaults to current directory)",
252
- ),
270
+ target: Annotated[
271
+ Path,
272
+ typer.Argument(
273
+ exists=True,
274
+ file_okay=False,
275
+ dir_okay=True,
276
+ help="Target git repository (defaults to current directory)",
277
+ ),
278
+ ] = Path("."),
253
279
  force: bool = typer.Option(
254
280
  False,
255
281
  "--force",
256
282
  "-y",
257
283
  help="Skip confirmation prompt and proceed with deletion",
258
284
  ),
259
- ):
285
+ ) -> None:
260
286
  r"""Remove all Rhiza-managed files from the repository.
261
287
 
262
288
  Reads the `.rhiza.history` file and removes all files that were
@@ -280,3 +306,50 @@ def uninstall(
280
306
  rhiza uninstall /path/to/project -y
281
307
  """
282
308
  uninstall_cmd(target, force)
309
+
310
+
311
+ @app.command()
312
+ def summarise(
313
+ target: Annotated[
314
+ Path,
315
+ typer.Argument(
316
+ exists=True,
317
+ file_okay=False,
318
+ dir_okay=True,
319
+ help="Target git repository (defaults to current directory)",
320
+ ),
321
+ ] = Path("."),
322
+ output: Annotated[
323
+ Path | None,
324
+ typer.Option(
325
+ "--output",
326
+ "-o",
327
+ help="Output file path (defaults to stdout)",
328
+ ),
329
+ ] = None,
330
+ ) -> None:
331
+ r"""Generate a summary of staged changes for PR descriptions.
332
+
333
+ Analyzes staged git changes and generates a structured PR description
334
+ that includes:
335
+
336
+ - Summary statistics (files added/modified/deleted)
337
+ - Changes categorized by type (workflows, configs, docs, tests, etc.)
338
+ - Template repository information
339
+ - Last sync date
340
+
341
+ This is useful when creating pull requests after running `rhiza materialize`
342
+ to provide reviewers with a clear overview of what changed.
343
+
344
+ Examples:
345
+ rhiza summarise
346
+ rhiza summarise --output pr-description.md
347
+ rhiza summarise /path/to/project -o description.md
348
+
349
+ Typical workflow:
350
+ rhiza materialize
351
+ git add .
352
+ rhiza summarise --output pr-body.md
353
+ gh pr create --title "chore: Sync with rhiza" --body-file pr-body.md
354
+ """
355
+ summarise_cmd(target, output)
@@ -8,7 +8,7 @@ configuration templates for Python projects.
8
8
 
9
9
  ### init
10
10
 
11
- Initialize or validate `.github/rhiza/template.yml` in a target directory.
11
+ Initialize or validate `.rhiza/template.yml` in a target directory.
12
12
 
13
13
  Creates a default configuration file if it doesn't exist, or validates
14
14
  an existing one. The default configuration includes common Python project
@@ -29,7 +29,7 @@ is used.
29
29
 
30
30
  Validate Rhiza template configuration.
31
31
 
32
- Validates the `.github/rhiza/template.yml` file to ensure it is syntactically
32
+ Validates the `.rhiza/template.yml` file to ensure it is syntactically
33
33
  correct and semantically valid. Performs comprehensive validation including
34
34
  YAML syntax checking, required field verification, field type validation,
35
35
  and repository format verification.
@@ -50,6 +50,8 @@ For more detailed usage examples and workflows, see the USAGE.md guide
50
50
  or try rhiza <command> --help
51
51
  """
52
52
 
53
- from .init import init # noqa: F401
54
- from .materialize import materialize # noqa: F401
55
- from .validate import validate # noqa: F401
53
+ from .init import init
54
+ from .materialize import materialize
55
+ from .validate import validate
56
+
57
+ __all__ = ["init", "materialize", "validate"]
rhiza/commands/init.py CHANGED
@@ -1,7 +1,7 @@
1
- """Command to initialize or validate .github/rhiza/template.yml.
1
+ """Command to initialize or validate .rhiza/template.yml.
2
2
 
3
3
  This module provides the init command that creates or validates the
4
- .github/rhiza/template.yml file, which defines where templates come from
4
+ .rhiza/template.yml file, which defines where templates come from
5
5
  and what paths are governed by Rhiza.
6
6
  """
7
7
 
@@ -52,7 +52,7 @@ def _validate_git_host(git_host: str | None) -> str | None:
52
52
  git_host = git_host.lower()
53
53
  if git_host not in ["github", "gitlab"]:
54
54
  logger.error(f"Invalid git-host: {git_host}. Must be 'github' or 'gitlab'")
55
- raise ValueError(f"Invalid git-host: {git_host}. Must be 'github' or 'gitlab'")
55
+ raise ValueError(f"Invalid git-host: {git_host}. Must be 'github' or 'gitlab'") # noqa: TRY003
56
56
  return git_host
57
57
 
58
58
 
@@ -81,7 +81,7 @@ def _prompt_git_host() -> str:
81
81
  git_host = "github"
82
82
  logger.debug("Non-interactive mode detected, defaulting to github")
83
83
 
84
- return git_host
84
+ return str(git_host)
85
85
 
86
86
 
87
87
  def _get_include_paths_for_host(git_host: str) -> list[str]:
@@ -124,12 +124,19 @@ def _get_include_paths_for_host(git_host: str) -> list[str]:
124
124
  ]
125
125
 
126
126
 
127
- def _create_template_file(target: Path, git_host: str) -> None:
127
+ def _create_template_file(
128
+ target: Path,
129
+ git_host: str,
130
+ template_repository: str | None = None,
131
+ template_branch: str | None = None,
132
+ ) -> None:
128
133
  """Create default template.yml file.
129
134
 
130
135
  Args:
131
136
  target: Target repository path.
132
137
  git_host: Git hosting platform.
138
+ template_repository: Custom template repository (format: owner/repo).
139
+ template_branch: Custom template branch.
133
140
  """
134
141
  rhiza_dir = target / ".rhiza"
135
142
  template_file = rhiza_dir / "template.yml"
@@ -141,9 +148,20 @@ def _create_template_file(target: Path, git_host: str) -> None:
141
148
  logger.debug("Using default template configuration")
142
149
 
143
150
  include_paths = _get_include_paths_for_host(git_host)
151
+
152
+ # Use custom template repository/branch if provided, otherwise use defaults
153
+ repo = template_repository or "jebel-quant/rhiza"
154
+ branch = template_branch or "main"
155
+
156
+ # Log when custom values are used
157
+ if template_repository:
158
+ logger.info(f"Using custom template repository: {repo}")
159
+ if template_branch:
160
+ logger.info(f"Using custom template branch: {branch}")
161
+
144
162
  default_template = RhizaTemplate(
145
- template_repository="jebel-quant/rhiza",
146
- template_branch="main",
163
+ template_repository=repo,
164
+ template_branch=branch,
147
165
  include=include_paths,
148
166
  )
149
167
 
@@ -243,10 +261,12 @@ def init(
243
261
  package_name: str | None = None,
244
262
  with_dev_dependencies: bool = False,
245
263
  git_host: str | None = None,
246
- ):
247
- """Initialize or validate .github/rhiza/template.yml in the target repository.
264
+ template_repository: str | None = None,
265
+ template_branch: str | None = None,
266
+ ) -> bool:
267
+ """Initialize or validate .rhiza/template.yml in the target repository.
248
268
 
249
- Creates a default .github/rhiza/template.yml file if it doesn't exist,
269
+ Creates a default .rhiza/template.yml file if it doesn't exist,
250
270
  or validates an existing one.
251
271
 
252
272
  Args:
@@ -256,6 +276,9 @@ def init(
256
276
  with_dev_dependencies: Include development dependencies in pyproject.toml.
257
277
  git_host: Target Git hosting platform ("github" or "gitlab"). Determines which
258
278
  CI/CD configuration files to include. If None, will prompt user interactively.
279
+ template_repository: Custom template repository (format: owner/repo).
280
+ Defaults to 'jebel-quant/rhiza'.
281
+ template_branch: Custom template branch. Defaults to 'main'.
259
282
 
260
283
  Returns:
261
284
  bool: True if validation passes, False otherwise.
@@ -275,7 +298,7 @@ def init(
275
298
  git_host = _prompt_git_host()
276
299
 
277
300
  # Create template file
278
- _create_template_file(target, git_host)
301
+ _create_template_file(target, git_host, template_repository, template_branch)
279
302
 
280
303
  # Bootstrap Python project structure
281
304
  if project_name is None:
@@ -8,7 +8,7 @@ into the target Git repository, and records managed files in
8
8
 
9
9
  import os
10
10
  import shutil
11
- import subprocess
11
+ import subprocess # nosec B404
12
12
  import sys
13
13
  import tempfile
14
14
  from pathlib import Path
@@ -20,6 +20,22 @@ from rhiza.models import RhizaTemplate
20
20
  from rhiza.subprocess_utils import get_git_executable
21
21
 
22
22
 
23
+ def _log_git_stderr_errors(stderr: str | None) -> None:
24
+ """Extract and log only relevant error messages from git stderr.
25
+
26
+ Args:
27
+ stderr: Git command stderr output.
28
+ """
29
+ if stderr:
30
+ # Extract relevant error message from git stderr
31
+ stderr_lines = stderr.strip().split("\n")
32
+ # Show only the most relevant error lines, skip verbose git output
33
+ for line in stderr_lines:
34
+ line = line.strip()
35
+ if line and (line.startswith("fatal:") or line.startswith("error:")):
36
+ logger.error(line)
37
+
38
+
23
39
  def _handle_target_branch(
24
40
  target: Path, target_branch: str | None, git_executable: str, git_env: dict[str, str]
25
41
  ) -> None:
@@ -37,7 +53,7 @@ def _handle_target_branch(
37
53
  logger.info(f"Creating/checking out target branch: {target_branch}")
38
54
  try:
39
55
  # Check if branch already exists using git rev-parse
40
- result = subprocess.run(
56
+ result = subprocess.run( # nosec B603
41
57
  [git_executable, "rev-parse", "--verify", target_branch],
42
58
  cwd=target,
43
59
  capture_output=True,
@@ -48,23 +64,29 @@ def _handle_target_branch(
48
64
  if result.returncode == 0:
49
65
  # Branch exists, switch to it
50
66
  logger.info(f"Branch '{target_branch}' exists, checking out...")
51
- subprocess.run(
67
+ subprocess.run( # nosec B603
52
68
  [git_executable, "checkout", target_branch],
53
69
  cwd=target,
54
70
  check=True,
71
+ capture_output=True,
72
+ text=True,
55
73
  env=git_env,
56
74
  )
57
75
  else:
58
76
  # Branch doesn't exist, create it from current HEAD
59
77
  logger.info(f"Creating new branch '{target_branch}'...")
60
- subprocess.run(
78
+ subprocess.run( # nosec B603
61
79
  [git_executable, "checkout", "-b", target_branch],
62
80
  cwd=target,
63
81
  check=True,
82
+ capture_output=True,
83
+ text=True,
64
84
  env=git_env,
65
85
  )
66
86
  except subprocess.CalledProcessError as e:
67
- logger.error(f"Failed to create/checkout branch '{target_branch}': {e}")
87
+ logger.error(f"Failed to create/checkout branch '{target_branch}'")
88
+ _log_git_stderr_errors(e.stderr)
89
+ logger.error("Please ensure you have no uncommitted changes or conflicts")
68
90
  sys.exit(1)
69
91
 
70
92
 
@@ -91,6 +113,9 @@ def _validate_and_load_template(target: Path, branch: str) -> tuple[RhizaTemplat
91
113
 
92
114
  # Extract template configuration settings
93
115
  rhiza_repo = template.template_repository
116
+ if not rhiza_repo:
117
+ logger.error("template-repository is not configured in template.yml")
118
+ raise RuntimeError("template-repository is required") # noqa: TRY003
94
119
  rhiza_branch = template.template_branch or branch
95
120
  include_paths = template.include
96
121
  excluded_paths = template.exclude
@@ -99,7 +124,7 @@ def _validate_and_load_template(target: Path, branch: str) -> tuple[RhizaTemplat
99
124
  if not include_paths:
100
125
  logger.error("No include paths found in template.yml")
101
126
  logger.error("Add at least one path to the 'include' list in template.yml")
102
- raise RuntimeError("No include paths found in template.yml")
127
+ raise RuntimeError("No include paths found in template.yml") # noqa: TRY003
103
128
 
104
129
  # Log the paths we'll be including
105
130
  logger.info("Include paths:")
@@ -136,7 +161,7 @@ def _construct_git_url(rhiza_repo: str, rhiza_host: str) -> str:
136
161
  else:
137
162
  logger.error(f"Unsupported template-host: {rhiza_host}")
138
163
  logger.error("template-host must be 'github' or 'gitlab'")
139
- raise ValueError(f"Unsupported template-host: {rhiza_host}. Must be 'github' or 'gitlab'.")
164
+ raise ValueError(f"Unsupported template-host: {rhiza_host}. Must be 'github' or 'gitlab'.") # noqa: TRY003
140
165
  return git_url
141
166
 
142
167
 
@@ -161,7 +186,7 @@ def _clone_template_repository(
161
186
  # Clone the repository using sparse checkout
162
187
  try:
163
188
  logger.debug("Executing git clone with sparse checkout")
164
- subprocess.run(
189
+ subprocess.run( # nosec B603
165
190
  [
166
191
  git_executable,
167
192
  "clone",
@@ -181,16 +206,18 @@ def _clone_template_repository(
181
206
  )
182
207
  logger.debug("Git clone completed successfully")
183
208
  except subprocess.CalledProcessError as e:
184
- logger.error(f"Failed to clone repository: {e}")
185
- if e.stderr:
186
- logger.error(f"Git error: {e.stderr.strip()}")
187
- logger.error(f"Check that the repository exists and branch '{rhiza_branch}' is valid")
188
- raise
209
+ logger.error(f"Failed to clone repository from {git_url}")
210
+ _log_git_stderr_errors(e.stderr)
211
+ logger.error("Please check that:")
212
+ logger.error(" - The repository exists and is accessible")
213
+ logger.error(f" - Branch '{rhiza_branch}' exists in the repository")
214
+ logger.error(" - You have network access to the git hosting service")
215
+ sys.exit(1)
189
216
 
190
217
  # Initialize sparse checkout in cone mode
191
218
  try:
192
219
  logger.debug("Initializing sparse checkout")
193
- subprocess.run(
220
+ subprocess.run( # nosec B603
194
221
  [git_executable, "sparse-checkout", "init", "--cone"],
195
222
  cwd=tmp_dir,
196
223
  check=True,
@@ -200,15 +227,14 @@ def _clone_template_repository(
200
227
  )
201
228
  logger.debug("Sparse checkout initialized")
202
229
  except subprocess.CalledProcessError as e:
203
- logger.error(f"Failed to initialize sparse checkout: {e}")
204
- if e.stderr:
205
- logger.error(f"Git error: {e.stderr.strip()}")
206
- raise
230
+ logger.error("Failed to initialize sparse checkout")
231
+ _log_git_stderr_errors(e.stderr)
232
+ sys.exit(1)
207
233
 
208
234
  # Set sparse checkout paths
209
235
  try:
210
236
  logger.debug(f"Setting sparse checkout paths: {include_paths}")
211
- subprocess.run(
237
+ subprocess.run( # nosec B603
212
238
  [git_executable, "sparse-checkout", "set", "--skip-checks", *include_paths],
213
239
  cwd=tmp_dir,
214
240
  check=True,
@@ -218,10 +244,9 @@ def _clone_template_repository(
218
244
  )
219
245
  logger.debug("Sparse checkout paths configured")
220
246
  except subprocess.CalledProcessError as e:
221
- logger.error(f"Failed to set sparse checkout paths: {e}")
222
- if e.stderr:
223
- logger.error(f"Git error: {e.stderr.strip()}")
224
- raise
247
+ logger.error("Failed to configure sparse checkout paths")
248
+ _log_git_stderr_errors(e.stderr)
249
+ sys.exit(1)
225
250
 
226
251
 
227
252
  def _copy_files_to_target(
rhiza/commands/migrate.py CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  This module implements the `migrate` command. It helps transition projects to use
4
4
  the new `.rhiza/` folder structure for storing Rhiza state and configuration files,
5
- separate from `.github/rhiza/` which contains template configuration.
5
+ separate from `.github/` which contains GitHub-specific configurations.
6
6
  """
7
7
 
8
8
  import shutil