tmuxpull 0.1.2__tar.gz → 0.2.1__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.
@@ -0,0 +1,323 @@
1
+ Metadata-Version: 2.5
2
+ Name: tmuxpull
3
+ Version: 0.2.1
4
+ Summary: Keep every git repo under a directory in step with its remote, concurrently, with a tmux session per repo that needs you
5
+ Project-URL: Homepage, https://github.com/nguyengg/tmuxpull
6
+ Project-URL: Repository, https://github.com/nguyengg/tmuxpull.git
7
+ Project-URL: Issues, https://github.com/nguyengg/tmuxpull/issues
8
+ Author-email: Henry Nguyen <5065089+nguyengg@users.noreply.github.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: automation,concurrent,git,rebase,tmux
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: MacOS
17
+ Classifier: Operating System :: POSIX
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: Software Development :: Version Control :: Git
22
+ Classifier: Topic :: System :: Systems Administration
23
+ Classifier: Topic :: Terminals
24
+ Requires-Python: >=3.11
25
+ Requires-Dist: libtmux>=0.35
26
+ Description-Content-Type: text/markdown
27
+
28
+ # tmuxpull
29
+
30
+ Keep every Git repo under a directory in step with its remote, concurrently, so
31
+ merge conflicts surface **this morning** instead of at push time — with a tmux
32
+ session per repo for the ones that need you.
33
+
34
+ ## Quick Start
35
+
36
+ ### Run instantly with curl (no install)
37
+
38
+ ```bash
39
+ # Python version — requires uv (https://docs.astral.sh/uv/)
40
+ curl -fsSL https://raw.githubusercontent.com/nguyengg/tmuxpull/main/bin/rebase-all.py | uv run - ~/Workspaces
41
+
42
+ # Zsh version — zero dependencies (just git + tmux)
43
+ curl -fsSL https://raw.githubusercontent.com/nguyengg/tmuxpull/main/bin/rebase-all | zsh -s -- ~/Workspaces
44
+ ```
45
+
46
+ ### Install with curl (one-liner)
47
+
48
+ ```bash
49
+ # Download the self-contained script to ~/.local/bin
50
+ curl -fsSL https://raw.githubusercontent.com/nguyengg/tmuxpull/main/bin/rebase-all.py -o ~/.local/bin/tmuxpull && chmod +x ~/.local/bin/tmuxpull
51
+ tmuxpull ~/Workspaces
52
+ ```
53
+
54
+ The script carries its own dependency metadata (PEP 723), so with `uv` on your PATH it bootstraps its own environment on first run — no venv, no pip install.
55
+
56
+ ### Install from PyPI
57
+
58
+ ```bash
59
+ # Install via pip/uv (recommended)
60
+ pip install tmuxpull
61
+ tmuxpull ~/Workspaces
62
+
63
+ # Or install as a uv tool
64
+ uv tool install tmuxpull
65
+ tmuxpull ~/Workspaces
66
+ ```
67
+
68
+ ## What it does to each repo
69
+
70
+ The **default branch** (`main`, or whatever `origin/HEAD` points at) is the
71
+ reference point — never whatever happens to be checked out. Every repo gets a
72
+ `git fetch --prune` first, then:
73
+
74
+ | you are on | what happens | your worktree |
75
+ |---|---|---|
76
+ | the default branch | `git pull --rebase --autostash` — your unpushed commits replay onto the new upstream | rebased in place |
77
+ | a branch you never pushed | `git rebase --autostash <remote>/<default>` — nothing is published yet, so replaying is free | rebased in place |
78
+ | a branch you **have** pushed | detect only: the branch fast-forwards to its own upstream, and `git merge-tree` probes it against the new default branch | untouched |
79
+ | detached HEAD | detect only | untouched |
80
+
81
+ Two things happen regardless: the local default branch is **fast-forwarded by a
82
+ plain ref update, with no checkout**, so the next branch you cut is current; and
83
+ if another worktree has it checked out, it's left alone for that worktree's own
84
+ run.
85
+
86
+ ### Why pushed branches are only probed
87
+
88
+ Rebasing commits that already exist on the remote means your next push needs
89
+ `--force-with-lease` — not something a batch tool should decide for you across a
90
+ dozen repos. `git merge-tree` answers "would this conflict?" entirely in memory:
91
+ no checkout, no index, nothing to clean up. It models a *merge*, so a clean
92
+ answer is a strong signal rather than a guarantee that replaying every commit is
93
+ clean.
94
+
95
+ Pass `--rebase-pushed` to rebase them for real. It refuses for any branch whose
96
+ upstream has commits you don't have — replaying only your side would orphan the
97
+ pushed ones.
98
+
99
+ ### Conflicts are left in progress
100
+
101
+ A rebase that stops is **not** aborted. The repo keeps its conflict markers and
102
+ its in-progress rebase, and its tmux session is where you resolve it. That's the
103
+ point of the tool: clean repos print a line and disappear, broken ones become
104
+ your work queue.
105
+
106
+ ## Usage
107
+
108
+ ```bash
109
+ tmuxpull [-d DEPTH] [-j JOBS] [--tmux {on,off}] [--rebase-pushed]
110
+ [--log PATH] [--no-log] [-v] [--dry-run] DIR [DIR ...]
111
+ ```
112
+
113
+ ### Options
114
+
115
+ - `-d, --max-depth N` — Directory search depth (default: 2)
116
+ - `-j, --jobs N` — Max concurrent repos (default: min(8, 2×CPU))
117
+ - `-x, --exclude GLOB` — Skip repos whose name matches the glob (repeatable), e.g. `-x 'kirodotdev/*'`
118
+ - `--tmux {on,off}` — Create per-repo tmux sessions (default: on)
119
+ - `--rebase-pushed` — Also rebase pushed branches (costs you a `--force-with-lease`)
120
+ - `--log PATH` — Write the run report here (default: `$XDG_STATE_HOME/tmuxpull/last-run.log`)
121
+ - `--no-log` — Don't write a report file
122
+ - `-v, --verbose` — Show incoming commit subjects (`-v` = top 3, `-vv` = all)
123
+ - `--dry-run` — List repos that would be processed, then exit
124
+ - `-V, --version` — Print the version and the file it's running from, then exit
125
+
126
+ ### Am I running the latest?
127
+
128
+ ```bash
129
+ $ tmuxpull --version
130
+ tmuxpull 0.2.1 (/home/you/.local/share/uv/tools/tmuxpull/lib/python3.14/site-packages/tmuxpull/__init__.py)
131
+ ```
132
+
133
+ The path is there because one machine can easily have three copies on `PATH` —
134
+ a `pip install` into whichever Python was current, a `uv tool install`, and a
135
+ curl'd standalone script — and the version number alone won't tell you which one
136
+ just ran. The second field is always the bare version, so `tmuxpull --version |
137
+ awk '{print $2}'` is scriptable.
138
+
139
+ Compare against what's published:
140
+
141
+ ```bash
142
+ curl -s https://pypi.org/pypi/tmuxpull/json | grep -o '"version":"[^"]*"' | head -1
143
+ ```
144
+
145
+ Upgrading depends on how it was installed:
146
+
147
+ ```bash
148
+ uv tool upgrade tmuxpull # uv tool install
149
+ pip install -U tmuxpull # pip install
150
+ ```
151
+
152
+ A `pip install` into a version-managed Python (mise, pyenv, asdf) is worth
153
+ avoiding: the package lives under that exact interpreter, so the next Python
154
+ upgrade silently leaves it behind — or drops it off `PATH` entirely. `uv tool
155
+ install tmuxpull` keeps it in its own environment instead. The curl one-liners
156
+ need no upgrade at all: they read `main` directly, so they're current the moment
157
+ a fix lands.
158
+
159
+ ### Per-repo git config
160
+
161
+ ```bash
162
+ # Skip a repo entirely (survives every run until unset) — e.g. a broken tip:
163
+ git -C ~/github.com/kirodotdev/KiroCrew config tmuxpull.ignore true
164
+ git -C ~/github.com/kirodotdev/KiroCrew config --unset tmuxpull.ignore
165
+
166
+ # Override the detected default branch:
167
+ git -C ~/github.com/acme/legacy config tmuxpull.defaultBranch release
168
+ ```
169
+
170
+ Default-branch detection order: `tmuxpull.defaultBranch`, then
171
+ `refs/remotes/<remote>/HEAD`, then a probe of `main` / `master` / `trunk`.
172
+ The remote is `origin` when present, otherwise the first one configured.
173
+
174
+ ### Examples
175
+
176
+ ```bash
177
+ # Morning sync across your workspace
178
+ tmuxpull ~/Workspaces ~/Projects
179
+
180
+ # High concurrency
181
+ tmuxpull -j 16 ~/Code
182
+
183
+ # Just print what would happen
184
+ tmuxpull --dry-run ~/Projects
185
+
186
+ # Verbose output showing incoming commit messages
187
+ tmuxpull -v ~/Workspaces
188
+ ```
189
+
190
+ ## Output
191
+
192
+ Per-repo summary lines, printed as each repo finishes:
193
+
194
+ ```
195
+ on-main-ff + 1 commit 1 file changed, 1 insertion(+)
196
+ on-main-replay ~ rebased 1 onto main, pulled 1 commit 1 file changed, 1 insertion(+)
197
+ feat-unpushed ~ rebased 1 onto main, main +1 1 file changed, 1 insertion(+)
198
+ up-to-date-repo = up to date
199
+ feat-pushed ! DIVERGES from main: 1 file would conflict (main +1)
200
+ feat-conflict ! CONFLICT: rebase stopped, 1 file -- resolve here (main +1)
201
+ broken-remote ! FAIL: could not read from remote repository
202
+ quiet - ignored (git config tmuxpull.ignore)
203
+ no-remote - no remote
204
+ ```
205
+
206
+ `!` lines go to stderr and mean you have to act; `+`/`~`/`=`/`-` go to stdout.
207
+ The exit code is 1 when anything needs attention.
208
+
209
+ ### The result list outlives the handoff
210
+
211
+ Choosing a tmux session used to cost you the summary — the process was replaced
212
+ by tmux and nothing survived a detach. Now three things persist it:
213
+
214
+ 1. **An attention block on stderr** before tmux takes the terminal, so it stays
215
+ in the launching shell's scrollback:
216
+
217
+ ```
218
+ 2 of 9 repos need attention:
219
+ feat-pushed ! DIVERGES from main: 1 file would conflict (main +1)
220
+ tmux attach -t Projects/feat-pushed
221
+ feat-conflict ! CONFLICT: rebase stopped, 1 file -- resolve here (main +1)
222
+ tmux attach -t Projects/feat-conflict
223
+ full report: ~/.local/state/tmuxpull/last-run.log
224
+ ```
225
+
226
+ 2. **A report file**, always written, whether or not you pick a session — every
227
+ repo, attention first, greppable by state (`grep '^\[conflict' last-run.log`).
228
+ It survives closing the terminal entirely.
229
+
230
+ 3. **A picker you come back to.** tmux runs as a child process, so detaching
231
+ returns you to the picker with the list reprinted: fix one repo, detach, pick
232
+ the next, `q` when you're done. Inside an existing tmux client this becomes
233
+ `tmux switch-client` (nested `attach` is refused) and control does not return.
234
+
235
+ When output is piped or redirected the picker is skipped and the full
236
+ `tmux attach -t <name>` list is printed instead, so scripts and CI still work.
237
+
238
+ ## Worktrees
239
+
240
+ A repo and its worktrees (`repo` + `repo.wt/feat`) are separate directories, so a
241
+ scan finds both — but they share **one object store and one ref namespace**.
242
+ tmuxpull groups repos by `git rev-parse --git-common-dir` and serializes each
243
+ group while running different groups in parallel, so concurrent jobs can't race
244
+ on ref locks or `FETCH_HEAD`.
245
+
246
+ ## Two Versions
247
+
248
+ ### `src/tmuxpull/` + `bin/rebase-all.py` (single source of truth)
249
+
250
+ The PyPI package (`src/tmuxpull/__init__.py`) is the canonical implementation.
251
+ `bin/rebase-all.py` — the standalone PEP 723 script the curl one-liners use — is
252
+ **generated from it** (`python scripts/gen_script.py`, or `mise run gen-script`);
253
+ a test fails if the two drift.
254
+
255
+ **Requirements**: Python 3.11+, git 2.38+ (for `merge-tree --write-tree`), tmux,
256
+ plus [uv](https://docs.astral.sh/uv/) for the standalone script
257
+
258
+ ### `bin/rebase-all` (Fallback)
259
+
260
+ - **Pure Zsh** — no Python dependencies
261
+ - Same branch handling, same report file, same attention block, same picker
262
+ - Same options, including `--rebase-pushed`, `--log` / `--no-log`, and both
263
+ `git config tmuxpull.*` knobs
264
+ - `tests/test_zsh_parity.py` builds one fixture per implementation and asserts
265
+ their summary lines are identical, so the two cannot drift silently
266
+
267
+ The one deliberate difference: summaries print in input order at the end of the
268
+ run (vs. Python's live completion-order `[n/N]` counter). If the two ever
269
+ disagree otherwise, the Python version is the source of truth.
270
+
271
+ **Requirements**: Zsh, git 2.38+, tmux
272
+
273
+ ## Installation
274
+
275
+ ### From PyPI (Recommended)
276
+
277
+ ```bash
278
+ # Install globally
279
+ pip install tmuxpull
280
+
281
+ # Or as a uv tool (isolated)
282
+ uv tool install tmuxpull
283
+ ```
284
+
285
+ ### From Source
286
+
287
+ ```bash
288
+ # Clone and install
289
+ git clone https://github.com/nguyengg/tmuxpull.git
290
+ cd tmuxpull
291
+ pip install .
292
+
293
+ # Or for development
294
+ uv sync --dev
295
+ ```
296
+
297
+ ### Zsh Fallback
298
+
299
+ For machines without Python, use the dependency-free Zsh script:
300
+ ```bash
301
+ chmod +x bin/rebase-all
302
+ ln -s $PWD/bin/rebase-all ~/.local/bin/
303
+ ```
304
+
305
+ ## Design
306
+
307
+ Finds Git repos by walking the filesystem looking for `.git` entries, up to a
308
+ configurable depth. Prunes common noise directories (`node_modules`, build
309
+ artifacts, Python venvs) to avoid slow traversals.
310
+
311
+ Work runs concurrently via `asyncio` (Python) or Zsh job control, capped to
312
+ avoid overwhelming git servers, and serialized per shared object store. Each
313
+ repo is isolated — one failure doesn't stop the others.
314
+
315
+ The tmux integration is the key workflow piece: clean repos just print their
316
+ summary and disappear, while repos needing intervention (conflict resolution,
317
+ diverged branches) open interactive sessions where you fix things. The picker is
318
+ your morning work queue, and the report file is what's left of it after you close
319
+ the terminal.
320
+
321
+ ## License
322
+
323
+ MIT
@@ -0,0 +1,296 @@
1
+ # tmuxpull
2
+
3
+ Keep every Git repo under a directory in step with its remote, concurrently, so
4
+ merge conflicts surface **this morning** instead of at push time — with a tmux
5
+ session per repo for the ones that need you.
6
+
7
+ ## Quick Start
8
+
9
+ ### Run instantly with curl (no install)
10
+
11
+ ```bash
12
+ # Python version — requires uv (https://docs.astral.sh/uv/)
13
+ curl -fsSL https://raw.githubusercontent.com/nguyengg/tmuxpull/main/bin/rebase-all.py | uv run - ~/Workspaces
14
+
15
+ # Zsh version — zero dependencies (just git + tmux)
16
+ curl -fsSL https://raw.githubusercontent.com/nguyengg/tmuxpull/main/bin/rebase-all | zsh -s -- ~/Workspaces
17
+ ```
18
+
19
+ ### Install with curl (one-liner)
20
+
21
+ ```bash
22
+ # Download the self-contained script to ~/.local/bin
23
+ curl -fsSL https://raw.githubusercontent.com/nguyengg/tmuxpull/main/bin/rebase-all.py -o ~/.local/bin/tmuxpull && chmod +x ~/.local/bin/tmuxpull
24
+ tmuxpull ~/Workspaces
25
+ ```
26
+
27
+ The script carries its own dependency metadata (PEP 723), so with `uv` on your PATH it bootstraps its own environment on first run — no venv, no pip install.
28
+
29
+ ### Install from PyPI
30
+
31
+ ```bash
32
+ # Install via pip/uv (recommended)
33
+ pip install tmuxpull
34
+ tmuxpull ~/Workspaces
35
+
36
+ # Or install as a uv tool
37
+ uv tool install tmuxpull
38
+ tmuxpull ~/Workspaces
39
+ ```
40
+
41
+ ## What it does to each repo
42
+
43
+ The **default branch** (`main`, or whatever `origin/HEAD` points at) is the
44
+ reference point — never whatever happens to be checked out. Every repo gets a
45
+ `git fetch --prune` first, then:
46
+
47
+ | you are on | what happens | your worktree |
48
+ |---|---|---|
49
+ | the default branch | `git pull --rebase --autostash` — your unpushed commits replay onto the new upstream | rebased in place |
50
+ | a branch you never pushed | `git rebase --autostash <remote>/<default>` — nothing is published yet, so replaying is free | rebased in place |
51
+ | a branch you **have** pushed | detect only: the branch fast-forwards to its own upstream, and `git merge-tree` probes it against the new default branch | untouched |
52
+ | detached HEAD | detect only | untouched |
53
+
54
+ Two things happen regardless: the local default branch is **fast-forwarded by a
55
+ plain ref update, with no checkout**, so the next branch you cut is current; and
56
+ if another worktree has it checked out, it's left alone for that worktree's own
57
+ run.
58
+
59
+ ### Why pushed branches are only probed
60
+
61
+ Rebasing commits that already exist on the remote means your next push needs
62
+ `--force-with-lease` — not something a batch tool should decide for you across a
63
+ dozen repos. `git merge-tree` answers "would this conflict?" entirely in memory:
64
+ no checkout, no index, nothing to clean up. It models a *merge*, so a clean
65
+ answer is a strong signal rather than a guarantee that replaying every commit is
66
+ clean.
67
+
68
+ Pass `--rebase-pushed` to rebase them for real. It refuses for any branch whose
69
+ upstream has commits you don't have — replaying only your side would orphan the
70
+ pushed ones.
71
+
72
+ ### Conflicts are left in progress
73
+
74
+ A rebase that stops is **not** aborted. The repo keeps its conflict markers and
75
+ its in-progress rebase, and its tmux session is where you resolve it. That's the
76
+ point of the tool: clean repos print a line and disappear, broken ones become
77
+ your work queue.
78
+
79
+ ## Usage
80
+
81
+ ```bash
82
+ tmuxpull [-d DEPTH] [-j JOBS] [--tmux {on,off}] [--rebase-pushed]
83
+ [--log PATH] [--no-log] [-v] [--dry-run] DIR [DIR ...]
84
+ ```
85
+
86
+ ### Options
87
+
88
+ - `-d, --max-depth N` — Directory search depth (default: 2)
89
+ - `-j, --jobs N` — Max concurrent repos (default: min(8, 2×CPU))
90
+ - `-x, --exclude GLOB` — Skip repos whose name matches the glob (repeatable), e.g. `-x 'kirodotdev/*'`
91
+ - `--tmux {on,off}` — Create per-repo tmux sessions (default: on)
92
+ - `--rebase-pushed` — Also rebase pushed branches (costs you a `--force-with-lease`)
93
+ - `--log PATH` — Write the run report here (default: `$XDG_STATE_HOME/tmuxpull/last-run.log`)
94
+ - `--no-log` — Don't write a report file
95
+ - `-v, --verbose` — Show incoming commit subjects (`-v` = top 3, `-vv` = all)
96
+ - `--dry-run` — List repos that would be processed, then exit
97
+ - `-V, --version` — Print the version and the file it's running from, then exit
98
+
99
+ ### Am I running the latest?
100
+
101
+ ```bash
102
+ $ tmuxpull --version
103
+ tmuxpull 0.2.1 (/home/you/.local/share/uv/tools/tmuxpull/lib/python3.14/site-packages/tmuxpull/__init__.py)
104
+ ```
105
+
106
+ The path is there because one machine can easily have three copies on `PATH` —
107
+ a `pip install` into whichever Python was current, a `uv tool install`, and a
108
+ curl'd standalone script — and the version number alone won't tell you which one
109
+ just ran. The second field is always the bare version, so `tmuxpull --version |
110
+ awk '{print $2}'` is scriptable.
111
+
112
+ Compare against what's published:
113
+
114
+ ```bash
115
+ curl -s https://pypi.org/pypi/tmuxpull/json | grep -o '"version":"[^"]*"' | head -1
116
+ ```
117
+
118
+ Upgrading depends on how it was installed:
119
+
120
+ ```bash
121
+ uv tool upgrade tmuxpull # uv tool install
122
+ pip install -U tmuxpull # pip install
123
+ ```
124
+
125
+ A `pip install` into a version-managed Python (mise, pyenv, asdf) is worth
126
+ avoiding: the package lives under that exact interpreter, so the next Python
127
+ upgrade silently leaves it behind — or drops it off `PATH` entirely. `uv tool
128
+ install tmuxpull` keeps it in its own environment instead. The curl one-liners
129
+ need no upgrade at all: they read `main` directly, so they're current the moment
130
+ a fix lands.
131
+
132
+ ### Per-repo git config
133
+
134
+ ```bash
135
+ # Skip a repo entirely (survives every run until unset) — e.g. a broken tip:
136
+ git -C ~/github.com/kirodotdev/KiroCrew config tmuxpull.ignore true
137
+ git -C ~/github.com/kirodotdev/KiroCrew config --unset tmuxpull.ignore
138
+
139
+ # Override the detected default branch:
140
+ git -C ~/github.com/acme/legacy config tmuxpull.defaultBranch release
141
+ ```
142
+
143
+ Default-branch detection order: `tmuxpull.defaultBranch`, then
144
+ `refs/remotes/<remote>/HEAD`, then a probe of `main` / `master` / `trunk`.
145
+ The remote is `origin` when present, otherwise the first one configured.
146
+
147
+ ### Examples
148
+
149
+ ```bash
150
+ # Morning sync across your workspace
151
+ tmuxpull ~/Workspaces ~/Projects
152
+
153
+ # High concurrency
154
+ tmuxpull -j 16 ~/Code
155
+
156
+ # Just print what would happen
157
+ tmuxpull --dry-run ~/Projects
158
+
159
+ # Verbose output showing incoming commit messages
160
+ tmuxpull -v ~/Workspaces
161
+ ```
162
+
163
+ ## Output
164
+
165
+ Per-repo summary lines, printed as each repo finishes:
166
+
167
+ ```
168
+ on-main-ff + 1 commit 1 file changed, 1 insertion(+)
169
+ on-main-replay ~ rebased 1 onto main, pulled 1 commit 1 file changed, 1 insertion(+)
170
+ feat-unpushed ~ rebased 1 onto main, main +1 1 file changed, 1 insertion(+)
171
+ up-to-date-repo = up to date
172
+ feat-pushed ! DIVERGES from main: 1 file would conflict (main +1)
173
+ feat-conflict ! CONFLICT: rebase stopped, 1 file -- resolve here (main +1)
174
+ broken-remote ! FAIL: could not read from remote repository
175
+ quiet - ignored (git config tmuxpull.ignore)
176
+ no-remote - no remote
177
+ ```
178
+
179
+ `!` lines go to stderr and mean you have to act; `+`/`~`/`=`/`-` go to stdout.
180
+ The exit code is 1 when anything needs attention.
181
+
182
+ ### The result list outlives the handoff
183
+
184
+ Choosing a tmux session used to cost you the summary — the process was replaced
185
+ by tmux and nothing survived a detach. Now three things persist it:
186
+
187
+ 1. **An attention block on stderr** before tmux takes the terminal, so it stays
188
+ in the launching shell's scrollback:
189
+
190
+ ```
191
+ 2 of 9 repos need attention:
192
+ feat-pushed ! DIVERGES from main: 1 file would conflict (main +1)
193
+ tmux attach -t Projects/feat-pushed
194
+ feat-conflict ! CONFLICT: rebase stopped, 1 file -- resolve here (main +1)
195
+ tmux attach -t Projects/feat-conflict
196
+ full report: ~/.local/state/tmuxpull/last-run.log
197
+ ```
198
+
199
+ 2. **A report file**, always written, whether or not you pick a session — every
200
+ repo, attention first, greppable by state (`grep '^\[conflict' last-run.log`).
201
+ It survives closing the terminal entirely.
202
+
203
+ 3. **A picker you come back to.** tmux runs as a child process, so detaching
204
+ returns you to the picker with the list reprinted: fix one repo, detach, pick
205
+ the next, `q` when you're done. Inside an existing tmux client this becomes
206
+ `tmux switch-client` (nested `attach` is refused) and control does not return.
207
+
208
+ When output is piped or redirected the picker is skipped and the full
209
+ `tmux attach -t <name>` list is printed instead, so scripts and CI still work.
210
+
211
+ ## Worktrees
212
+
213
+ A repo and its worktrees (`repo` + `repo.wt/feat`) are separate directories, so a
214
+ scan finds both — but they share **one object store and one ref namespace**.
215
+ tmuxpull groups repos by `git rev-parse --git-common-dir` and serializes each
216
+ group while running different groups in parallel, so concurrent jobs can't race
217
+ on ref locks or `FETCH_HEAD`.
218
+
219
+ ## Two Versions
220
+
221
+ ### `src/tmuxpull/` + `bin/rebase-all.py` (single source of truth)
222
+
223
+ The PyPI package (`src/tmuxpull/__init__.py`) is the canonical implementation.
224
+ `bin/rebase-all.py` — the standalone PEP 723 script the curl one-liners use — is
225
+ **generated from it** (`python scripts/gen_script.py`, or `mise run gen-script`);
226
+ a test fails if the two drift.
227
+
228
+ **Requirements**: Python 3.11+, git 2.38+ (for `merge-tree --write-tree`), tmux,
229
+ plus [uv](https://docs.astral.sh/uv/) for the standalone script
230
+
231
+ ### `bin/rebase-all` (Fallback)
232
+
233
+ - **Pure Zsh** — no Python dependencies
234
+ - Same branch handling, same report file, same attention block, same picker
235
+ - Same options, including `--rebase-pushed`, `--log` / `--no-log`, and both
236
+ `git config tmuxpull.*` knobs
237
+ - `tests/test_zsh_parity.py` builds one fixture per implementation and asserts
238
+ their summary lines are identical, so the two cannot drift silently
239
+
240
+ The one deliberate difference: summaries print in input order at the end of the
241
+ run (vs. Python's live completion-order `[n/N]` counter). If the two ever
242
+ disagree otherwise, the Python version is the source of truth.
243
+
244
+ **Requirements**: Zsh, git 2.38+, tmux
245
+
246
+ ## Installation
247
+
248
+ ### From PyPI (Recommended)
249
+
250
+ ```bash
251
+ # Install globally
252
+ pip install tmuxpull
253
+
254
+ # Or as a uv tool (isolated)
255
+ uv tool install tmuxpull
256
+ ```
257
+
258
+ ### From Source
259
+
260
+ ```bash
261
+ # Clone and install
262
+ git clone https://github.com/nguyengg/tmuxpull.git
263
+ cd tmuxpull
264
+ pip install .
265
+
266
+ # Or for development
267
+ uv sync --dev
268
+ ```
269
+
270
+ ### Zsh Fallback
271
+
272
+ For machines without Python, use the dependency-free Zsh script:
273
+ ```bash
274
+ chmod +x bin/rebase-all
275
+ ln -s $PWD/bin/rebase-all ~/.local/bin/
276
+ ```
277
+
278
+ ## Design
279
+
280
+ Finds Git repos by walking the filesystem looking for `.git` entries, up to a
281
+ configurable depth. Prunes common noise directories (`node_modules`, build
282
+ artifacts, Python venvs) to avoid slow traversals.
283
+
284
+ Work runs concurrently via `asyncio` (Python) or Zsh job control, capped to
285
+ avoid overwhelming git servers, and serialized per shared object store. Each
286
+ repo is isolated — one failure doesn't stop the others.
287
+
288
+ The tmux integration is the key workflow piece: clean repos just print their
289
+ summary and disappear, while repos needing intervention (conflict resolution,
290
+ diverged branches) open interactive sessions where you fix things. The picker is
291
+ your morning work queue, and the report file is what's left of it after you close
292
+ the terminal.
293
+
294
+ ## License
295
+
296
+ MIT