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.
- {worktree_env-0.2.0 → worktree_env-0.3.0}/.github/workflows/publish.yml +0 -6
- {worktree_env-0.2.0 → worktree_env-0.3.0}/CHANGELOG.md +7 -2
- {worktree_env-0.2.0 → worktree_env-0.3.0}/PKG-INFO +5 -3
- {worktree_env-0.2.0 → worktree_env-0.3.0}/README.md +4 -2
- {worktree_env-0.2.0 → worktree_env-0.3.0}/README.zh-CN.md +4 -2
- {worktree_env-0.2.0 → worktree_env-0.3.0}/examples/fullstack.yaml +10 -3
- {worktree_env-0.2.0 → worktree_env-0.3.0}/pyproject.toml +4 -4
- {worktree_env-0.2.0/src/git_worktree_env → worktree_env-0.3.0/src/worktree_env}/__init__.py +1 -1
- {worktree_env-0.2.0/src/git_worktree_env → worktree_env-0.3.0/src/worktree_env}/__main__.py +1 -1
- {worktree_env-0.2.0/src/git_worktree_env → worktree_env-0.3.0/src/worktree_env}/config.py +11 -3
- {worktree_env-0.2.0/src/git_worktree_env → worktree_env-0.3.0/src/worktree_env}/profiles.py +29 -0
- {worktree_env-0.2.0/src/git_worktree_env → worktree_env-0.3.0/src/worktree_env}/projector.py +22 -6
- {worktree_env-0.2.0 → worktree_env-0.3.0}/tests/conftest.py +1 -1
- {worktree_env-0.2.0 → worktree_env-0.3.0}/tests/test_cli.py +2 -2
- {worktree_env-0.2.0 → worktree_env-0.3.0}/tests/test_config.py +7 -3
- {worktree_env-0.2.0 → worktree_env-0.3.0}/tests/test_hooks.py +3 -3
- worktree_env-0.3.0/tests/test_profiles.py +67 -0
- {worktree_env-0.2.0 → worktree_env-0.3.0}/tests/test_projector.py +35 -6
- {worktree_env-0.2.0 → worktree_env-0.3.0}/tests/test_reconciler.py +5 -5
- {worktree_env-0.2.0 → worktree_env-0.3.0}/tests/test_registry.py +4 -4
- {worktree_env-0.2.0 → worktree_env-0.3.0}/uv.lock +1 -1
- worktree_env-0.2.0/migration/git-worktree-env/README.md +0 -43
- worktree_env-0.2.0/migration/git-worktree-env/pyproject.toml +0 -37
- worktree_env-0.2.0/migration/git-worktree-env/src/git_worktree_env_compat/__init__.py +0 -8
- worktree_env-0.2.0/tests/test_profiles.py +0 -34
- {worktree_env-0.2.0 → worktree_env-0.3.0}/.github/workflows/tests.yml +0 -0
- {worktree_env-0.2.0 → worktree_env-0.3.0}/.gitignore +0 -0
- {worktree_env-0.2.0 → worktree_env-0.3.0}/CONTRIBUTING.md +0 -0
- {worktree_env-0.2.0 → worktree_env-0.3.0}/LICENSE +0 -0
- {worktree_env-0.2.0 → worktree_env-0.3.0}/SECURITY.md +0 -0
- {worktree_env-0.2.0/src/git_worktree_env → worktree_env-0.3.0/src/worktree_env}/cli.py +0 -0
- {worktree_env-0.2.0/src/git_worktree_env → worktree_env-0.3.0/src/worktree_env}/hooks.py +0 -0
- {worktree_env-0.2.0/src/git_worktree_env → worktree_env-0.3.0/src/worktree_env}/paths.py +0 -0
- {worktree_env-0.2.0/src/git_worktree_env → worktree_env-0.3.0/src/worktree_env}/reconciler.py +0 -0
- {worktree_env-0.2.0/src/git_worktree_env → worktree_env-0.3.0/src/worktree_env}/registry.py +0 -0
- {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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
213
|
+
- command: [npm]
|
|
214
|
+
args: [install]
|
|
214
215
|
cwd: frontend-dir # 若在 Worktree 根目录执行,则填写“.”
|
|
215
216
|
skip_if: node_modules # 若该文件或目录存在,则跳过该命令
|
|
216
217
|
|
|
217
|
-
- command: uv
|
|
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
|
|
31
|
+
# Each command is trusted local configuration and is executed directly.
|
|
30
32
|
init:
|
|
31
|
-
- command: npm
|
|
32
|
-
|
|
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.
|
|
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 = "
|
|
41
|
-
worktree-env = "
|
|
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/
|
|
49
|
+
packages = ["src/worktree_env"]
|
|
50
50
|
|
|
51
51
|
[tool.pytest.ini_options]
|
|
52
52
|
testpaths = ["tests"]
|
|
@@ -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
|
|
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
|
|
58
|
-
|
|
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
|
{worktree_env-0.2.0/src/git_worktree_env → worktree_env-0.3.0/src/worktree_env}/projector.py
RENAMED
|
@@ -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 =
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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: {
|
|
135
|
+
log(f"initializer started: {shlex.join(argv)} -> {log_path}")
|
|
120
136
|
|
|
121
137
|
|
|
122
138
|
def apply_worktree(
|
|
@@ -2,7 +2,7 @@ import argparse
|
|
|
2
2
|
|
|
3
3
|
import pytest
|
|
4
4
|
|
|
5
|
-
from
|
|
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
|
-
"
|
|
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
|
|
3
|
+
from worktree_env.config import (
|
|
4
4
|
initialize_config,
|
|
5
5
|
initialize_profile_template,
|
|
6
6
|
load_port_pool,
|
|
7
7
|
)
|
|
8
|
-
from
|
|
9
|
-
from
|
|
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
|
|
6
|
-
from
|
|
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
|
-
"
|
|
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
|
|
4
|
-
from
|
|
5
|
-
from
|
|
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("
|
|
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("
|
|
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("
|
|
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
|
|
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
|
|
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("
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
|
7
|
-
from
|
|
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
|
|
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("
|
|
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 = {
|
|
@@ -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,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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{worktree_env-0.2.0/src/git_worktree_env → worktree_env-0.3.0/src/worktree_env}/reconciler.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|