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.
@@ -0,0 +1,24 @@
1
+ # Python
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ *.pyd
6
+ .Python
7
+ .venv/
8
+ venv/
9
+ env/
10
+ .env
11
+ *.egg-info/
12
+
13
+ # Editors
14
+ .vscode/
15
+ .idea/
16
+ *.swp
17
+ *.swo
18
+ *~
19
+ .DS_Store
20
+
21
+ # Temporary
22
+ *.log
23
+ *.tmp
24
+ .cache/
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.
@@ -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
@@ -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))