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.
- {workforest-0.2.1 → workforest-0.2.3}/.github/workflows/publish_aur.yml +4 -25
- workforest-0.2.3/.github/workflows/publish_homebrew.yml +76 -0
- {workforest-0.2.1 → workforest-0.2.3}/.gitignore +4 -1
- {workforest-0.2.1 → workforest-0.2.3}/PKG-INFO +17 -8
- {workforest-0.2.1 → workforest-0.2.3}/README.md +15 -7
- workforest-0.2.1/packaging/AUR/PKGBUILD → workforest-0.2.3/packaging/AUR/PKGBUILD.template +6 -2
- workforest-0.2.3/packaging/homebrew/workforest.rb.template +52 -0
- {workforest-0.2.1 → workforest-0.2.3}/pyproject.toml +1 -0
- {workforest-0.2.1 → workforest-0.2.3}/src/workforest/__init__.py +1 -1
- {workforest-0.2.1 → workforest-0.2.3}/src/workforest/launch.py +44 -2
- {workforest-0.2.1 → workforest-0.2.3}/src/workforest/tui.py +1 -1
- {workforest-0.2.1 → workforest-0.2.3}/tests/conftest.py +2 -1
- {workforest-0.2.1 → workforest-0.2.3}/tests/test_launch.py +66 -0
- workforest-0.2.1/packaging/AUR/.SRCINFO +0 -19
- {workforest-0.2.1 → workforest-0.2.3}/.github/workflows/ci.yml +0 -0
- {workforest-0.2.1 → workforest-0.2.3}/.github/workflows/publish_pypi.yml +0 -0
- {workforest-0.2.1 → workforest-0.2.3}/.github/workflows/release.yml +0 -0
- {workforest-0.2.1 → workforest-0.2.3}/LICENSE +0 -0
- {workforest-0.2.1 → workforest-0.2.3}/Makefile +0 -0
- {workforest-0.2.1 → workforest-0.2.3}/completions/_workforest +0 -0
- {workforest-0.2.1 → workforest-0.2.3}/src/workforest/__main__.py +0 -0
- {workforest-0.2.1 → workforest-0.2.3}/src/workforest/cli.py +0 -0
- {workforest-0.2.1 → workforest-0.2.3}/src/workforest/commands.py +0 -0
- {workforest-0.2.1 → workforest-0.2.3}/src/workforest/completions.py +0 -0
- {workforest-0.2.1 → workforest-0.2.3}/src/workforest/config.py +0 -0
- {workforest-0.2.1 → workforest-0.2.3}/src/workforest/errors.py +0 -0
- {workforest-0.2.1 → workforest-0.2.3}/src/workforest/examples/config.yaml +0 -0
- {workforest-0.2.1 → workforest-0.2.3}/src/workforest/gitutil.py +0 -0
- {workforest-0.2.1 → workforest-0.2.3}/src/workforest/hooks.py +0 -0
- {workforest-0.2.1 → workforest-0.2.3}/src/workforest/integrations/__init__.py +0 -0
- {workforest-0.2.1 → workforest-0.2.3}/src/workforest/integrations/claude.py +0 -0
- {workforest-0.2.1 → workforest-0.2.3}/src/workforest/output.py +0 -0
- {workforest-0.2.1 → workforest-0.2.3}/src/workforest/shell/completion.bash +0 -0
- {workforest-0.2.1 → workforest-0.2.3}/src/workforest/shell/completion.zsh +0 -0
- {workforest-0.2.1 → workforest-0.2.3}/src/workforest/shell/workforest.sh +0 -0
- {workforest-0.2.1 → workforest-0.2.3}/src/workforest/shellinit.py +0 -0
- {workforest-0.2.1 → workforest-0.2.3}/tests/__init__.py +0 -0
- {workforest-0.2.1 → workforest-0.2.3}/tests/test_claude.py +0 -0
- {workforest-0.2.1 → workforest-0.2.3}/tests/test_cli.py +0 -0
- {workforest-0.2.1 → workforest-0.2.3}/tests/test_commands.py +0 -0
- {workforest-0.2.1 → workforest-0.2.3}/tests/test_config.py +0 -0
- {workforest-0.2.1 → workforest-0.2.3}/tests/test_gitutil.py +0 -0
- {workforest-0.2.1 → workforest-0.2.3}/tests/test_harness.py +0 -0
- {workforest-0.2.1 → workforest-0.2.3}/tests/test_hooks.py +0 -0
- {workforest-0.2.1 → workforest-0.2.3}/tests/test_shell.py +0 -0
- {workforest-0.2.1 → workforest-0.2.3}/tests/test_tui.py +0 -0
- {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:
|
|
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:
|
|
24
|
+
- name: Render PKGBUILD from template
|
|
25
25
|
run: |
|
|
26
26
|
version="${GITHUB_REF_NAME#v}"
|
|
27
27
|
cd packaging/AUR
|
|
28
|
-
sed
|
|
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.
|
|
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
|
|
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
|
|
199
|
-
`packaging
|
|
200
|
-
|
|
201
|
-
`
|
|
202
|
-
|
|
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
|
|
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
|
|
180
|
-
`packaging
|
|
181
|
-
|
|
182
|
-
`
|
|
183
|
-
|
|
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
|
|
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=('
|
|
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
|
]
|
|
@@ -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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|