tmuxpull 0.1.2__tar.gz → 0.2.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.
@@ -0,0 +1,289 @@
1
+ Metadata-Version: 2.5
2
+ Name: tmuxpull
3
+ Version: 0.2.0
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
+
125
+ ### Per-repo git config
126
+
127
+ ```bash
128
+ # Skip a repo entirely (survives every run until unset) — e.g. a broken tip:
129
+ git -C ~/github.com/kirodotdev/KiroCrew config tmuxpull.ignore true
130
+ git -C ~/github.com/kirodotdev/KiroCrew config --unset tmuxpull.ignore
131
+
132
+ # Override the detected default branch:
133
+ git -C ~/github.com/acme/legacy config tmuxpull.defaultBranch release
134
+ ```
135
+
136
+ Default-branch detection order: `tmuxpull.defaultBranch`, then
137
+ `refs/remotes/<remote>/HEAD`, then a probe of `main` / `master` / `trunk`.
138
+ The remote is `origin` when present, otherwise the first one configured.
139
+
140
+ ### Examples
141
+
142
+ ```bash
143
+ # Morning sync across your workspace
144
+ tmuxpull ~/Workspaces ~/Projects
145
+
146
+ # High concurrency
147
+ tmuxpull -j 16 ~/Code
148
+
149
+ # Just print what would happen
150
+ tmuxpull --dry-run ~/Projects
151
+
152
+ # Verbose output showing incoming commit messages
153
+ tmuxpull -v ~/Workspaces
154
+ ```
155
+
156
+ ## Output
157
+
158
+ Per-repo summary lines, printed as each repo finishes:
159
+
160
+ ```
161
+ on-main-ff + 1 commit 1 file changed, 1 insertion(+)
162
+ on-main-replay ~ rebased 1 onto main, pulled 1 commit 1 file changed, 1 insertion(+)
163
+ feat-unpushed ~ rebased 1 onto main, main +1 1 file changed, 1 insertion(+)
164
+ up-to-date-repo = up to date
165
+ feat-pushed ! DIVERGES from main: 1 file would conflict (main +1)
166
+ feat-conflict ! CONFLICT: rebase stopped, 1 file -- resolve here (main +1)
167
+ broken-remote ! FAIL: could not read from remote repository
168
+ quiet - ignored (git config tmuxpull.ignore)
169
+ no-remote - no remote
170
+ ```
171
+
172
+ `!` lines go to stderr and mean you have to act; `+`/`~`/`=`/`-` go to stdout.
173
+ The exit code is 1 when anything needs attention.
174
+
175
+ ### The result list outlives the handoff
176
+
177
+ Choosing a tmux session used to cost you the summary — the process was replaced
178
+ by tmux and nothing survived a detach. Now three things persist it:
179
+
180
+ 1. **An attention block on stderr** before tmux takes the terminal, so it stays
181
+ in the launching shell's scrollback:
182
+
183
+ ```
184
+ 2 of 9 repos need attention:
185
+ feat-pushed ! DIVERGES from main: 1 file would conflict (main +1)
186
+ tmux attach -t Projects/feat-pushed
187
+ feat-conflict ! CONFLICT: rebase stopped, 1 file -- resolve here (main +1)
188
+ tmux attach -t Projects/feat-conflict
189
+ full report: ~/.local/state/tmuxpull/last-run.log
190
+ ```
191
+
192
+ 2. **A report file**, always written, whether or not you pick a session — every
193
+ repo, attention first, greppable by state (`grep '^\[conflict' last-run.log`).
194
+ It survives closing the terminal entirely.
195
+
196
+ 3. **A picker you come back to.** tmux runs as a child process, so detaching
197
+ returns you to the picker with the list reprinted: fix one repo, detach, pick
198
+ the next, `q` when you're done. Inside an existing tmux client this becomes
199
+ `tmux switch-client` (nested `attach` is refused) and control does not return.
200
+
201
+ When output is piped or redirected the picker is skipped and the full
202
+ `tmux attach -t <name>` list is printed instead, so scripts and CI still work.
203
+
204
+ ## Worktrees
205
+
206
+ A repo and its worktrees (`repo` + `repo.wt/feat`) are separate directories, so a
207
+ scan finds both — but they share **one object store and one ref namespace**.
208
+ tmuxpull groups repos by `git rev-parse --git-common-dir` and serializes each
209
+ group while running different groups in parallel, so concurrent jobs can't race
210
+ on ref locks or `FETCH_HEAD`.
211
+
212
+ ## Two Versions
213
+
214
+ ### `src/tmuxpull/` + `bin/rebase-all.py` (single source of truth)
215
+
216
+ The PyPI package (`src/tmuxpull/__init__.py`) is the canonical implementation.
217
+ `bin/rebase-all.py` — the standalone PEP 723 script the curl one-liners use — is
218
+ **generated from it** (`python scripts/gen_script.py`, or `mise run gen-script`);
219
+ a test fails if the two drift.
220
+
221
+ **Requirements**: Python 3.11+, git 2.38+ (for `merge-tree --write-tree`), tmux,
222
+ plus [uv](https://docs.astral.sh/uv/) for the standalone script
223
+
224
+ ### `bin/rebase-all` (Fallback)
225
+
226
+ - **Pure Zsh** — no Python dependencies
227
+ - Same branch handling, same report file, same attention block, same picker
228
+ - Same options, including `--rebase-pushed`, `--log` / `--no-log`, and both
229
+ `git config tmuxpull.*` knobs
230
+ - `tests/test_zsh_parity.py` builds one fixture per implementation and asserts
231
+ their summary lines are identical, so the two cannot drift silently
232
+
233
+ The one deliberate difference: summaries print in input order at the end of the
234
+ run (vs. Python's live completion-order `[n/N]` counter). If the two ever
235
+ disagree otherwise, the Python version is the source of truth.
236
+
237
+ **Requirements**: Zsh, git 2.38+, tmux
238
+
239
+ ## Installation
240
+
241
+ ### From PyPI (Recommended)
242
+
243
+ ```bash
244
+ # Install globally
245
+ pip install tmuxpull
246
+
247
+ # Or as a uv tool (isolated)
248
+ uv tool install tmuxpull
249
+ ```
250
+
251
+ ### From Source
252
+
253
+ ```bash
254
+ # Clone and install
255
+ git clone https://github.com/nguyengg/tmuxpull.git
256
+ cd tmuxpull
257
+ pip install .
258
+
259
+ # Or for development
260
+ uv sync --dev
261
+ ```
262
+
263
+ ### Zsh Fallback
264
+
265
+ For machines without Python, use the dependency-free Zsh script:
266
+ ```bash
267
+ chmod +x bin/rebase-all
268
+ ln -s $PWD/bin/rebase-all ~/.local/bin/
269
+ ```
270
+
271
+ ## Design
272
+
273
+ Finds Git repos by walking the filesystem looking for `.git` entries, up to a
274
+ configurable depth. Prunes common noise directories (`node_modules`, build
275
+ artifacts, Python venvs) to avoid slow traversals.
276
+
277
+ Work runs concurrently via `asyncio` (Python) or Zsh job control, capped to
278
+ avoid overwhelming git servers, and serialized per shared object store. Each
279
+ repo is isolated — one failure doesn't stop the others.
280
+
281
+ The tmux integration is the key workflow piece: clean repos just print their
282
+ summary and disappear, while repos needing intervention (conflict resolution,
283
+ diverged branches) open interactive sessions where you fix things. The picker is
284
+ your morning work queue, and the report file is what's left of it after you close
285
+ the terminal.
286
+
287
+ ## License
288
+
289
+ MIT
@@ -0,0 +1,262 @@
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
+
98
+ ### Per-repo git config
99
+
100
+ ```bash
101
+ # Skip a repo entirely (survives every run until unset) — e.g. a broken tip:
102
+ git -C ~/github.com/kirodotdev/KiroCrew config tmuxpull.ignore true
103
+ git -C ~/github.com/kirodotdev/KiroCrew config --unset tmuxpull.ignore
104
+
105
+ # Override the detected default branch:
106
+ git -C ~/github.com/acme/legacy config tmuxpull.defaultBranch release
107
+ ```
108
+
109
+ Default-branch detection order: `tmuxpull.defaultBranch`, then
110
+ `refs/remotes/<remote>/HEAD`, then a probe of `main` / `master` / `trunk`.
111
+ The remote is `origin` when present, otherwise the first one configured.
112
+
113
+ ### Examples
114
+
115
+ ```bash
116
+ # Morning sync across your workspace
117
+ tmuxpull ~/Workspaces ~/Projects
118
+
119
+ # High concurrency
120
+ tmuxpull -j 16 ~/Code
121
+
122
+ # Just print what would happen
123
+ tmuxpull --dry-run ~/Projects
124
+
125
+ # Verbose output showing incoming commit messages
126
+ tmuxpull -v ~/Workspaces
127
+ ```
128
+
129
+ ## Output
130
+
131
+ Per-repo summary lines, printed as each repo finishes:
132
+
133
+ ```
134
+ on-main-ff + 1 commit 1 file changed, 1 insertion(+)
135
+ on-main-replay ~ rebased 1 onto main, pulled 1 commit 1 file changed, 1 insertion(+)
136
+ feat-unpushed ~ rebased 1 onto main, main +1 1 file changed, 1 insertion(+)
137
+ up-to-date-repo = up to date
138
+ feat-pushed ! DIVERGES from main: 1 file would conflict (main +1)
139
+ feat-conflict ! CONFLICT: rebase stopped, 1 file -- resolve here (main +1)
140
+ broken-remote ! FAIL: could not read from remote repository
141
+ quiet - ignored (git config tmuxpull.ignore)
142
+ no-remote - no remote
143
+ ```
144
+
145
+ `!` lines go to stderr and mean you have to act; `+`/`~`/`=`/`-` go to stdout.
146
+ The exit code is 1 when anything needs attention.
147
+
148
+ ### The result list outlives the handoff
149
+
150
+ Choosing a tmux session used to cost you the summary — the process was replaced
151
+ by tmux and nothing survived a detach. Now three things persist it:
152
+
153
+ 1. **An attention block on stderr** before tmux takes the terminal, so it stays
154
+ in the launching shell's scrollback:
155
+
156
+ ```
157
+ 2 of 9 repos need attention:
158
+ feat-pushed ! DIVERGES from main: 1 file would conflict (main +1)
159
+ tmux attach -t Projects/feat-pushed
160
+ feat-conflict ! CONFLICT: rebase stopped, 1 file -- resolve here (main +1)
161
+ tmux attach -t Projects/feat-conflict
162
+ full report: ~/.local/state/tmuxpull/last-run.log
163
+ ```
164
+
165
+ 2. **A report file**, always written, whether or not you pick a session — every
166
+ repo, attention first, greppable by state (`grep '^\[conflict' last-run.log`).
167
+ It survives closing the terminal entirely.
168
+
169
+ 3. **A picker you come back to.** tmux runs as a child process, so detaching
170
+ returns you to the picker with the list reprinted: fix one repo, detach, pick
171
+ the next, `q` when you're done. Inside an existing tmux client this becomes
172
+ `tmux switch-client` (nested `attach` is refused) and control does not return.
173
+
174
+ When output is piped or redirected the picker is skipped and the full
175
+ `tmux attach -t <name>` list is printed instead, so scripts and CI still work.
176
+
177
+ ## Worktrees
178
+
179
+ A repo and its worktrees (`repo` + `repo.wt/feat`) are separate directories, so a
180
+ scan finds both — but they share **one object store and one ref namespace**.
181
+ tmuxpull groups repos by `git rev-parse --git-common-dir` and serializes each
182
+ group while running different groups in parallel, so concurrent jobs can't race
183
+ on ref locks or `FETCH_HEAD`.
184
+
185
+ ## Two Versions
186
+
187
+ ### `src/tmuxpull/` + `bin/rebase-all.py` (single source of truth)
188
+
189
+ The PyPI package (`src/tmuxpull/__init__.py`) is the canonical implementation.
190
+ `bin/rebase-all.py` — the standalone PEP 723 script the curl one-liners use — is
191
+ **generated from it** (`python scripts/gen_script.py`, or `mise run gen-script`);
192
+ a test fails if the two drift.
193
+
194
+ **Requirements**: Python 3.11+, git 2.38+ (for `merge-tree --write-tree`), tmux,
195
+ plus [uv](https://docs.astral.sh/uv/) for the standalone script
196
+
197
+ ### `bin/rebase-all` (Fallback)
198
+
199
+ - **Pure Zsh** — no Python dependencies
200
+ - Same branch handling, same report file, same attention block, same picker
201
+ - Same options, including `--rebase-pushed`, `--log` / `--no-log`, and both
202
+ `git config tmuxpull.*` knobs
203
+ - `tests/test_zsh_parity.py` builds one fixture per implementation and asserts
204
+ their summary lines are identical, so the two cannot drift silently
205
+
206
+ The one deliberate difference: summaries print in input order at the end of the
207
+ run (vs. Python's live completion-order `[n/N]` counter). If the two ever
208
+ disagree otherwise, the Python version is the source of truth.
209
+
210
+ **Requirements**: Zsh, git 2.38+, tmux
211
+
212
+ ## Installation
213
+
214
+ ### From PyPI (Recommended)
215
+
216
+ ```bash
217
+ # Install globally
218
+ pip install tmuxpull
219
+
220
+ # Or as a uv tool (isolated)
221
+ uv tool install tmuxpull
222
+ ```
223
+
224
+ ### From Source
225
+
226
+ ```bash
227
+ # Clone and install
228
+ git clone https://github.com/nguyengg/tmuxpull.git
229
+ cd tmuxpull
230
+ pip install .
231
+
232
+ # Or for development
233
+ uv sync --dev
234
+ ```
235
+
236
+ ### Zsh Fallback
237
+
238
+ For machines without Python, use the dependency-free Zsh script:
239
+ ```bash
240
+ chmod +x bin/rebase-all
241
+ ln -s $PWD/bin/rebase-all ~/.local/bin/
242
+ ```
243
+
244
+ ## Design
245
+
246
+ Finds Git repos by walking the filesystem looking for `.git` entries, up to a
247
+ configurable depth. Prunes common noise directories (`node_modules`, build
248
+ artifacts, Python venvs) to avoid slow traversals.
249
+
250
+ Work runs concurrently via `asyncio` (Python) or Zsh job control, capped to
251
+ avoid overwhelming git servers, and serialized per shared object store. Each
252
+ repo is isolated — one failure doesn't stop the others.
253
+
254
+ The tmux integration is the key workflow piece: clean repos just print their
255
+ summary and disappear, while repos needing intervention (conflict resolution,
256
+ diverged branches) open interactive sessions where you fix things. The picker is
257
+ your morning work queue, and the report file is what's left of it after you close
258
+ the terminal.
259
+
260
+ ## License
261
+
262
+ MIT