workforest 0.2.1__tar.gz → 0.2.3__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 (47) hide show
  1. {workforest-0.2.1 → workforest-0.2.3}/.github/workflows/publish_aur.yml +4 -25
  2. workforest-0.2.3/.github/workflows/publish_homebrew.yml +76 -0
  3. {workforest-0.2.1 → workforest-0.2.3}/.gitignore +4 -1
  4. {workforest-0.2.1 → workforest-0.2.3}/PKG-INFO +17 -8
  5. {workforest-0.2.1 → workforest-0.2.3}/README.md +15 -7
  6. workforest-0.2.1/packaging/AUR/PKGBUILD → workforest-0.2.3/packaging/AUR/PKGBUILD.template +6 -2
  7. workforest-0.2.3/packaging/homebrew/workforest.rb.template +52 -0
  8. {workforest-0.2.1 → workforest-0.2.3}/pyproject.toml +1 -0
  9. {workforest-0.2.1 → workforest-0.2.3}/src/workforest/__init__.py +1 -1
  10. {workforest-0.2.1 → workforest-0.2.3}/src/workforest/launch.py +44 -2
  11. {workforest-0.2.1 → workforest-0.2.3}/src/workforest/tui.py +1 -1
  12. {workforest-0.2.1 → workforest-0.2.3}/tests/conftest.py +2 -1
  13. {workforest-0.2.1 → workforest-0.2.3}/tests/test_launch.py +66 -0
  14. workforest-0.2.1/packaging/AUR/.SRCINFO +0 -19
  15. {workforest-0.2.1 → workforest-0.2.3}/.github/workflows/ci.yml +0 -0
  16. {workforest-0.2.1 → workforest-0.2.3}/.github/workflows/publish_pypi.yml +0 -0
  17. {workforest-0.2.1 → workforest-0.2.3}/.github/workflows/release.yml +0 -0
  18. {workforest-0.2.1 → workforest-0.2.3}/LICENSE +0 -0
  19. {workforest-0.2.1 → workforest-0.2.3}/Makefile +0 -0
  20. {workforest-0.2.1 → workforest-0.2.3}/completions/_workforest +0 -0
  21. {workforest-0.2.1 → workforest-0.2.3}/src/workforest/__main__.py +0 -0
  22. {workforest-0.2.1 → workforest-0.2.3}/src/workforest/cli.py +0 -0
  23. {workforest-0.2.1 → workforest-0.2.3}/src/workforest/commands.py +0 -0
  24. {workforest-0.2.1 → workforest-0.2.3}/src/workforest/completions.py +0 -0
  25. {workforest-0.2.1 → workforest-0.2.3}/src/workforest/config.py +0 -0
  26. {workforest-0.2.1 → workforest-0.2.3}/src/workforest/errors.py +0 -0
  27. {workforest-0.2.1 → workforest-0.2.3}/src/workforest/examples/config.yaml +0 -0
  28. {workforest-0.2.1 → workforest-0.2.3}/src/workforest/gitutil.py +0 -0
  29. {workforest-0.2.1 → workforest-0.2.3}/src/workforest/hooks.py +0 -0
  30. {workforest-0.2.1 → workforest-0.2.3}/src/workforest/integrations/__init__.py +0 -0
  31. {workforest-0.2.1 → workforest-0.2.3}/src/workforest/integrations/claude.py +0 -0
  32. {workforest-0.2.1 → workforest-0.2.3}/src/workforest/output.py +0 -0
  33. {workforest-0.2.1 → workforest-0.2.3}/src/workforest/shell/completion.bash +0 -0
  34. {workforest-0.2.1 → workforest-0.2.3}/src/workforest/shell/completion.zsh +0 -0
  35. {workforest-0.2.1 → workforest-0.2.3}/src/workforest/shell/workforest.sh +0 -0
  36. {workforest-0.2.1 → workforest-0.2.3}/src/workforest/shellinit.py +0 -0
  37. {workforest-0.2.1 → workforest-0.2.3}/tests/__init__.py +0 -0
  38. {workforest-0.2.1 → workforest-0.2.3}/tests/test_claude.py +0 -0
  39. {workforest-0.2.1 → workforest-0.2.3}/tests/test_cli.py +0 -0
  40. {workforest-0.2.1 → workforest-0.2.3}/tests/test_commands.py +0 -0
  41. {workforest-0.2.1 → workforest-0.2.3}/tests/test_config.py +0 -0
  42. {workforest-0.2.1 → workforest-0.2.3}/tests/test_gitutil.py +0 -0
  43. {workforest-0.2.1 → workforest-0.2.3}/tests/test_harness.py +0 -0
  44. {workforest-0.2.1 → workforest-0.2.3}/tests/test_hooks.py +0 -0
  45. {workforest-0.2.1 → workforest-0.2.3}/tests/test_shell.py +0 -0
  46. {workforest-0.2.1 → workforest-0.2.3}/tests/test_tui.py +0 -0
  47. {workforest-0.2.1 → workforest-0.2.3}/uv.lock +0 -0
@@ -12,7 +12,7 @@ jobs:
12
12
  name: aur
13
13
  url: https://aur.archlinux.org/packages/workforest
14
14
  permissions:
15
- contents: write # commit the generated PKGBUILD back to main
15
+ contents: read
16
16
  steps:
17
17
  - name: Install system dependencies
18
18
  run: >
@@ -21,15 +21,15 @@ jobs:
21
21
 
22
22
  - uses: actions/checkout@v4
23
23
 
24
- - name: Update PKGBUILD to released version
24
+ - name: Render PKGBUILD from template
25
25
  run: |
26
26
  version="${GITHUB_REF_NAME#v}"
27
27
  cd packaging/AUR
28
- sed -i "s/^pkgver=.*/pkgver=$version/" PKGBUILD
29
- sed -i "s/^pkgrel=.*/pkgrel=1/" PKGBUILD
28
+ sed "s/@VERSION@/$version/g" PKGBUILD.template > PKGBUILD
30
29
  # makepkg refuses to run as root
31
30
  useradd -m builder
32
31
  chown -R builder .
32
+ # updpkgsums replaces the @SHA256@ placeholder with the real checksum
33
33
  runuser -u builder -- updpkgsums
34
34
  runuser -u builder -- sh -c 'makepkg --printsrcinfo > .SRCINFO'
35
35
 
@@ -70,24 +70,3 @@ jobs:
70
70
  git commit -m "Update to $version"
71
71
  git push origin HEAD:master
72
72
  fi
73
-
74
- - name: Commit generated PKGBUILD back to main
75
- run: |
76
- version="${GITHUB_REF_NAME#v}"
77
- cp packaging/AUR/PKGBUILD packaging/AUR/.SRCINFO /tmp/
78
- # The workspace is created by the host runner under a different UID
79
- # than the container's root, so git rejects it without this.
80
- git config --global --add safe.directory "$GITHUB_WORKSPACE"
81
- # The workflow runs detached at the release tag; move to main to commit.
82
- # -f discards the in-place edits we just copied to /tmp; build
83
- # artifacts in packaging/AUR are untracked and never git-added.
84
- git fetch origin main
85
- git checkout -f -B main origin/main
86
- cp /tmp/PKGBUILD /tmp/.SRCINFO packaging/AUR/
87
- git add packaging/AUR/PKGBUILD packaging/AUR/.SRCINFO
88
- if git diff --cached --quiet; then
89
- echo "Repo copy already up to date."
90
- else
91
- git commit -m "Update PKGBUILD to $version"
92
- git push origin main
93
- fi
@@ -0,0 +1,76 @@
1
+ name: Publish to Homebrew tap
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ workflow_dispatch:
7
+ inputs:
8
+ version:
9
+ description: "Release version to publish (e.g. 0.2.1)"
10
+ required: true
11
+
12
+ jobs:
13
+ publish:
14
+ runs-on: macos-latest
15
+ env:
16
+ RELEASE_VERSION: ${{ github.event_name == 'workflow_dispatch' && inputs.version || github.ref_name }}
17
+ environment:
18
+ name: homebrew-tap
19
+ url: https://github.com/ArkadyBuryakov/homebrew-tap
20
+ permissions:
21
+ contents: read
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+
25
+ - name: Render formula from template
26
+ run: |
27
+ version="${RELEASE_VERSION#v}"
28
+ url="https://github.com/ArkadyBuryakov/workforest/archive/v$version.tar.gz"
29
+ curl -fsSL "$url" -o /tmp/workforest.tar.gz
30
+ sha256=$(shasum -a 256 /tmp/workforest.tar.gz | cut -d' ' -f1)
31
+ cd packaging/homebrew
32
+ sed -e "s/@VERSION@/$version/g" -e "s/@SHA256@/$sha256/g" \
33
+ workforest.rb.template > workforest.rb
34
+
35
+ - name: Build, test, and smoke test
36
+ run: |
37
+ # Homebrew >= 4.6.4 refuses to install from a bare file path —
38
+ # formulas must live in a tap, so stage ours in a throwaway one.
39
+ brew tap-new --no-git arkadyburyakov/test
40
+ cp packaging/homebrew/workforest.rb "$(brew --repository arkadyburyakov/test)/Formula/"
41
+ brew install --build-from-source arkadyburyakov/test/workforest
42
+ brew test arkadyburyakov/test/workforest
43
+ workforest --version
44
+ wf --version
45
+ workforest shell-init bash | bash -n
46
+ workforest shell-init zsh | head -1 | grep -q Workforest
47
+
48
+ - name: Push to tap
49
+ env:
50
+ TAP_SSH_PRIVATE_KEY: ${{ secrets.TAP_SSH_PRIVATE_KEY }}
51
+ run: |
52
+ version="${RELEASE_VERSION#v}"
53
+ # Host keys are pinned rather than keyscanned (same policy as the
54
+ # AUR workflow); these are GitHub's published SSH fingerprints.
55
+ mkdir -p /tmp/ssh
56
+ printf '%s\n' "$TAP_SSH_PRIVATE_KEY" > /tmp/ssh/tap
57
+ chmod 600 /tmp/ssh/tap
58
+ cat > /tmp/ssh/known_hosts <<'EOF'
59
+ github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl
60
+ github.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=
61
+ EOF
62
+ export GIT_SSH_COMMAND="ssh -i /tmp/ssh/tap -o UserKnownHostsFile=/tmp/ssh/known_hosts -o IdentitiesOnly=yes"
63
+ git config --global user.name "Arkady Buryakov"
64
+ git config --global user.email "arkady@buryakov.pro"
65
+ git clone git@github.com:ArkadyBuryakov/homebrew-tap.git /tmp/tap
66
+ cd /tmp/tap
67
+ git checkout main 2>/dev/null || git checkout -b main
68
+ mkdir -p Formula
69
+ cp "$GITHUB_WORKSPACE/packaging/homebrew/workforest.rb" Formula/workforest.rb
70
+ git add Formula/workforest.rb
71
+ if git diff --cached --quiet; then
72
+ echo "No changes to publish."
73
+ else
74
+ git commit -m "workforest $version"
75
+ git push origin main
76
+ fi
@@ -13,9 +13,12 @@ build/
13
13
  .coverage
14
14
  htmlcov/
15
15
 
16
- # Packaging artifacts
16
+ # Packaging artifacts (rendered from *.template at release time)
17
17
  *.pkg.tar.zst
18
18
  pkg/
19
+ packaging/AUR/PKGBUILD
20
+ packaging/AUR/.SRCINFO
21
+ packaging/homebrew/workforest.rb
19
22
 
20
23
  # Tooling config
21
24
  .workforest.yaml
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: workforest
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: Git worktree forest management: create, open, and clean up per-branch worktrees with project-defined setup hooks
5
5
  Project-URL: Homepage, https://github.com/ArkadyBuryakov/workforest
6
6
  Author-email: Arkady Buryakov <arkady@buryakov.pro>
@@ -10,6 +10,7 @@ Keywords: cli,developer-tools,git,worktree
10
10
  Classifier: Development Status :: 4 - Beta
11
11
  Classifier: Environment :: Console
12
12
  Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: MacOS
13
14
  Classifier: Operating System :: POSIX :: Linux
14
15
  Classifier: Programming Language :: Python :: 3.14
15
16
  Classifier: Topic :: Software Development :: Version Control :: Git
@@ -48,6 +49,9 @@ opened, and cleaned up with one command.
48
49
  # Arch Linux
49
50
  yay -S workforest # AUR
50
51
 
52
+ # macOS (or Linux with Homebrew)
53
+ brew install arkadyburyakov/tap/workforest
54
+
51
55
  # anywhere else
52
56
  uv tool install workforest # or: pipx install workforest
53
57
  ```
@@ -64,7 +68,8 @@ shell's directory — a plain binary cannot) and registers completions. Without
64
68
  it everything still works, but "open in current shell" prints the `cd`
65
69
  command instead of performing it.
66
70
 
67
- Requirements: Linux, git ≥ 2.36, Python ≥ 3.14. Optional: `fzf` for the TUI.
71
+ Requirements: Linux or macOS, git ≥ 2.36, Python ≥ 3.14 (the AUR and
72
+ Homebrew packages bring their own). Optional: `fzf` for the TUI.
68
73
 
69
74
  ## Quick start
70
75
 
@@ -131,7 +136,9 @@ word-splits into multiple arguments, while `{x}` — `{worktree}`, `{target}`,
131
136
  Openers run with the worktree root as working directory; in
132
137
  `window_command` the resolved opener command is additionally available as
133
138
  `$WF_COMMAND` (spliced into argv words) or `{command}` (one argument, for
134
- `$SHELL -c` wrappers).
139
+ `$SHELL -c` wrappers). Spawned windows shed activation state inherited from
140
+ the invoking shell (Python venv, conda, nvm, rvm) so the new session starts
141
+ clean instead of carrying an environment it cannot deactivate.
135
142
 
136
143
  Fully commented reference configs:
137
144
  [`config.yaml`](src/workforest/examples/config.yaml) (user/system) and
@@ -195,11 +202,13 @@ make install # install this checkout as a uv tool (~/.local/bin/workforest)
195
202
  make uninstall # remove it again
196
203
  ```
197
204
 
198
- Packaging recipes live under
199
- `packaging/` (one directory per package manager; currently `packaging/AUR/`).
200
- Release: bump `__version__`, tag, update `sha256sums` in
201
- `packaging/AUR/PKGBUILD`, regenerate `.SRCINFO`
202
- (`makepkg --printsrcinfo > .SRCINFO` inside `packaging/AUR/`), push to AUR.
205
+ Packaging templates live under `packaging/` (one directory per package
206
+ manager: `packaging/AUR/`, `packaging/homebrew/`); the `@VERSION@` and
207
+ `@SHA256@` placeholders are filled in at release time.
208
+ Release: bump `__version__` and push to main — CI tags the release,
209
+ renders the templates, and publishes to PyPI, the AUR, and the
210
+ [Homebrew tap](https://github.com/ArkadyBuryakov/homebrew-tap). The
211
+ published AUR package and tap are the only places rendered recipes exist.
203
212
 
204
213
  ## License
205
214
 
@@ -29,6 +29,9 @@ opened, and cleaned up with one command.
29
29
  # Arch Linux
30
30
  yay -S workforest # AUR
31
31
 
32
+ # macOS (or Linux with Homebrew)
33
+ brew install arkadyburyakov/tap/workforest
34
+
32
35
  # anywhere else
33
36
  uv tool install workforest # or: pipx install workforest
34
37
  ```
@@ -45,7 +48,8 @@ shell's directory — a plain binary cannot) and registers completions. Without
45
48
  it everything still works, but "open in current shell" prints the `cd`
46
49
  command instead of performing it.
47
50
 
48
- Requirements: Linux, git ≥ 2.36, Python ≥ 3.14. Optional: `fzf` for the TUI.
51
+ Requirements: Linux or macOS, git ≥ 2.36, Python ≥ 3.14 (the AUR and
52
+ Homebrew packages bring their own). Optional: `fzf` for the TUI.
49
53
 
50
54
  ## Quick start
51
55
 
@@ -112,7 +116,9 @@ word-splits into multiple arguments, while `{x}` — `{worktree}`, `{target}`,
112
116
  Openers run with the worktree root as working directory; in
113
117
  `window_command` the resolved opener command is additionally available as
114
118
  `$WF_COMMAND` (spliced into argv words) or `{command}` (one argument, for
115
- `$SHELL -c` wrappers).
119
+ `$SHELL -c` wrappers). Spawned windows shed activation state inherited from
120
+ the invoking shell (Python venv, conda, nvm, rvm) so the new session starts
121
+ clean instead of carrying an environment it cannot deactivate.
116
122
 
117
123
  Fully commented reference configs:
118
124
  [`config.yaml`](src/workforest/examples/config.yaml) (user/system) and
@@ -176,11 +182,13 @@ make install # install this checkout as a uv tool (~/.local/bin/workforest)
176
182
  make uninstall # remove it again
177
183
  ```
178
184
 
179
- Packaging recipes live under
180
- `packaging/` (one directory per package manager; currently `packaging/AUR/`).
181
- Release: bump `__version__`, tag, update `sha256sums` in
182
- `packaging/AUR/PKGBUILD`, regenerate `.SRCINFO`
183
- (`makepkg --printsrcinfo > .SRCINFO` inside `packaging/AUR/`), push to AUR.
185
+ Packaging templates live under `packaging/` (one directory per package
186
+ manager: `packaging/AUR/`, `packaging/homebrew/`); the `@VERSION@` and
187
+ `@SHA256@` placeholders are filled in at release time.
188
+ Release: bump `__version__` and push to main — CI tags the release,
189
+ renders the templates, and publishes to PyPI, the AUR, and the
190
+ [Homebrew tap](https://github.com/ArkadyBuryakov/homebrew-tap). The
191
+ published AUR package and tap are the only places rendered recipes exist.
184
192
 
185
193
  ## License
186
194
 
@@ -1,6 +1,10 @@
1
1
  # Maintainer: Arkady Buryakov <arkady@buryakov.pro>
2
+ #
3
+ # Template — not a buildable PKGBUILD. On release the publish_aur workflow
4
+ # substitutes @VERSION@, fills sha256sums via updpkgsums, generates .SRCINFO,
5
+ # and pushes the rendered files to the AUR; nothing is committed back here.
2
6
  pkgname=workforest
3
- pkgver=0.2.0
7
+ pkgver=@VERSION@
4
8
  pkgrel=1
5
9
  pkgdesc="Git worktree forest management: per-branch worktrees with project-defined setup hooks"
6
10
  arch=(any)
@@ -10,7 +14,7 @@ depends=(python python-yaml git)
10
14
  makedepends=(python-build python-installer python-wheel python-hatchling)
11
15
  optdepends=('fzf: interactive TUI (workforest tui)')
12
16
  source=("$pkgname-$pkgver.tar.gz::$url/archive/v$pkgver.tar.gz")
13
- sha256sums=('dc1ba2518e2612714e023f7fc088aeaa37f9e0d5bffbcbc07c2dd15f109bb476')
17
+ sha256sums=('@SHA256@')
14
18
 
15
19
  build() {
16
20
  cd "$pkgname-$pkgver"
@@ -0,0 +1,52 @@
1
+ # Template — not an installable formula. On release the publish_homebrew
2
+ # workflow substitutes @VERSION@ and @SHA256@ and pushes the rendered
3
+ # Formula/workforest.rb to the ArkadyBuryakov/homebrew-tap repo; nothing is
4
+ # committed back here. This copy is the source of truth for everything else
5
+ # (deps, completions, caveats, test).
6
+ class Workforest < Formula
7
+ include Language::Python::Virtualenv
8
+
9
+ desc "Git worktree forest management with per-branch setup hooks"
10
+ homepage "https://github.com/ArkadyBuryakov/workforest"
11
+ url "https://github.com/ArkadyBuryakov/workforest/archive/v@VERSION@.tar.gz"
12
+ sha256 "@SHA256@"
13
+ license "MIT"
14
+ head "https://github.com/ArkadyBuryakov/workforest.git", branch: "main"
15
+
16
+ depends_on "fzf"
17
+ depends_on "python@3.14"
18
+
19
+ resource "pyyaml" do
20
+ url "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz"
21
+ sha256 "d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"
22
+ end
23
+
24
+ def install
25
+ virtualenv_install_with_resources
26
+
27
+ # Completions for the plain binary; the wf shell function itself comes
28
+ # from `workforest shell-init` (see caveats).
29
+ bash_completion.install "src/workforest/shell/completion.bash" => "workforest"
30
+ zsh_completion.install "completions/_workforest"
31
+
32
+ # Reference configs, mirroring the Arch package's /usr/share/doc layout.
33
+ (doc/"examples").install "src/workforest/examples/config.yaml"
34
+ (doc/"examples").install "src/workforest/examples/.workforest.yaml" => "workforest.project.yaml"
35
+ end
36
+
37
+ def caveats
38
+ <<~EOS
39
+ To upgrade `wf` to a shell function (so `wf open` can change your
40
+ shell's directory) and register completions, add this to your
41
+ ~/.zshrc or ~/.bashrc:
42
+ eval "$(workforest shell-init)"
43
+ EOS
44
+ end
45
+
46
+ test do
47
+ assert_match version.to_s, shell_output("#{bin}/workforest --version")
48
+ assert_match version.to_s, shell_output("#{bin}/wf --version")
49
+ (testpath/"init.bash").write shell_output("#{bin}/workforest shell-init bash")
50
+ system "bash", "-n", testpath/"init.bash"
51
+ end
52
+ end
@@ -18,6 +18,7 @@ classifiers = [
18
18
  "Environment :: Console",
19
19
  "Intended Audience :: Developers",
20
20
  "Operating System :: POSIX :: Linux",
21
+ "Operating System :: MacOS",
21
22
  "Programming Language :: Python :: 3.14",
22
23
  "Topic :: Software Development :: Version Control :: Git",
23
24
  ]
@@ -1,3 +1,3 @@
1
1
  """Workforest — git worktree forest management."""
2
2
 
3
- __version__ = "0.2.1"
3
+ __version__ = "0.2.3"
@@ -26,6 +26,19 @@ from workforest.errors import WorkforestError
26
26
 
27
27
  _PLACEHOLDER = re.compile(r"\{([a-z_]+)\}")
28
28
 
29
+ # Shell-session activation state that must not leak into a spawned window:
30
+ # (prefix variable, bin subdirs under it that activation put on PATH,
31
+ # companion variables set alongside it). An empty subdir means the prefix
32
+ # variable is itself the PATH entry.
33
+ _ACTIVATION_STATE: tuple[tuple[str, tuple[str, ...], tuple[str, ...]], ...] = (
34
+ ("VIRTUAL_ENV", ("bin", "Scripts"), ("VIRTUAL_ENV_PROMPT",)),
35
+ ("CONDA_PREFIX", ("bin",), ("CONDA_DEFAULT_ENV", "CONDA_PROMPT_MODIFIER", "CONDA_SHLVL")),
36
+ ("NVM_BIN", ("",), ("NVM_INC",)),
37
+ ("GEM_HOME", ("bin",), ("GEM_PATH",)),
38
+ ("MY_RUBY_HOME", ("bin",), ("RUBY_VERSION",)),
39
+ )
40
+ _CONDA_STACK = re.compile(r"CONDA_PREFIX_\d+")
41
+
29
42
 
30
43
  @dataclass(slots=True, frozen=True)
31
44
  class ShellAction:
@@ -131,6 +144,34 @@ def launch(
131
144
  return ShellAction(f"cd {shlex.quote(str(worktree))} && {assignments} {command}")
132
145
 
133
146
 
147
+ def scrub_activation_state(env: dict[str, str]) -> dict[str, str]:
148
+ """Drop venv/conda/nvm/rvm activation inherited from the invoking shell.
149
+
150
+ A spawned window is a fresh context: the tools' `deactivate` counterparts
151
+ are shell functions that don't exist there, so inherited activation is
152
+ unremovable and points at the wrong worktree's environment. Prompt-hook
153
+ managers (direnv, mise, asdf) re-derive their state in the new shell and
154
+ need no help; nix-shell is left alone because on NixOS its PATH entries
155
+ are indistinguishable from the system PATH.
156
+ """
157
+ env = dict(env)
158
+ stale_dirs: set[str] = set()
159
+ for var, subdirs, companions in _ACTIVATION_STATE:
160
+ prefix = env.pop(var, None)
161
+ for name in companions:
162
+ env.pop(name, None)
163
+ if prefix:
164
+ stale_dirs.update(str(Path(prefix) / sub) if sub else prefix for sub in subdirs)
165
+ for var in [name for name in env if _CONDA_STACK.fullmatch(name)]:
166
+ stale_dirs.add(str(Path(env.pop(var)) / "bin"))
167
+ path = env.get("PATH")
168
+ if path and stale_dirs:
169
+ env["PATH"] = os.pathsep.join(
170
+ entry for entry in path.split(os.pathsep) if entry not in stale_dirs
171
+ )
172
+ return env
173
+
174
+
134
175
  def spawn_window(
135
176
  window_template: str,
136
177
  *,
@@ -142,7 +183,8 @@ def spawn_window(
142
183
 
143
184
  The resolved opener command joins the family as {command} (one argument,
144
185
  e.g. for `$SHELL -c {command}`) / `$WF_COMMAND` (spliced into argv words);
145
- it is template-only and not exported to the environment.
186
+ it is template-only and not exported to the environment. The inherited
187
+ environment is passed through scrub_activation_state first.
146
188
  """
147
189
  expanded = expand_template(window_template, {**variables, "WF_COMMAND": command})
148
190
  argv = shlex.split(expanded)
@@ -152,7 +194,7 @@ def spawn_window(
152
194
  subprocess.Popen(
153
195
  argv,
154
196
  cwd=cwd,
155
- env={**os.environ, **variables},
197
+ env=scrub_activation_state({**os.environ, **variables}),
156
198
  start_new_session=True,
157
199
  stdin=subprocess.DEVNULL,
158
200
  stdout=subprocess.DEVNULL,
@@ -161,7 +161,7 @@ def run(initial_mode: str | None = None) -> CommandResult: # pragma: no cover -
161
161
  is unit-tested; this function only wires them to the fzf subprocess."""
162
162
  if shutil.which("fzf") is None:
163
163
  raise WorkforestError(
164
- "the TUI requires fzf (e.g. pacman -S fzf); "
164
+ "the TUI requires fzf (e.g. pacman -S fzf, brew install fzf); "
165
165
  "all actions are also available as plain subcommands"
166
166
  )
167
167
  ctx = commands.build_context()
@@ -150,7 +150,8 @@ def recorder(tmp_path: Path) -> Recorder:
150
150
  log = tmp_path / "recorder.log"
151
151
  script = tmp_path / "recorder"
152
152
  script.write_text(
153
- f'#!/bin/sh\necho "argv=$* argc=$# cwd=$PWD wf_worktree=$WF_WORKTREE" >> {log}\n'
153
+ f'#!/bin/sh\necho "argv=$* argc=$# cwd=$PWD wf_worktree=$WF_WORKTREE'
154
+ f' virtual_env=$VIRTUAL_ENV" >> {log}\n'
154
155
  )
155
156
  script.chmod(script.stat().st_mode | stat.S_IXUSR)
156
157
  return Recorder(path=script, log=log)
@@ -1,5 +1,6 @@
1
1
  """launch: opener/window template resolution and the cd protocol."""
2
2
 
3
+ import os
3
4
  import shlex
4
5
  from pathlib import Path
5
6
 
@@ -101,6 +102,61 @@ class TestTemplateExpansion:
101
102
  )
102
103
 
103
104
 
105
+ class TestScrubActivationState:
106
+ def test_venv_vars_and_path_entry_removed(self) -> None:
107
+ env = {
108
+ "VIRTUAL_ENV": "/repo/.venv",
109
+ "VIRTUAL_ENV_PROMPT": "(.venv)",
110
+ "PATH": "/repo/.venv/bin:/usr/local/bin:/usr/bin",
111
+ "HOME": "/home/u",
112
+ }
113
+ assert launch.scrub_activation_state(env) == {
114
+ "PATH": "/usr/local/bin:/usr/bin",
115
+ "HOME": "/home/u",
116
+ }
117
+
118
+ def test_clean_environment_untouched(self) -> None:
119
+ env = {"PATH": "/usr/local/bin:/usr/bin", "HOME": "/home/u", "WF_BRANCH": "feat"}
120
+ assert launch.scrub_activation_state(env) == env
121
+
122
+ def test_conda_including_stacked_prefixes(self) -> None:
123
+ env = {
124
+ "CONDA_PREFIX": "/opt/conda/envs/proj",
125
+ "CONDA_PREFIX_1": "/opt/conda",
126
+ "CONDA_DEFAULT_ENV": "proj",
127
+ "CONDA_SHLVL": "2",
128
+ "CONDA_PROMPT_MODIFIER": "(proj) ",
129
+ "PATH": "/opt/conda/envs/proj/bin:/opt/conda/bin:/opt/conda/condabin:/usr/bin",
130
+ }
131
+ # condabin survives so `conda` itself keeps working in the window.
132
+ assert launch.scrub_activation_state(env) == {
133
+ "PATH": "/opt/conda/condabin:/usr/bin",
134
+ }
135
+
136
+ def test_nvm_bin_is_itself_the_path_entry(self) -> None:
137
+ env = {
138
+ "NVM_BIN": "/home/u/.nvm/versions/node/v22.0.0/bin",
139
+ "NVM_INC": "/home/u/.nvm/versions/node/v22.0.0/include/node",
140
+ "PATH": "/home/u/.nvm/versions/node/v22.0.0/bin:/usr/bin",
141
+ }
142
+ assert launch.scrub_activation_state(env) == {"PATH": "/usr/bin"}
143
+
144
+ def test_rvm_ruby(self) -> None:
145
+ env = {
146
+ "GEM_HOME": "/home/u/.rvm/gems/ruby-3.3.0",
147
+ "GEM_PATH": "/home/u/.rvm/gems/ruby-3.3.0:/home/u/.rvm/gems/ruby-3.3.0@global",
148
+ "MY_RUBY_HOME": "/home/u/.rvm/rubies/ruby-3.3.0",
149
+ "RUBY_VERSION": "ruby-3.3.0",
150
+ "PATH": (
151
+ "/home/u/.rvm/gems/ruby-3.3.0/bin:/home/u/.rvm/rubies/ruby-3.3.0/bin:/usr/bin"
152
+ ),
153
+ }
154
+ assert launch.scrub_activation_state(env) == {"PATH": "/usr/bin"}
155
+
156
+ def test_missing_path_is_fine(self) -> None:
157
+ assert launch.scrub_activation_state({"VIRTUAL_ENV": "/repo/.venv"}) == {}
158
+
159
+
104
160
  def run_launch(
105
161
  cfg: Config,
106
162
  tmp_path: Path,
@@ -186,6 +242,16 @@ class TestLaunch:
186
242
  (line,) = recorder.wait_for_lines(1)
187
243
  assert f"wf_worktree={worktree}" in line
188
244
 
245
+ def test_window_process_sheds_inherited_venv(
246
+ self, tmp_path: Path, recorder: Recorder, monkeypatch: pytest.MonkeyPatch
247
+ ) -> None:
248
+ monkeypatch.setenv("VIRTUAL_ENV", str(tmp_path / ".venv"))
249
+ monkeypatch.setenv("PATH", f"{tmp_path / '.venv' / 'bin'}:{os.environ['PATH']}")
250
+ cfg = Config(window_command=f"{recorder.path} $WF_COMMAND")
251
+ run_launch(cfg, tmp_path, opener_arg="x")
252
+ (line,) = recorder.wait_for_lines(1)
253
+ assert line.endswith("virtual_env=")
254
+
189
255
  def test_empty_window_command_after_expansion(
190
256
  self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
191
257
  ) -> None:
@@ -1,19 +0,0 @@
1
- pkgbase = workforest
2
- pkgdesc = Git worktree forest management: per-branch worktrees with project-defined setup hooks
3
- pkgver = 0.2.0
4
- pkgrel = 1
5
- url = https://github.com/ArkadyBuryakov/workforest
6
- arch = any
7
- license = MIT
8
- makedepends = python-build
9
- makedepends = python-installer
10
- makedepends = python-wheel
11
- makedepends = python-hatchling
12
- depends = python
13
- depends = python-yaml
14
- depends = git
15
- optdepends = fzf: interactive TUI (workforest tui)
16
- source = workforest-0.2.0.tar.gz::https://github.com/ArkadyBuryakov/workforest/archive/v0.2.0.tar.gz
17
- sha256sums = dc1ba2518e2612714e023f7fc088aeaa37f9e0d5bffbcbc07c2dd15f109bb476
18
-
19
- pkgname = workforest
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes