worktree-env 0.2.0__tar.gz → 0.3.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.
Files changed (36) hide show
  1. {worktree_env-0.2.0 → worktree_env-0.3.0}/.github/workflows/publish.yml +0 -6
  2. {worktree_env-0.2.0 → worktree_env-0.3.0}/CHANGELOG.md +7 -2
  3. {worktree_env-0.2.0 → worktree_env-0.3.0}/PKG-INFO +5 -3
  4. {worktree_env-0.2.0 → worktree_env-0.3.0}/README.md +4 -2
  5. {worktree_env-0.2.0 → worktree_env-0.3.0}/README.zh-CN.md +4 -2
  6. {worktree_env-0.2.0 → worktree_env-0.3.0}/examples/fullstack.yaml +10 -3
  7. {worktree_env-0.2.0 → worktree_env-0.3.0}/pyproject.toml +4 -4
  8. {worktree_env-0.2.0/src/git_worktree_env → worktree_env-0.3.0/src/worktree_env}/__init__.py +1 -1
  9. {worktree_env-0.2.0/src/git_worktree_env → worktree_env-0.3.0/src/worktree_env}/__main__.py +1 -1
  10. {worktree_env-0.2.0/src/git_worktree_env → worktree_env-0.3.0/src/worktree_env}/config.py +11 -3
  11. {worktree_env-0.2.0/src/git_worktree_env → worktree_env-0.3.0/src/worktree_env}/profiles.py +29 -0
  12. {worktree_env-0.2.0/src/git_worktree_env → worktree_env-0.3.0/src/worktree_env}/projector.py +22 -6
  13. {worktree_env-0.2.0 → worktree_env-0.3.0}/tests/conftest.py +1 -1
  14. {worktree_env-0.2.0 → worktree_env-0.3.0}/tests/test_cli.py +2 -2
  15. {worktree_env-0.2.0 → worktree_env-0.3.0}/tests/test_config.py +7 -3
  16. {worktree_env-0.2.0 → worktree_env-0.3.0}/tests/test_hooks.py +3 -3
  17. worktree_env-0.3.0/tests/test_profiles.py +67 -0
  18. {worktree_env-0.2.0 → worktree_env-0.3.0}/tests/test_projector.py +35 -6
  19. {worktree_env-0.2.0 → worktree_env-0.3.0}/tests/test_reconciler.py +5 -5
  20. {worktree_env-0.2.0 → worktree_env-0.3.0}/tests/test_registry.py +4 -4
  21. {worktree_env-0.2.0 → worktree_env-0.3.0}/uv.lock +1 -1
  22. worktree_env-0.2.0/migration/git-worktree-env/README.md +0 -43
  23. worktree_env-0.2.0/migration/git-worktree-env/pyproject.toml +0 -37
  24. worktree_env-0.2.0/migration/git-worktree-env/src/git_worktree_env_compat/__init__.py +0 -8
  25. worktree_env-0.2.0/tests/test_profiles.py +0 -34
  26. {worktree_env-0.2.0 → worktree_env-0.3.0}/.github/workflows/tests.yml +0 -0
  27. {worktree_env-0.2.0 → worktree_env-0.3.0}/.gitignore +0 -0
  28. {worktree_env-0.2.0 → worktree_env-0.3.0}/CONTRIBUTING.md +0 -0
  29. {worktree_env-0.2.0 → worktree_env-0.3.0}/LICENSE +0 -0
  30. {worktree_env-0.2.0 → worktree_env-0.3.0}/SECURITY.md +0 -0
  31. {worktree_env-0.2.0/src/git_worktree_env → worktree_env-0.3.0/src/worktree_env}/cli.py +0 -0
  32. {worktree_env-0.2.0/src/git_worktree_env → worktree_env-0.3.0/src/worktree_env}/hooks.py +0 -0
  33. {worktree_env-0.2.0/src/git_worktree_env → worktree_env-0.3.0/src/worktree_env}/paths.py +0 -0
  34. {worktree_env-0.2.0/src/git_worktree_env → worktree_env-0.3.0/src/worktree_env}/reconciler.py +0 -0
  35. {worktree_env-0.2.0/src/git_worktree_env → worktree_env-0.3.0/src/worktree_env}/registry.py +0 -0
  36. {worktree_env-0.2.0/src/git_worktree_env → worktree_env-0.3.0/src/worktree_env}/utils.py +0 -0
@@ -20,11 +20,5 @@ jobs:
20
20
  - uses: astral-sh/setup-uv@v6
21
21
  - name: Build worktree-env
22
22
  run: uv build
23
- - name: Build git-worktree-env transition package
24
- if: github.ref == 'refs/tags/v0.2.0'
25
- run: uv build migration/git-worktree-env --out-dir migration/git-worktree-env/dist
26
23
  - name: Publish worktree-env
27
24
  run: uv publish dist/* --trusted-publishing always
28
- - name: Publish git-worktree-env transition package
29
- if: github.ref == 'refs/tags/v0.2.0'
30
- run: uv publish migration/git-worktree-env/dist/* --trusted-publishing always
@@ -4,12 +4,17 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.3.0] - 2026-08-21
8
+
9
+ - Changed profile initializers to `command` and `args` string arrays, executed
10
+ directly without an implicit shell.
11
+ - Renamed the Python module from `git_worktree_env` to `worktree_env`.
12
+
7
13
  ## [0.2.0] - 2026-08-21
8
14
 
9
15
  - Renamed the PyPI package and GitHub repository from `git-worktree-env` to
10
16
  `worktree-env`.
11
- - Kept the `wte` command, `git_worktree_env` Python module, and existing
12
- `~/.config/wte/` configuration compatible.
17
+ - Kept the `wte` command and existing `~/.config/wte/` configuration compatible.
13
18
  - Added a final `git-worktree-env` transition package and runtime migration guidance.
14
19
 
15
20
  ## [0.1.4] - 2026-08-20
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: worktree-env
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Per-worktree ports, secrets, and local environment setup for Git
5
5
  Project-URL: Homepage, https://github.com/archcst/worktree-env
6
6
  Project-URL: Repository, https://github.com/archcst/worktree-env
@@ -286,11 +286,13 @@ environment to be initialized in the background:
286
286
 
287
287
  ```yaml
288
288
  init:
289
- - command: npm install
289
+ - command: [npm]
290
+ args: [install]
290
291
  cwd: frontend-dir # Use "." to run from the worktree root.
291
292
  skip_if: node_modules # Skip this command if the file or directory exists.
292
293
 
293
- - command: uv sync
294
+ - command: [uv]
295
+ args: [sync]
294
296
  cwd: backend-dir # Use "." to run from the worktree root.
295
297
  skip_if: .venv # Skip this command if the file or directory exists.
296
298
  ```
@@ -259,11 +259,13 @@ environment to be initialized in the background:
259
259
 
260
260
  ```yaml
261
261
  init:
262
- - command: npm install
262
+ - command: [npm]
263
+ args: [install]
263
264
  cwd: frontend-dir # Use "." to run from the worktree root.
264
265
  skip_if: node_modules # Skip this command if the file or directory exists.
265
266
 
266
- - command: uv sync
267
+ - command: [uv]
268
+ args: [sync]
267
269
  cwd: backend-dir # Use "." to run from the worktree root.
268
270
  skip_if: .venv # Skip this command if the file or directory exists.
269
271
  ```
@@ -210,11 +210,13 @@ wte monitor disable
210
210
 
211
211
  ```yaml
212
212
  init:
213
- - command: npm install
213
+ - command: [npm]
214
+ args: [install]
214
215
  cwd: frontend-dir # 若在 Worktree 根目录执行,则填写“.”
215
216
  skip_if: node_modules # 若该文件或目录存在,则跳过该命令
216
217
 
217
- - command: uv sync
218
+ - command: [uv]
219
+ args: [sync]
218
220
  cwd: backend-dir # 若在 Worktree 根目录执行,则填写“.”
219
221
  skip_if: .venv # 若该文件或目录存在,则跳过该命令
220
222
  ```
@@ -11,6 +11,8 @@ ports:
11
11
 
12
12
  # Sources stay outside Git. Targets are symlinks inside every linked worktree.
13
13
  secrets:
14
+ - source: $HOME/.config/example-app/frontend.env
15
+ target: apps/frontend/.env
14
16
  - source: $HOME/.config/example-app/backend.env
15
17
  target: apps/backend/.env
16
18
 
@@ -26,8 +28,13 @@ writes:
26
28
  CORS_ORIGIN=http://127.0.0.1:${frontend}
27
29
 
28
30
  # Initializers run in the background only from the post-checkout hook.
29
- # Each command is trusted local configuration and is executed by Bash.
31
+ # Each command is trusted local configuration and is executed directly.
30
32
  init:
31
- - command: npm install
32
- cwd: .
33
+ - command: [npm]
34
+ args: [install]
35
+ cwd: apps/frontend
33
36
  skip_if: node_modules
37
+ - command: [uv]
38
+ args: [sync]
39
+ cwd: apps/backend
40
+ skip_if: .venv
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "worktree-env"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Per-worktree ports, secrets, and local environment setup for Git"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -37,8 +37,8 @@ Repository = "https://github.com/archcst/worktree-env"
37
37
  Issues = "https://github.com/archcst/worktree-env/issues"
38
38
 
39
39
  [project.scripts]
40
- wte = "git_worktree_env.cli:main"
41
- worktree-env = "git_worktree_env.cli:main"
40
+ wte = "worktree_env.cli:main"
41
+ worktree-env = "worktree_env.cli:main"
42
42
 
43
43
  [dependency-groups]
44
44
  dev = [
@@ -46,7 +46,7 @@ dev = [
46
46
  ]
47
47
 
48
48
  [tool.hatch.build.targets.wheel]
49
- packages = ["src/git_worktree_env"]
49
+ packages = ["src/worktree_env"]
50
50
 
51
51
  [tool.pytest.ini_options]
52
52
  testpaths = ["tests"]
@@ -1,3 +1,3 @@
1
1
  """worktree-env public package."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.3.0"
@@ -1,4 +1,4 @@
1
- """Allow ``python -m git_worktree_env`` to behave like ``wte``."""
1
+ """Allow ``python -m worktree_env`` to behave like ``wte``."""
2
2
 
3
3
  from .cli import main
4
4
 
@@ -36,6 +36,8 @@ ports:
36
36
  # Optional local secret files. Sources stay outside Git; targets are replaced
37
37
  # with symlinks inside each matched worktree.
38
38
  secrets:
39
+ - source: $HOME/.config/example-app/frontend.env
40
+ target: apps/frontend/.env
39
41
  - source: $HOME/.config/example-app/backend.env
40
42
  target: apps/backend/.env
41
43
 
@@ -52,11 +54,17 @@ writes:
52
54
  CORS_ORIGIN=http://127.0.0.1:${frontend}
53
55
 
54
56
  # Optional post-checkout initializers. Commands are trusted local configuration
55
- # executed by Bash in the background. skip_if is resolved relative to cwd.
57
+ # executed directly in the background. args are appended to command, and skip_if
58
+ # is resolved relative to cwd.
56
59
  init:
57
- - command: npm install
58
- cwd: .
60
+ - command: [npm]
61
+ args: [install]
62
+ cwd: apps/frontend
59
63
  skip_if: node_modules
64
+ - command: [uv]
65
+ args: [sync]
66
+ cwd: apps/backend
67
+ skip_if: .venv
60
68
  """
61
69
 
62
70
 
@@ -210,6 +210,35 @@ def validate_profiles(paths: AppPaths) -> Tuple[List[str], List[str]]:
210
210
  elif Path(str(target)).is_absolute() or ".." in Path(str(target)).parts:
211
211
  errors.append(f"{label}: secrets[{index}].target must stay inside the worktree")
212
212
 
213
+ initializers = profile.get("init")
214
+ if initializers is None:
215
+ initializers = []
216
+ if not isinstance(initializers, list):
217
+ errors.append(f"{label}: init must be a list")
218
+ else:
219
+ for index, entry in enumerate(initializers):
220
+ if not isinstance(entry, dict):
221
+ errors.append(f"{label}: init[{index}] must be a mapping")
222
+ continue
223
+ command = entry.get("command")
224
+ if (
225
+ not isinstance(command, list)
226
+ or not command
227
+ or not all(isinstance(value, str) for value in command)
228
+ or not command[0].strip()
229
+ ):
230
+ errors.append(
231
+ f"{label}: init[{index}].command must be a non-empty string list"
232
+ )
233
+ args = entry.get("args", [])
234
+ if not isinstance(args, list) or not all(
235
+ isinstance(value, str) for value in args
236
+ ):
237
+ errors.append(f"{label}: init[{index}].args must be a string list")
238
+ cwd = entry.get("cwd")
239
+ if not isinstance(cwd, str) or not cwd.strip():
240
+ errors.append(f"{label}: init[{index}].cwd is required")
241
+
213
242
  if not profiles:
214
243
  warnings.append(f"no profiles found in {paths.profiles}")
215
244
  return errors, warnings
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import os
6
+ import shlex
6
7
  import subprocess
7
8
  import tempfile
8
9
  from dataclasses import dataclass
@@ -90,11 +91,26 @@ def _init_entries(profile: Profile) -> List[Dict[str, Any]]:
90
91
  def run_initializers(profile: Profile, root: Path) -> None:
91
92
  """Start missing dependency setup tasks without blocking Git checkout."""
92
93
  for index, item in enumerate(_init_entries(profile)):
93
- command = str(item.get("command") or "").strip()
94
- cwd_raw = str(item.get("cwd") or "").strip()
95
- if not command or not cwd_raw:
96
- log(f"initializer skipped; command and cwd are required: {item}")
94
+ command = item.get("command")
95
+ args = item.get("args", [])
96
+ cwd_raw = item.get("cwd")
97
+ if (
98
+ not isinstance(command, list)
99
+ or not command
100
+ or not all(isinstance(value, str) for value in command)
101
+ or not command[0].strip()
102
+ ):
103
+ log(f"initializer skipped; command must be a non-empty string list: {item}")
97
104
  continue
105
+ if not isinstance(args, list) or not all(
106
+ isinstance(value, str) for value in args
107
+ ):
108
+ log(f"initializer skipped; args must be a string list: {item}")
109
+ continue
110
+ if not isinstance(cwd_raw, str) or not cwd_raw.strip():
111
+ log(f"initializer skipped; cwd is required: {item}")
112
+ continue
113
+ argv = command + args
98
114
  cwd = expand_profile_path(cwd_raw, profile, root=root).resolve()
99
115
  if not cwd.is_dir():
100
116
  log(f"initializer skipped; directory does not exist: {cwd}")
@@ -108,7 +124,7 @@ def run_initializers(profile: Profile, root: Path) -> None:
108
124
  output = log_path.open("a")
109
125
  try:
110
126
  subprocess.Popen(
111
- ["bash", "-lc", command],
127
+ argv,
112
128
  cwd=str(cwd),
113
129
  stdout=output,
114
130
  stderr=subprocess.STDOUT,
@@ -116,7 +132,7 @@ def run_initializers(profile: Profile, root: Path) -> None:
116
132
  )
117
133
  finally:
118
134
  output.close()
119
- log(f"initializer started: {command} -> {log_path}")
135
+ log(f"initializer started: {shlex.join(argv)} -> {log_path}")
120
136
 
121
137
 
122
138
  def apply_worktree(
@@ -6,7 +6,7 @@ from typing import Sequence
6
6
 
7
7
  import pytest
8
8
 
9
- from git_worktree_env.paths import AppPaths
9
+ from worktree_env.paths import AppPaths
10
10
 
11
11
 
12
12
  def run(command: Sequence[str], cwd: Path) -> str:
@@ -2,7 +2,7 @@ import argparse
2
2
 
3
3
  import pytest
4
4
 
5
- from git_worktree_env.cli import _build_parser, _warn_if_legacy_distribution_installed
5
+ from worktree_env.cli import _build_parser, _warn_if_legacy_distribution_installed
6
6
 
7
7
 
8
8
  def test_public_cli_contains_only_the_six_user_commands():
@@ -37,7 +37,7 @@ def test_sync_does_not_accept_a_worktree_path():
37
37
 
38
38
  def test_legacy_distribution_prints_migration_instructions(monkeypatch, capsys):
39
39
  monkeypatch.setattr(
40
- "git_worktree_env.cli.metadata.distribution", lambda _name: object()
40
+ "worktree_env.cli.metadata.distribution", lambda _name: object()
41
41
  )
42
42
 
43
43
  _warn_if_legacy_distribution_installed()
@@ -1,12 +1,12 @@
1
1
  import pytest
2
2
 
3
- from git_worktree_env.config import (
3
+ from worktree_env.config import (
4
4
  initialize_config,
5
5
  initialize_profile_template,
6
6
  load_port_pool,
7
7
  )
8
- from git_worktree_env.paths import AppPaths
9
- from git_worktree_env.utils import WteError
8
+ from worktree_env.paths import AppPaths
9
+ from worktree_env.utils import WteError
10
10
 
11
11
 
12
12
  def test_init_generates_documented_port_range(tmp_path, monkeypatch):
@@ -33,6 +33,10 @@ def test_init_template_is_commented_and_not_an_active_yaml_profile(app_paths):
33
33
  assert "# Optional local secret files" in text
34
34
  assert "# Optional generated files" in text
35
35
  assert "# Optional post-checkout initializers" in text
36
+ assert "$HOME/.config/example-app/frontend.env" in text
37
+ assert "$HOME/.config/example-app/backend.env" in text
38
+ assert "cwd: apps/frontend" in text
39
+ assert "cwd: apps/backend" in text
36
40
  assert "wte monitor enable" in text
37
41
 
38
42
 
@@ -2,8 +2,8 @@ from pathlib import Path
2
2
 
3
3
  import pytest
4
4
 
5
- from git_worktree_env.hooks import _dispatcher_script, install_hooks
6
- from git_worktree_env.utils import WteError
5
+ from worktree_env.hooks import _dispatcher_script, install_hooks
6
+ from worktree_env.utils import WteError
7
7
 
8
8
 
9
9
  def test_dispatcher_runs_wte_only_for_post_checkout():
@@ -22,7 +22,7 @@ def test_install_refuses_to_replace_an_existing_global_hooks_path(
22
22
  existing = tmp_path / "other-hooks"
23
23
  existing.mkdir()
24
24
  monkeypatch.setattr(
25
- "git_worktree_env.hooks.current_hooks_path", lambda: str(existing)
25
+ "worktree_env.hooks.current_hooks_path", lambda: str(existing)
26
26
  )
27
27
 
28
28
  with pytest.raises(WteError, match="wte did not change it"):
@@ -0,0 +1,67 @@
1
+ from pathlib import Path
2
+
3
+ from worktree_env.profiles import find_profile, main_worktree_root, validate_profiles
4
+
5
+
6
+ def test_linked_worktree_matches_its_main_profile(app_paths, git_worktrees):
7
+ main, linked = git_worktrees
8
+ (app_paths.profiles / "example.yaml").write_text(
9
+ "name: example\n"
10
+ "match:\n"
11
+ f" main_worktree: {main}\n"
12
+ "ports:\n"
13
+ " - id: web\n"
14
+ )
15
+
16
+ profile = find_profile(app_paths, linked)
17
+
18
+ assert profile is not None
19
+ assert profile["name"] == "example"
20
+ assert main_worktree_root(linked) == main
21
+
22
+
23
+ def test_validation_rejects_duplicate_main_worktrees(app_paths, git_worktrees):
24
+ main, _ = git_worktrees
25
+ for name in ("one", "two"):
26
+ (app_paths.profiles / f"{name}.yaml").write_text(
27
+ f"name: {name}\n"
28
+ f"match:\n main_worktree: {main}\n"
29
+ "ports:\n - id: web\n"
30
+ )
31
+
32
+ errors, _warnings = validate_profiles(app_paths)
33
+
34
+ assert any("also configured" in error for error in errors)
35
+
36
+
37
+ def test_validation_accepts_initializer_command_and_args_lists(app_paths, tmp_path):
38
+ (app_paths.profiles / "example.yaml").write_text(
39
+ "name: example\n"
40
+ f"match:\n main_worktree: {tmp_path}\n"
41
+ "ports:\n - id: web\n"
42
+ "init:\n"
43
+ " - command: [npm]\n"
44
+ " args: [install]\n"
45
+ " cwd: .\n"
46
+ )
47
+
48
+ errors, _warnings = validate_profiles(app_paths)
49
+
50
+ assert not errors
51
+
52
+
53
+ def test_validation_rejects_initializer_string_command(app_paths, tmp_path):
54
+ (app_paths.profiles / "example.yaml").write_text(
55
+ "name: example\n"
56
+ f"match:\n main_worktree: {tmp_path}\n"
57
+ "ports:\n - id: web\n"
58
+ "init:\n"
59
+ " - command: npm install\n"
60
+ " cwd: .\n"
61
+ )
62
+
63
+ errors, _warnings = validate_profiles(app_paths)
64
+
65
+ assert any(
66
+ "init[0].command must be a non-empty string list" in error for error in errors
67
+ )
@@ -1,8 +1,8 @@
1
1
  import pytest
2
2
 
3
- from git_worktree_env.projector import apply_worktree
4
- from git_worktree_env.registry import load_registry
5
- from git_worktree_env.utils import WteError
3
+ from worktree_env.projector import apply_worktree, run_initializers
4
+ from worktree_env.registry import load_registry
5
+ from worktree_env.utils import WteError
6
6
 
7
7
 
8
8
  def test_projection_links_secrets_and_writes_ports(app_paths, git_worktrees, tmp_path, monkeypatch):
@@ -25,7 +25,7 @@ def test_projection_links_secrets_and_writes_ports(app_paths, git_worktrees, tmp
25
25
  generated_path = linked / "app/.env.development"
26
26
  generated_path.parent.mkdir(parents=True)
27
27
  generated_path.write_text("KEEP=old\n")
28
- monkeypatch.setattr("git_worktree_env.registry.port_is_free", lambda _port: True)
28
+ monkeypatch.setattr("worktree_env.registry.port_is_free", lambda _port: True)
29
29
 
30
30
  result = apply_worktree(app_paths, linked)
31
31
 
@@ -39,6 +39,35 @@ def test_projection_links_secrets_and_writes_ports(app_paths, git_worktrees, tmp
39
39
  assert load_registry(app_paths)[str(linked)]["profile"] == "example"
40
40
 
41
41
 
42
+ def test_initializers_execute_command_and_args_without_a_shell(
43
+ tmp_path, monkeypatch, capsys
44
+ ):
45
+ calls = []
46
+ profile = {
47
+ "name": "example",
48
+ "init": [
49
+ {
50
+ "command": ["tool", "subcommand"],
51
+ "args": ["literal;value", "two words"],
52
+ "cwd": ".",
53
+ }
54
+ ],
55
+ }
56
+ monkeypatch.setattr(
57
+ "worktree_env.projector.tempfile.gettempdir", lambda: str(tmp_path)
58
+ )
59
+ monkeypatch.setattr(
60
+ "worktree_env.projector.subprocess.Popen",
61
+ lambda argv, **kwargs: calls.append((argv, kwargs)),
62
+ )
63
+
64
+ run_initializers(profile, tmp_path)
65
+
66
+ assert calls[0][0] == ["tool", "subcommand", "literal;value", "two words"]
67
+ assert calls[0][1]["cwd"] == str(tmp_path.resolve())
68
+ assert "tool subcommand 'literal;value' 'two words'" in capsys.readouterr().err
69
+
70
+
42
71
  def test_failed_projection_does_not_commit_a_registry_entry(
43
72
  app_paths, git_worktrees, monkeypatch
44
73
  ):
@@ -49,12 +78,12 @@ def test_failed_projection_does_not_commit_a_registry_entry(
49
78
  "ports:\n - id: web\n"
50
79
  "writes:\n - path: generated.env\n body: PORT=${web}\n"
51
80
  )
52
- monkeypatch.setattr("git_worktree_env.registry.port_is_free", lambda _port: True)
81
+ monkeypatch.setattr("worktree_env.registry.port_is_free", lambda _port: True)
53
82
 
54
83
  def fail_writes(*_args):
55
84
  raise WteError("write failed")
56
85
 
57
- monkeypatch.setattr("git_worktree_env.projector.apply_writes", fail_writes)
86
+ monkeypatch.setattr("worktree_env.projector.apply_writes", fail_writes)
58
87
 
59
88
  with pytest.raises(WteError, match="write failed"):
60
89
  apply_worktree(app_paths, linked)
@@ -1,7 +1,7 @@
1
1
  import plistlib
2
2
  from pathlib import Path
3
3
 
4
- from git_worktree_env.reconciler import (
4
+ from worktree_env.reconciler import (
5
5
  LAUNCHD_LABEL,
6
6
  _listed_worktrees,
7
7
  _write_launch_agent,
@@ -9,7 +9,7 @@ from git_worktree_env.reconciler import (
9
9
  discover_watch_paths,
10
10
  reconcile_once,
11
11
  )
12
- from git_worktree_env.registry import load_registry
12
+ from worktree_env.registry import load_registry
13
13
 
14
14
 
15
15
  def test_git_porcelain_discovery_returns_main_and_linked_worktrees(git_worktrees):
@@ -41,7 +41,7 @@ def test_reconciler_projects_only_unregistered_worktrees(
41
41
  "ports:\n - id: web\n"
42
42
  "writes:\n - path: generated.env\n body: PORT=${web}\n"
43
43
  )
44
- monkeypatch.setattr("git_worktree_env.registry.port_is_free", lambda _port: True)
44
+ monkeypatch.setattr("worktree_env.registry.port_is_free", lambda _port: True)
45
45
 
46
46
  first = reconcile_once(app_paths)
47
47
  second = reconcile_once(app_paths)
@@ -61,7 +61,7 @@ def test_launch_agent_watches_common_git_metadata(
61
61
  agent = tmp_path / "wte.plist"
62
62
  watched = (tmp_path / "repo/.git/worktrees",)
63
63
  monkeypatch.setattr(
64
- "git_worktree_env.reconciler._launch_agent_path", lambda: agent
64
+ "worktree_env.reconciler._launch_agent_path", lambda: agent
65
65
  )
66
66
 
67
67
  _write_launch_agent(app_paths, Path("/opt/bin/wte"), watched)
@@ -77,7 +77,7 @@ def test_systemd_path_unit_uses_one_shot_reconciliation(tmp_path, monkeypatch):
77
77
  unit_dir = tmp_path / "systemd/user"
78
78
  watched = (tmp_path / "repo/.git/worktrees",)
79
79
  monkeypatch.setattr(
80
- "git_worktree_env.reconciler._systemd_user_dir", lambda: unit_dir
80
+ "worktree_env.reconciler._systemd_user_dir", lambda: unit_dir
81
81
  )
82
82
 
83
83
  service, path_unit = _write_systemd_units(Path("/opt/bin/wte"), watched)
@@ -3,14 +3,14 @@ from pathlib import Path
3
3
 
4
4
  import pytest
5
5
 
6
- from git_worktree_env.config import PortPool
7
- from git_worktree_env.registry import (
6
+ from worktree_env.config import PortPool
7
+ from worktree_env.registry import (
8
8
  allocate_ports,
9
9
  load_registry,
10
10
  prune_registry,
11
11
  save_registry,
12
12
  )
13
- from git_worktree_env.utils import WteError
13
+ from worktree_env.utils import WteError
14
14
 
15
15
 
16
16
  def test_allocation_is_sticky_and_contiguous(app_paths, tmp_path, monkeypatch):
@@ -21,7 +21,7 @@ def test_allocation_is_sticky_and_contiguous(app_paths, tmp_path, monkeypatch):
21
21
  "_file": str(tmp_path / "example.yaml"),
22
22
  "ports": [{"id": "frontend"}, {"id": "backend"}],
23
23
  }
24
- monkeypatch.setattr("git_worktree_env.registry.port_is_free", lambda _port: True)
24
+ monkeypatch.setattr("worktree_env.registry.port_is_free", lambda _port: True)
25
25
 
26
26
  start, ports = allocate_ports(profile, root, {}, PortPool(42000, 42010))
27
27
  registry = {
@@ -258,7 +258,7 @@ wheels = [
258
258
 
259
259
  [[package]]
260
260
  name = "worktree-env"
261
- version = "0.2.0"
261
+ version = "0.3.0"
262
262
  source = { editable = "." }
263
263
  dependencies = [
264
264
  { name = "pyyaml" },
@@ -1,43 +0,0 @@
1
- # `git-worktree-env` has moved
2
-
3
- > **This package has been renamed to [`worktree-env`](https://pypi.org/project/worktree-env/).**
4
-
5
- This is the final transition release of `git-worktree-env`. It installs the renamed
6
- package so existing users can continue to run `wte`, but future releases will only be
7
- published as `worktree-env`.
8
-
9
- ## Migrate an existing installation
10
-
11
- For an installation managed by `uv`:
12
-
13
- ```bash
14
- uv tool uninstall git-worktree-env
15
- uv tool install worktree-env
16
- wte init
17
- # If you previously enabled the optional Monitor:
18
- wte monitor enable
19
- ```
20
-
21
- For an installation managed by `pipx`:
22
-
23
- ```bash
24
- pipx uninstall git-worktree-env
25
- pipx install worktree-env
26
- wte init
27
- # If you previously enabled the optional Monitor:
28
- wte monitor enable
29
- ```
30
-
31
- For an installation managed by `pip`:
32
-
33
- ```bash
34
- python -m pip uninstall -y git-worktree-env
35
- python -m pip install --upgrade --force-reinstall worktree-env
36
- wte init
37
- # If you previously enabled the optional Monitor:
38
- wte monitor enable
39
- ```
40
-
41
- Running `wte init` refreshes the Git hook's absolute executable path. The `wte`
42
- command, the `git_worktree_env` Python import, and all existing configuration under
43
- `~/.config/wte/` remain compatible.
@@ -1,37 +0,0 @@
1
- [build-system]
2
- requires = ["hatchling>=1.26"]
3
- build-backend = "hatchling.build"
4
-
5
- [project]
6
- name = "git-worktree-env"
7
- version = "0.2.0"
8
- description = "Transition package for the renamed worktree-env project"
9
- readme = "README.md"
10
- requires-python = ">=3.9"
11
- license = "MIT"
12
- authors = [
13
- { name = "Shitong Chen" },
14
- ]
15
- classifiers = [
16
- "Development Status :: 7 - Inactive",
17
- "Environment :: Console",
18
- "License :: OSI Approved :: MIT License",
19
- "Operating System :: MacOS",
20
- "Operating System :: POSIX :: Linux",
21
- "Programming Language :: Python :: 3",
22
- ]
23
- dependencies = [
24
- "worktree-env>=0.2.0",
25
- ]
26
-
27
- [project.urls]
28
- Homepage = "https://github.com/archcst/worktree-env"
29
- Repository = "https://github.com/archcst/worktree-env"
30
- Issues = "https://github.com/archcst/worktree-env/issues"
31
-
32
- [project.scripts]
33
- wte = "git_worktree_env_compat:main"
34
- git-worktree-env = "git_worktree_env_compat:main"
35
-
36
- [tool.hatch.build.targets.wheel]
37
- packages = ["src/git_worktree_env_compat"]
@@ -1,8 +0,0 @@
1
- """Console-script bridge from git-worktree-env to worktree-env."""
2
-
3
-
4
- def main() -> int:
5
- """Delegate to the CLI supplied by the renamed distribution."""
6
- from git_worktree_env.cli import main as worktree_env_main
7
-
8
- return worktree_env_main()
@@ -1,34 +0,0 @@
1
- from pathlib import Path
2
-
3
- from git_worktree_env.profiles import find_profile, main_worktree_root, validate_profiles
4
-
5
-
6
- def test_linked_worktree_matches_its_main_profile(app_paths, git_worktrees):
7
- main, linked = git_worktrees
8
- (app_paths.profiles / "example.yaml").write_text(
9
- "name: example\n"
10
- "match:\n"
11
- f" main_worktree: {main}\n"
12
- "ports:\n"
13
- " - id: web\n"
14
- )
15
-
16
- profile = find_profile(app_paths, linked)
17
-
18
- assert profile is not None
19
- assert profile["name"] == "example"
20
- assert main_worktree_root(linked) == main
21
-
22
-
23
- def test_validation_rejects_duplicate_main_worktrees(app_paths, git_worktrees):
24
- main, _ = git_worktrees
25
- for name in ("one", "two"):
26
- (app_paths.profiles / f"{name}.yaml").write_text(
27
- f"name: {name}\n"
28
- f"match:\n main_worktree: {main}\n"
29
- "ports:\n - id: web\n"
30
- )
31
-
32
- errors, _warnings = validate_profiles(app_paths)
33
-
34
- assert any("also configured" in error for error in errors)
File without changes
File without changes
File without changes