tmuxpull 0.1.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.1.0/.gitignore +24 -0
- tmuxpull-0.1.0/LICENSE +21 -0
- tmuxpull-0.1.0/PKG-INFO +181 -0
- tmuxpull-0.1.0/README.md +154 -0
- tmuxpull-0.1.0/bin/rebase-all +199 -0
- tmuxpull-0.1.0/bin/rebase-all.py +359 -0
- tmuxpull-0.1.0/mise.toml +30 -0
- tmuxpull-0.1.0/pyproject.toml +68 -0
- tmuxpull-0.1.0/src/tmuxpull/__init__.py +370 -0
- tmuxpull-0.1.0/src/tmuxpull/__main__.py +6 -0
- tmuxpull-0.1.0/tests/test_tmuxpull.py +81 -0
- tmuxpull-0.1.0/uv.lock +23 -0
tmuxpull-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Henry Nguyen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
tmuxpull-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: tmuxpull
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Concurrent git pull --rebase --autostash across multiple repos with tmux attention windows
|
|
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
|
+
Concurrent `git pull --rebase --autostash` across multiple Git repositories with tmux integration.
|
|
31
|
+
|
|
32
|
+
## Quick Start
|
|
33
|
+
|
|
34
|
+
### Run instantly with curl (no install)
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# Python version — requires uv (https://docs.astral.sh/uv/)
|
|
38
|
+
curl -fsSL https://raw.githubusercontent.com/nguyengg/tmuxpull/main/bin/rebase-all.py | uv run - ~/Workspaces
|
|
39
|
+
|
|
40
|
+
# Zsh version — zero dependencies (just git + tmux)
|
|
41
|
+
curl -fsSL https://raw.githubusercontent.com/nguyengg/tmuxpull/main/bin/rebase-all | zsh -s -- ~/Workspaces
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Install with curl (one-liner)
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# Download the self-contained script to ~/.local/bin
|
|
48
|
+
curl -fsSL https://raw.githubusercontent.com/nguyengg/tmuxpull/main/bin/rebase-all.py -o ~/.local/bin/tmuxpull && chmod +x ~/.local/bin/tmuxpull
|
|
49
|
+
tmuxpull ~/Workspaces
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
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.
|
|
53
|
+
|
|
54
|
+
### Install from PyPI
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# Install via pip/uv (recommended)
|
|
58
|
+
pip install tmuxpull
|
|
59
|
+
tmuxpull ~/Workspaces
|
|
60
|
+
|
|
61
|
+
# Or install as a uv tool
|
|
62
|
+
uv tool install tmuxpull
|
|
63
|
+
tmuxpull ~/Workspaces
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Both scan for Git repos under the given directories, pull with rebase concurrently, print a summary per repo, and open tmux windows for repos that need your attention (conflicts, failures, etc.).
|
|
67
|
+
|
|
68
|
+
## Features
|
|
69
|
+
|
|
70
|
+
- **Concurrent execution** with configurable job limits
|
|
71
|
+
- **Smart repo discovery** with depth limits and noise filtering (skips `node_modules`, `.venv`, etc.)
|
|
72
|
+
- **Per-repo summaries** showing commits pulled and file change stats (Python version)
|
|
73
|
+
- **tmux integration** — opens windows for repos needing attention, landing on `git status`
|
|
74
|
+
- **Multiple modes**: attention-only (default), all repos, or no tmux
|
|
75
|
+
- **PEP 723 packaging** (Python) — zero-setup single file with dependencies declared inline
|
|
76
|
+
|
|
77
|
+
## Usage
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
tmuxpull [-d DEPTH] [-j JOBS] [--tmux {all,attn,off}]
|
|
81
|
+
[-s SESSION] [-v] [--dry-run] DIR [DIR ...]
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Options
|
|
85
|
+
|
|
86
|
+
- `-d, --max-depth N` — Directory search depth (default: 2)
|
|
87
|
+
- `-j, --jobs N` — Max concurrent rebases (default: min(8, 2×CPU))
|
|
88
|
+
- `--tmux {all,attn,off}` — tmux windows for: all repos, attention-only (default), or none
|
|
89
|
+
- `-s, --session NAME` — tmux session name (default: "rebase")
|
|
90
|
+
- `-v, --verbose` — Show commit subjects (-v = top 3, -vv = all)
|
|
91
|
+
- `--dry-run` — List repos that would be processed, then exit
|
|
92
|
+
|
|
93
|
+
### Examples
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
# Morning sync across your workspace
|
|
97
|
+
tmuxpull ~/Workspaces ~/Projects
|
|
98
|
+
|
|
99
|
+
# High concurrency, all repos get tmux windows
|
|
100
|
+
tmuxpull -j 16 --tmux all ~/Code
|
|
101
|
+
|
|
102
|
+
# Just print what would happen
|
|
103
|
+
tmuxpull --dry-run ~/Projects
|
|
104
|
+
|
|
105
|
+
# Verbose output showing commit messages
|
|
106
|
+
tmuxpull -v ~/Workspaces
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Output
|
|
110
|
+
|
|
111
|
+
Per-repo summary lines:
|
|
112
|
+
```
|
|
113
|
+
my-project ✓ 3 commits 8 files changed, 213 insertions(+), 41 deletions(-)
|
|
114
|
+
other-repo · up to date
|
|
115
|
+
broken-thing ✗ FAIL: could not apply autostash
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Failed repos open tmux windows in the "rebase" session (or `-s NAME`), landing on `git status` so you see what's broken. Attach with `tmux attach -t rebase`.
|
|
119
|
+
|
|
120
|
+
## Two Versions
|
|
121
|
+
|
|
122
|
+
### `bin/rebase-all.py` (Recommended)
|
|
123
|
+
|
|
124
|
+
- **Standard Python package** with proper console script entry point
|
|
125
|
+
- Rich per-repo summaries with git log output and diffstat
|
|
126
|
+
- Better error handling and progress reporting
|
|
127
|
+
- Structured data model for repo state
|
|
128
|
+
|
|
129
|
+
**Requirements**: Python 3.11+, tmux, git
|
|
130
|
+
|
|
131
|
+
### `bin/rebase-all` (Fallback)
|
|
132
|
+
|
|
133
|
+
- **Pure Zsh** — no Python dependencies
|
|
134
|
+
- Basic summaries (commit count only, no diffstat)
|
|
135
|
+
- Simpler concurrency model with job control
|
|
136
|
+
|
|
137
|
+
**Requirements**: Zsh, tmux, git
|
|
138
|
+
|
|
139
|
+
## Installation
|
|
140
|
+
|
|
141
|
+
### From PyPI (Recommended)
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
# Install globally
|
|
145
|
+
pip install tmuxpull
|
|
146
|
+
|
|
147
|
+
# Or as a uv tool (isolated)
|
|
148
|
+
uv tool install tmuxpull
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### From Source
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
# Clone and install
|
|
155
|
+
git clone https://github.com/nguyengg/tmuxpull.git
|
|
156
|
+
cd tmuxpull
|
|
157
|
+
pip install .
|
|
158
|
+
|
|
159
|
+
# Or for development
|
|
160
|
+
uv sync --dev
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Zsh Fallback
|
|
164
|
+
|
|
165
|
+
For machines without Python, use the dependency-free Zsh script:
|
|
166
|
+
```bash
|
|
167
|
+
chmod +x bin/rebase-all
|
|
168
|
+
ln -s $PWD/bin/rebase-all ~/.local/bin/
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Design
|
|
172
|
+
|
|
173
|
+
Finds Git repos by walking the filesystem looking for `.git` directories, up to a configurable depth. Prunes common noise directories (`node_modules`, build artifacts, Python venvs) to avoid slow traversals.
|
|
174
|
+
|
|
175
|
+
Rebases run concurrently via `asyncio` (Python) or Zsh job control, capped at a reasonable limit to avoid overwhelming git servers. Each repo is isolated — failures don't stop other repos.
|
|
176
|
+
|
|
177
|
+
The tmux integration is the key workflow piece: clean repos just print their summary and disappear, while repos needing intervention (conflict resolution, stash conflicts, etc.) open interactive windows where you can fix things. `tmux attach -t rebase` becomes your "work queue" for the morning.
|
|
178
|
+
|
|
179
|
+
## License
|
|
180
|
+
|
|
181
|
+
MIT
|
tmuxpull-0.1.0/README.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# tmuxpull
|
|
2
|
+
|
|
3
|
+
Concurrent `git pull --rebase --autostash` across multiple Git repositories with tmux integration.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
### Run instantly with curl (no install)
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Python version — requires uv (https://docs.astral.sh/uv/)
|
|
11
|
+
curl -fsSL https://raw.githubusercontent.com/nguyengg/tmuxpull/main/bin/rebase-all.py | uv run - ~/Workspaces
|
|
12
|
+
|
|
13
|
+
# Zsh version — zero dependencies (just git + tmux)
|
|
14
|
+
curl -fsSL https://raw.githubusercontent.com/nguyengg/tmuxpull/main/bin/rebase-all | zsh -s -- ~/Workspaces
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Install with curl (one-liner)
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# Download the self-contained script to ~/.local/bin
|
|
21
|
+
curl -fsSL https://raw.githubusercontent.com/nguyengg/tmuxpull/main/bin/rebase-all.py -o ~/.local/bin/tmuxpull && chmod +x ~/.local/bin/tmuxpull
|
|
22
|
+
tmuxpull ~/Workspaces
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
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.
|
|
26
|
+
|
|
27
|
+
### Install from PyPI
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# Install via pip/uv (recommended)
|
|
31
|
+
pip install tmuxpull
|
|
32
|
+
tmuxpull ~/Workspaces
|
|
33
|
+
|
|
34
|
+
# Or install as a uv tool
|
|
35
|
+
uv tool install tmuxpull
|
|
36
|
+
tmuxpull ~/Workspaces
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Both scan for Git repos under the given directories, pull with rebase concurrently, print a summary per repo, and open tmux windows for repos that need your attention (conflicts, failures, etc.).
|
|
40
|
+
|
|
41
|
+
## Features
|
|
42
|
+
|
|
43
|
+
- **Concurrent execution** with configurable job limits
|
|
44
|
+
- **Smart repo discovery** with depth limits and noise filtering (skips `node_modules`, `.venv`, etc.)
|
|
45
|
+
- **Per-repo summaries** showing commits pulled and file change stats (Python version)
|
|
46
|
+
- **tmux integration** — opens windows for repos needing attention, landing on `git status`
|
|
47
|
+
- **Multiple modes**: attention-only (default), all repos, or no tmux
|
|
48
|
+
- **PEP 723 packaging** (Python) — zero-setup single file with dependencies declared inline
|
|
49
|
+
|
|
50
|
+
## Usage
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
tmuxpull [-d DEPTH] [-j JOBS] [--tmux {all,attn,off}]
|
|
54
|
+
[-s SESSION] [-v] [--dry-run] DIR [DIR ...]
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Options
|
|
58
|
+
|
|
59
|
+
- `-d, --max-depth N` — Directory search depth (default: 2)
|
|
60
|
+
- `-j, --jobs N` — Max concurrent rebases (default: min(8, 2×CPU))
|
|
61
|
+
- `--tmux {all,attn,off}` — tmux windows for: all repos, attention-only (default), or none
|
|
62
|
+
- `-s, --session NAME` — tmux session name (default: "rebase")
|
|
63
|
+
- `-v, --verbose` — Show commit subjects (-v = top 3, -vv = all)
|
|
64
|
+
- `--dry-run` — List repos that would be processed, then exit
|
|
65
|
+
|
|
66
|
+
### Examples
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# Morning sync across your workspace
|
|
70
|
+
tmuxpull ~/Workspaces ~/Projects
|
|
71
|
+
|
|
72
|
+
# High concurrency, all repos get tmux windows
|
|
73
|
+
tmuxpull -j 16 --tmux all ~/Code
|
|
74
|
+
|
|
75
|
+
# Just print what would happen
|
|
76
|
+
tmuxpull --dry-run ~/Projects
|
|
77
|
+
|
|
78
|
+
# Verbose output showing commit messages
|
|
79
|
+
tmuxpull -v ~/Workspaces
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Output
|
|
83
|
+
|
|
84
|
+
Per-repo summary lines:
|
|
85
|
+
```
|
|
86
|
+
my-project ✓ 3 commits 8 files changed, 213 insertions(+), 41 deletions(-)
|
|
87
|
+
other-repo · up to date
|
|
88
|
+
broken-thing ✗ FAIL: could not apply autostash
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Failed repos open tmux windows in the "rebase" session (or `-s NAME`), landing on `git status` so you see what's broken. Attach with `tmux attach -t rebase`.
|
|
92
|
+
|
|
93
|
+
## Two Versions
|
|
94
|
+
|
|
95
|
+
### `bin/rebase-all.py` (Recommended)
|
|
96
|
+
|
|
97
|
+
- **Standard Python package** with proper console script entry point
|
|
98
|
+
- Rich per-repo summaries with git log output and diffstat
|
|
99
|
+
- Better error handling and progress reporting
|
|
100
|
+
- Structured data model for repo state
|
|
101
|
+
|
|
102
|
+
**Requirements**: Python 3.11+, tmux, git
|
|
103
|
+
|
|
104
|
+
### `bin/rebase-all` (Fallback)
|
|
105
|
+
|
|
106
|
+
- **Pure Zsh** — no Python dependencies
|
|
107
|
+
- Basic summaries (commit count only, no diffstat)
|
|
108
|
+
- Simpler concurrency model with job control
|
|
109
|
+
|
|
110
|
+
**Requirements**: Zsh, tmux, git
|
|
111
|
+
|
|
112
|
+
## Installation
|
|
113
|
+
|
|
114
|
+
### From PyPI (Recommended)
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
# Install globally
|
|
118
|
+
pip install tmuxpull
|
|
119
|
+
|
|
120
|
+
# Or as a uv tool (isolated)
|
|
121
|
+
uv tool install tmuxpull
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### From Source
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
# Clone and install
|
|
128
|
+
git clone https://github.com/nguyengg/tmuxpull.git
|
|
129
|
+
cd tmuxpull
|
|
130
|
+
pip install .
|
|
131
|
+
|
|
132
|
+
# Or for development
|
|
133
|
+
uv sync --dev
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Zsh Fallback
|
|
137
|
+
|
|
138
|
+
For machines without Python, use the dependency-free Zsh script:
|
|
139
|
+
```bash
|
|
140
|
+
chmod +x bin/rebase-all
|
|
141
|
+
ln -s $PWD/bin/rebase-all ~/.local/bin/
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Design
|
|
145
|
+
|
|
146
|
+
Finds Git repos by walking the filesystem looking for `.git` directories, up to a configurable depth. Prunes common noise directories (`node_modules`, build artifacts, Python venvs) to avoid slow traversals.
|
|
147
|
+
|
|
148
|
+
Rebases run concurrently via `asyncio` (Python) or Zsh job control, capped at a reasonable limit to avoid overwhelming git servers. Each repo is isolated — failures don't stop other repos.
|
|
149
|
+
|
|
150
|
+
The tmux integration is the key workflow piece: clean repos just print their summary and disappear, while repos needing intervention (conflict resolution, stash conflicts, etc.) open interactive windows where you can fix things. `tmux attach -t rebase` becomes your "work queue" for the morning.
|
|
151
|
+
|
|
152
|
+
## License
|
|
153
|
+
|
|
154
|
+
MIT
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/bin/zsh
|
|
2
|
+
# rebase-all -- concurrent git pull --rebase --autostash with tmux attention windows
|
|
3
|
+
#
|
|
4
|
+
# Zsh fallback for machines without Python/uv. For the full-featured version,
|
|
5
|
+
# use rebase-all.py (PEP 723 single-file with libtmux, git log summaries, etc.).
|
|
6
|
+
#
|
|
7
|
+
# Usage: rebase-all [-j JOBS] [-d DEPTH] [-s SESSION] [--dry-run] DIR [DIR...]
|
|
8
|
+
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
|
|
11
|
+
# defaults
|
|
12
|
+
max_depth=2
|
|
13
|
+
jobs=8
|
|
14
|
+
session="rebase"
|
|
15
|
+
dry_run=false
|
|
16
|
+
tmux_mode="attn" # all | attn | off
|
|
17
|
+
|
|
18
|
+
usage() {
|
|
19
|
+
cat >&2 <<'EOF'
|
|
20
|
+
Usage: rebase-all [-j JOBS] [-d DEPTH] [-s SESSION] [--dry-run] DIR [DIR...]
|
|
21
|
+
|
|
22
|
+
Options:
|
|
23
|
+
-j JOBS Max concurrent rebases (default: 8)
|
|
24
|
+
-d DEPTH Directory search depth (default: 2)
|
|
25
|
+
-s SESSION tmux session name (default: rebase)
|
|
26
|
+
--tmux MODE Open tmux windows for: all repos, attn (default), or off
|
|
27
|
+
--dry-run List repos and exit
|
|
28
|
+
-h, --help Show this help
|
|
29
|
+
|
|
30
|
+
Examples:
|
|
31
|
+
rebase-all ~/Workspaces
|
|
32
|
+
rebase-all -j 4 --tmux all ~/Projects ~/Work
|
|
33
|
+
EOF
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
while [[ $# -gt 0 ]]; do
|
|
37
|
+
case $1 in
|
|
38
|
+
-j) jobs=$2; shift 2 ;;
|
|
39
|
+
-d) max_depth=$2; shift 2 ;;
|
|
40
|
+
-s) session=$2; shift 2 ;;
|
|
41
|
+
--tmux) tmux_mode=$2; shift 2 ;;
|
|
42
|
+
--dry-run) dry_run=true; shift ;;
|
|
43
|
+
-h|--help) usage; exit 0 ;;
|
|
44
|
+
-*) echo "Unknown option: $1" >&2; usage; exit 1 ;;
|
|
45
|
+
*) break ;;
|
|
46
|
+
esac
|
|
47
|
+
done
|
|
48
|
+
|
|
49
|
+
if [[ $# -eq 0 ]]; then
|
|
50
|
+
echo "Error: no directories specified" >&2
|
|
51
|
+
usage
|
|
52
|
+
exit 1
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
# find repos
|
|
56
|
+
repos=()
|
|
57
|
+
for root in "$@"; do
|
|
58
|
+
if [[ ! -d "$root" ]]; then
|
|
59
|
+
echo "skip: $root is not a directory" >&2
|
|
60
|
+
continue
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
# find .git dirs, respecting max_depth and skipping noise dirs
|
|
64
|
+
while IFS= read -r -d '' gitdir; do
|
|
65
|
+
repo="${gitdir%/.git}"
|
|
66
|
+
repos+=("$repo")
|
|
67
|
+
done < <(find "$root" -mindepth 1 -maxdepth $((max_depth + 1)) \
|
|
68
|
+
-name ".git" -type d \
|
|
69
|
+
-not -path "*/node_modules/*" \
|
|
70
|
+
-not -path "*/.venv/*" \
|
|
71
|
+
-not -path "*/venv/*" \
|
|
72
|
+
-not -path "*/target/*" \
|
|
73
|
+
-not -path "*/build/*" \
|
|
74
|
+
-not -path "*/dist/*" \
|
|
75
|
+
-not -path "*/__pycache__/*" \
|
|
76
|
+
-print0 2>/dev/null)
|
|
77
|
+
done
|
|
78
|
+
|
|
79
|
+
if [[ ${#repos[@]} -eq 0 ]]; then
|
|
80
|
+
echo "no git repos found" >&2
|
|
81
|
+
exit 1
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
if [[ "$dry_run" == "true" ]]; then
|
|
85
|
+
printf '%s\n' "${repos[@]}"
|
|
86
|
+
exit 0
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
echo "rebasing ${#repos[@]} repo(s) (jobs=$jobs)..." >&2
|
|
90
|
+
|
|
91
|
+
# set up temp dir for per-repo status files
|
|
92
|
+
tmpdir=$(mktemp -d)
|
|
93
|
+
trap "rm -rf $tmpdir" EXIT
|
|
94
|
+
|
|
95
|
+
# concurrent rebase function
|
|
96
|
+
rebase_one() {
|
|
97
|
+
local repo=$1
|
|
98
|
+
local slug=${repo//\//_} # sanitize path for filename
|
|
99
|
+
local old_sha new_sha
|
|
100
|
+
|
|
101
|
+
cd "$repo"
|
|
102
|
+
old_sha=$(git rev-parse HEAD 2>/dev/null || echo "")
|
|
103
|
+
|
|
104
|
+
if git pull --rebase --autostash >"$tmpdir/$slug.out" 2>"$tmpdir/$slug.err"; then
|
|
105
|
+
echo "0" > "$tmpdir/$slug.status"
|
|
106
|
+
new_sha=$(git rev-parse HEAD 2>/dev/null || echo "$old_sha")
|
|
107
|
+
|
|
108
|
+
# simple change detection
|
|
109
|
+
if [[ -n "$old_sha" && "$old_sha" != "$new_sha" ]]; then
|
|
110
|
+
local count=$(git rev-list --count "$old_sha..$new_sha" 2>/dev/null || echo "0")
|
|
111
|
+
echo "✓ $count commit(s)" > "$tmpdir/$slug.summary"
|
|
112
|
+
else
|
|
113
|
+
echo "· up to date" > "$tmpdir/$slug.summary"
|
|
114
|
+
fi
|
|
115
|
+
else
|
|
116
|
+
local rc=$?
|
|
117
|
+
echo "$rc" > "$tmpdir/$slug.status"
|
|
118
|
+
local err_tail=$(tail -n1 "$tmpdir/$slug.err" 2>/dev/null || echo "exit $rc")
|
|
119
|
+
echo "✗ FAIL: $err_tail" > "$tmpdir/$slug.summary"
|
|
120
|
+
fi
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# fan out work with job control
|
|
124
|
+
pids=()
|
|
125
|
+
for repo in "${repos[@]}"; do
|
|
126
|
+
rebase_one "$repo" &
|
|
127
|
+
pids+=($!)
|
|
128
|
+
|
|
129
|
+
# wait for a slot if we're at the job limit
|
|
130
|
+
if (( ${#pids[@]} >= jobs )); then
|
|
131
|
+
wait -n # wait for next job to complete (zsh 5.9+, bash 4.3+)
|
|
132
|
+
# rebuild pids array without completed jobs
|
|
133
|
+
local new_pids=()
|
|
134
|
+
for pid in "${pids[@]}"; do
|
|
135
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
136
|
+
new_pids+=("$pid")
|
|
137
|
+
fi
|
|
138
|
+
done
|
|
139
|
+
pids=("${new_pids[@]}")
|
|
140
|
+
fi
|
|
141
|
+
done
|
|
142
|
+
|
|
143
|
+
# wait for remaining jobs
|
|
144
|
+
wait
|
|
145
|
+
|
|
146
|
+
# collect results and print report
|
|
147
|
+
fails=0
|
|
148
|
+
needs_attention=()
|
|
149
|
+
|
|
150
|
+
# find max repo name length for alignment
|
|
151
|
+
max_width=0
|
|
152
|
+
for repo in "${repos[@]}"; do
|
|
153
|
+
local name=${repo##*/} # basename
|
|
154
|
+
(( ${#name} > max_width )) && max_width=${#name}
|
|
155
|
+
done
|
|
156
|
+
|
|
157
|
+
for repo in "${repos[@]}"; do
|
|
158
|
+
local slug=${repo//\//_}
|
|
159
|
+
local name=${repo##*/}
|
|
160
|
+
local status=$(cat "$tmpdir/$slug.status" 2>/dev/null || echo "999")
|
|
161
|
+
local summary=$(cat "$tmpdir/$slug.summary" 2>/dev/null || echo "unknown")
|
|
162
|
+
|
|
163
|
+
printf "%-${max_width}s %s\n" "$name" "$summary"
|
|
164
|
+
|
|
165
|
+
if [[ "$status" != "0" ]]; then
|
|
166
|
+
(( fails++ ))
|
|
167
|
+
needs_attention+=("$repo")
|
|
168
|
+
fi
|
|
169
|
+
done
|
|
170
|
+
|
|
171
|
+
# open tmux windows for repos needing attention
|
|
172
|
+
if [[ "$tmux_mode" != "off" ]] && command -v tmux >/dev/null; then
|
|
173
|
+
opened=0
|
|
174
|
+
target_repos=()
|
|
175
|
+
|
|
176
|
+
case "$tmux_mode" in
|
|
177
|
+
all) target_repos=("${repos[@]}") ;;
|
|
178
|
+
attn) target_repos=("${needs_attention[@]}") ;;
|
|
179
|
+
esac
|
|
180
|
+
|
|
181
|
+
for repo in "${target_repos[@]}"; do
|
|
182
|
+
local win_name=${repo##*/} # basename
|
|
183
|
+
local clean_name=${win_name//[: .]/_} # sanitize for tmux
|
|
184
|
+
|
|
185
|
+
if tmux has-session -t "$session" 2>/dev/null; then
|
|
186
|
+
tmux new-window -t "$session:" -n "$clean_name" -c "$repo" \; send-keys 'git status' Enter
|
|
187
|
+
else
|
|
188
|
+
tmux new-session -d -s "$session" -n "$clean_name" -c "$repo" \; send-keys 'git status' Enter
|
|
189
|
+
fi
|
|
190
|
+
(( opened++ ))
|
|
191
|
+
done
|
|
192
|
+
|
|
193
|
+
if (( opened > 0 )); then
|
|
194
|
+
echo >&2
|
|
195
|
+
echo "$opened tmux window(s) in session '$session'. attach: tmux attach -t $session" >&2
|
|
196
|
+
fi
|
|
197
|
+
fi
|
|
198
|
+
|
|
199
|
+
exit $((fails > 0 ? 1 : 0))
|