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.
- tmuxpull-0.2.0/PKG-INFO +289 -0
- tmuxpull-0.2.0/README.md +262 -0
- tmuxpull-0.2.0/bin/rebase-all +875 -0
- tmuxpull-0.2.0/bin/rebase-all.py +1085 -0
- {tmuxpull-0.1.2 → tmuxpull-0.2.0}/pyproject.toml +2 -2
- tmuxpull-0.2.0/src/tmuxpull/__init__.py +1072 -0
- {tmuxpull-0.1.2 → tmuxpull-0.2.0}/tests/test_ignore.py +19 -5
- tmuxpull-0.2.0/tests/test_report.py +182 -0
- tmuxpull-0.2.0/tests/test_scenarios.py +544 -0
- tmuxpull-0.2.0/tests/test_zsh_parity.py +176 -0
- {tmuxpull-0.1.2 → tmuxpull-0.2.0}/uv.lock +1 -1
- tmuxpull-0.1.2/PKG-INFO +0 -210
- tmuxpull-0.1.2/README.md +0 -183
- tmuxpull-0.1.2/bin/rebase-all +0 -436
- tmuxpull-0.1.2/bin/rebase-all.py +0 -520
- tmuxpull-0.1.2/src/tmuxpull/__init__.py +0 -507
- {tmuxpull-0.1.2 → tmuxpull-0.2.0}/.gitignore +0 -0
- {tmuxpull-0.1.2 → tmuxpull-0.2.0}/LICENSE +0 -0
- {tmuxpull-0.1.2 → tmuxpull-0.2.0}/mise.toml +0 -0
- {tmuxpull-0.1.2 → tmuxpull-0.2.0}/scripts/gen_script.py +0 -0
- {tmuxpull-0.1.2 → tmuxpull-0.2.0}/src/tmuxpull/__main__.py +0 -0
- {tmuxpull-0.1.2 → tmuxpull-0.2.0}/tests/test_script_sync.py +0 -0
- {tmuxpull-0.1.2 → tmuxpull-0.2.0}/tests/test_tmuxpull.py +0 -0
tmuxpull-0.2.0/PKG-INFO
ADDED
|
@@ -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
|
tmuxpull-0.2.0/README.md
ADDED
|
@@ -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
|