@codebend3r/che 1.1.0 โ 1.2.1
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.
- package/README.md +58 -26
- package/bin/art.py +22 -20
- package/bin/branchsync.py +324 -0
- package/bin/che.py +208 -587
- package/bin/commands.py +219 -231
- package/bin/doctor.py +220 -0
- package/bin/drives/eject-all-drives.py +21 -38
- package/bin/drives/mount-all-drives.py +18 -62
- package/bin/drives/ping-nas.py +77 -105
- package/bin/files/compress-folders.py +7 -11
- package/bin/files/delete-by-ext.py +10 -14
- package/bin/files/delete-empty-folders.py +33 -37
- package/bin/files/delete-smb-files.py +12 -21
- package/bin/files/files-under-size.py +5 -11
- package/bin/files/find-largest-files.py +21 -25
- package/bin/files/list-permission.py +40 -0
- package/bin/files/make-alpha-dir.py +2 -0
- package/bin/git/all-actions.py +350 -253
- package/bin/git/checkout-my-branches.py +42 -63
- package/bin/git/clean-stale-branches.py +23 -48
- package/bin/git/prune-worktrees.py +12 -51
- package/bin/git/sync-all-branches.py +75 -297
- package/bin/git/update-local-branches.py +67 -73
- package/bin/gitutils.py +263 -0
- package/bin/install.py +393 -1182
- package/bin/machine.py +345 -0
- package/bin/media.py +127 -0
- package/bin/menu.py +437 -0
- package/bin/nas.py +102 -0
- package/bin/rcfile.py +380 -0
- package/bin/shellgen.py +503 -431
- package/bin/system/btop-launch.py +17 -13
- package/bin/system/update-brew.py +57 -68
- package/bin/tui.py +156 -153
- package/bin/utils.py +165 -237
- package/bin/video/delete-duplicate-videos.py +64 -62
- package/bin/video/detect-green-magenta-videos.py +105 -95
- package/bin/video/find-movie-by-year.py +3 -0
- package/bin/video/find-video-mkv-issues.py +48 -65
- package/bin/video/fix-codecs.py +25 -30
- package/bin/video/largest-tv-shows.py +60 -57
- package/bin/video/remove-metadata.py +3 -6
- package/bin/video/rename-video-file.py +74 -84
- package/bin/video/scan-videos-audio-language.py +16 -33
- package/bin/video/show-codecs.py +20 -24
- package/bin/video/validate-video-files.py +5 -8
- package/bin/video/video-list.py +17 -20
- package/package.json +4 -10
- package/shell/che.bash +33 -33
- package/shell/che.fish +7 -7
- package/shell/che.zsh +33 -33
- package/shell/completions/_che +19 -19
- package/shell/completions/che.bash +15 -15
- package/shell/completions/che.fish +34 -50
|
@@ -1,366 +1,144 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""๐
|
|
2
|
+
"""๐ Update every branch and worktree, keeping checkouts, locks and local work.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
makes main un-checkoutable and stale branches un-deletable.
|
|
9
|
-
|
|
10
|
-
Steps, in order:
|
|
11
|
-
1. git fetch --all --prune
|
|
12
|
-
2. Push every linked worktree's branch to origin
|
|
13
|
-
3. Remove each pushed + clean worktree, keeping its branch
|
|
14
|
-
4. Prune stale worktree admin records
|
|
15
|
-
5. Switch the main clone to the main branch
|
|
16
|
-
6. clean-stale-branches (delete branches whose upstream is gone)
|
|
17
|
-
7. update-local-branches (rebase every branch onto its upstream)
|
|
18
|
-
8. checkout-my-branches (check out your remote branches that aren't local)
|
|
19
|
-
|
|
20
|
-
A worktree is only collapsed when it is clean AND fully pushed. Anything else
|
|
21
|
-
is kept and reported with the reason.
|
|
4
|
+
Fetch once, rebase branches onto their upstream (or origin/<branch>), and track
|
|
5
|
+
new origin branches authored by you. Dirty tracked/untracked work is saved in
|
|
6
|
+
an exact recovery stash and restored with its index. Conflicts are reported;
|
|
7
|
+
independent branches continue. Nothing is pushed, hard reset or deleted.
|
|
22
8
|
|
|
23
9
|
Usage:
|
|
24
|
-
sync-all-branches.py [--dry-run] [--
|
|
25
|
-
[--author=EMAIL] [--limit=N]
|
|
10
|
+
sync-all-branches.py [--dry-run] [--author=EMAIL] [--limit=N]
|
|
26
11
|
|
|
27
12
|
Options:
|
|
28
|
-
--dry-run
|
|
29
|
-
--
|
|
30
|
-
--
|
|
31
|
-
the worktree is pushed and clean)
|
|
32
|
-
--author=EMAIL Passed through to checkout-my-branches
|
|
33
|
-
--limit=N Passed through to checkout-my-branches
|
|
13
|
+
--dry-run Preview cached refs only; no fetch or changes
|
|
14
|
+
--author=EMAIL Author of new origin branches (default: git config user.email)
|
|
15
|
+
--limit=N Recent origin refs to scan for new branches (default: 100)
|
|
34
16
|
--help Show help
|
|
35
17
|
|
|
36
18
|
Env:
|
|
37
|
-
DRY_RUN=true Same as --dry-run (used by the
|
|
19
|
+
DRY_RUN=true Same as --dry-run (used by the shell -dr wrapper)
|
|
38
20
|
"""
|
|
39
21
|
|
|
40
22
|
from __future__ import annotations
|
|
41
23
|
|
|
24
|
+
import argparse
|
|
25
|
+
import subprocess
|
|
42
26
|
import sys
|
|
43
27
|
from pathlib import Path
|
|
44
28
|
|
|
45
29
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
46
30
|
|
|
31
|
+
from branchsync import BranchSync
|
|
32
|
+
from gitutils import current_branch, fetch_remotes, for_each_ref, local_branches, skip_git_hooks
|
|
47
33
|
from utils import (
|
|
48
34
|
add_bool_flag,
|
|
49
|
-
branch_is_pushed,
|
|
50
35
|
build_parser,
|
|
51
36
|
env_bool,
|
|
52
|
-
|
|
37
|
+
heading,
|
|
53
38
|
info,
|
|
54
39
|
log,
|
|
55
|
-
|
|
56
|
-
note,
|
|
40
|
+
parse_count,
|
|
57
41
|
require_binary,
|
|
58
42
|
run,
|
|
59
43
|
run_cli,
|
|
60
44
|
run_output,
|
|
61
|
-
success,
|
|
62
45
|
warning,
|
|
63
|
-
worktree_branch_map,
|
|
64
|
-
worktree_is_clean,
|
|
65
46
|
)
|
|
66
47
|
|
|
67
|
-
__version__ = "
|
|
68
|
-
|
|
69
|
-
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
70
|
-
|
|
48
|
+
__version__ = "3.0.0"
|
|
71
49
|
|
|
72
|
-
def delegate(script: str, args: list[str], *, cwd: Path, dry_run: bool) -> None:
|
|
73
|
-
"""Run a sibling script in the main worktree, aborting the sync if it fails.
|
|
74
50
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
cwd=
|
|
85
|
-
env={"DRY_RUN": "true" if dry_run else "false"},
|
|
51
|
+
def track_new(sync: BranchSync, author: str, limit: int) -> list[str]:
|
|
52
|
+
tracked: list[str] = []
|
|
53
|
+
local = {branch for (branch,) in local_branches(cwd=sync.root)}
|
|
54
|
+
refs = for_each_ref(
|
|
55
|
+
"refs/remotes/origin/",
|
|
56
|
+
"authoremail:trim",
|
|
57
|
+
"refname:lstrip=3",
|
|
58
|
+
"symref",
|
|
59
|
+
sort="-committerdate",
|
|
60
|
+
cwd=sync.root,
|
|
86
61
|
)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def collapse_worktrees(
|
|
90
|
-
*,
|
|
91
|
-
main_wt: Path,
|
|
92
|
-
push: bool,
|
|
93
|
-
keep_locked: bool,
|
|
94
|
-
dry_run: bool,
|
|
95
|
-
) -> tuple[list[str], list[str], list[str]]:
|
|
96
|
-
"""Push, then remove, every linked worktree that is clean and fully pushed.
|
|
97
|
-
|
|
98
|
-
Returns ``(pushed, collapsed, kept)`` for the summary. ``kept`` entries
|
|
99
|
-
carry the reason, because "nothing happened" is the one outcome a user
|
|
100
|
-
needs explained.
|
|
101
|
-
"""
|
|
102
|
-
pushed: list[str] = []
|
|
103
|
-
collapsed: list[str] = []
|
|
104
|
-
kept: list[str] = []
|
|
105
|
-
|
|
106
|
-
info("๐ฟ Inspecting linked worktrees...")
|
|
107
|
-
|
|
108
|
-
# Snapshot the map first: removing a worktree mutates the list git reports.
|
|
109
|
-
worktrees = [
|
|
110
|
-
(branch, path)
|
|
111
|
-
for branch, path in worktree_branch_map(cwd=main_wt).items()
|
|
112
|
-
if path and Path(path) != main_wt
|
|
113
|
-
]
|
|
114
|
-
|
|
115
|
-
if not worktrees:
|
|
116
|
-
info(" โน๏ธ No linked worktrees with a branch checked out.") # noqa: RUF001
|
|
117
|
-
|
|
118
|
-
for branch, path in worktrees:
|
|
119
|
-
# A dirty worktree holds work that only exists there. Never touch it.
|
|
120
|
-
if not worktree_is_clean(path):
|
|
121
|
-
kept.append(f"{branch} ({path}) โ dirty")
|
|
122
|
-
warning(f" โญ๏ธ {branch} โ dirty, keeping {path}")
|
|
62
|
+
for email, branch, symref in refs[:limit]:
|
|
63
|
+
if symref or email != author or branch in local:
|
|
123
64
|
continue
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if not branch_is_pushed(branch, cwd=main_wt):
|
|
127
|
-
if not push:
|
|
128
|
-
kept.append(f"{branch} ({path}) โ unpushed, --no-push")
|
|
129
|
-
warning(f" โญ๏ธ {branch} โ unpushed and --no-push, keeping {path}")
|
|
130
|
-
continue
|
|
131
|
-
|
|
132
|
-
if dry_run:
|
|
133
|
-
info(f" ๐ต Would push: {branch} โ origin")
|
|
134
|
-
pushed.append(branch)
|
|
135
|
-
else:
|
|
136
|
-
log(f" โฌ๏ธ Pushing {branch} โ origin")
|
|
137
|
-
result = run(
|
|
138
|
-
["git", "-C", path, "push", "--quiet", "--set-upstream", "origin", branch],
|
|
139
|
-
check=False,
|
|
140
|
-
)
|
|
141
|
-
if result.returncode != 0:
|
|
142
|
-
kept.append(f"{branch} ({path}) โ push failed")
|
|
143
|
-
warning(f" โ push failed for {branch}, keeping {path}")
|
|
144
|
-
continue
|
|
145
|
-
pushed.append(branch)
|
|
146
|
-
|
|
147
|
-
# Re-check rather than assume: a push can succeed and still leave the
|
|
148
|
-
# branch behind its upstream if someone else pushed in between.
|
|
149
|
-
if not dry_run and not branch_is_pushed(branch, cwd=main_wt):
|
|
150
|
-
kept.append(f"{branch} ({path}) โ still not fully pushed")
|
|
151
|
-
warning(f" โญ๏ธ {branch} โ still not fully pushed, keeping {path}")
|
|
65
|
+
if sync.dry_run:
|
|
66
|
+
info(f"๐ต Would track: {branch}")
|
|
152
67
|
continue
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
# A lock is metadata stamped by whatever created the worktree (a tool,
|
|
160
|
-
# an agent, the user), and `git worktree remove` refuses while one is
|
|
161
|
-
# set. The lock says nothing about unsaved work โ the clean and pushed
|
|
162
|
-
# gates above are what protect that โ so release it rather than
|
|
163
|
-
# skipping. This is NOT --force, which is the flag that discards work.
|
|
164
|
-
if not keep_locked:
|
|
165
|
-
run(["git", "worktree", "unlock", path], check=False, capture=True, cwd=main_wt)
|
|
166
|
-
|
|
167
|
-
removed = run(["git", "worktree", "remove", path], check=False, cwd=main_wt)
|
|
168
|
-
if removed.returncode == 0:
|
|
169
|
-
success(f" โ
Collapsed {path} โ branch {branch} kept")
|
|
170
|
-
collapsed.append(f"{branch} ({path})")
|
|
68
|
+
if run(
|
|
69
|
+
["git", "branch", "--track", branch, f"refs/remotes/origin/{branch}"],
|
|
70
|
+
cwd=sync.root,
|
|
71
|
+
check=False,
|
|
72
|
+
).returncode:
|
|
73
|
+
sync.block(branch, "could not create tracking branch")
|
|
171
74
|
else:
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
return pushed, collapsed, kept
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
def report(
|
|
179
|
-
*,
|
|
180
|
-
pushed: list[str],
|
|
181
|
-
collapsed: list[str],
|
|
182
|
-
kept: list[str],
|
|
183
|
-
started_in: Path,
|
|
184
|
-
main_wt: Path,
|
|
185
|
-
) -> None:
|
|
186
|
-
"""Print the worktree summary."""
|
|
187
|
-
print()
|
|
188
|
-
note("โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ")
|
|
189
|
-
success("โโโ worktree summary โโโ")
|
|
190
|
-
success(f"โฌ๏ธ Pushed: {len(pushed)}")
|
|
191
|
-
for branch in pushed:
|
|
192
|
-
success(f" โข ๐ฟ {branch}")
|
|
193
|
-
success(f"๐ฆ Collapsed: {len(collapsed)}")
|
|
194
|
-
for branch in collapsed:
|
|
195
|
-
success(f" โข ๐ชฆ {branch}")
|
|
196
|
-
if kept:
|
|
197
|
-
warning(f"โญ๏ธ Kept: {len(kept)}")
|
|
198
|
-
for branch in kept:
|
|
199
|
-
warning(f" โข ๐ฟ {branch}")
|
|
200
|
-
|
|
201
|
-
if started_in != main_wt:
|
|
202
|
-
info(f"๐ Started in {started_in}; finished in {main_wt}.")
|
|
203
|
-
if not started_in.is_dir():
|
|
204
|
-
warning(f"โ ๏ธ {started_in} was collapsed โ your shell is in a stale directory.")
|
|
205
|
-
warning(f" Run: cd {main_wt}")
|
|
75
|
+
tracked.append(branch)
|
|
76
|
+
return tracked
|
|
206
77
|
|
|
207
78
|
|
|
208
79
|
def main(argv: list[str] | None = None) -> int:
|
|
209
80
|
parser = build_parser(
|
|
210
81
|
prog="sync-all-branches",
|
|
211
|
-
description="๐
|
|
82
|
+
description="๐ Update every branch and worktree, preserving locks and local work.",
|
|
212
83
|
)
|
|
213
84
|
add_bool_flag(
|
|
214
85
|
parser,
|
|
215
86
|
"--dry-run",
|
|
216
87
|
default=env_bool("DRY_RUN", False),
|
|
217
|
-
help="
|
|
218
|
-
)
|
|
219
|
-
add_bool_flag(
|
|
220
|
-
parser,
|
|
221
|
-
"--no-push",
|
|
222
|
-
allow_value=False,
|
|
223
|
-
help="Never push; only collapse worktrees that are already pushed",
|
|
224
|
-
)
|
|
225
|
-
add_bool_flag(
|
|
226
|
-
parser,
|
|
227
|
-
"--keep-locked",
|
|
228
|
-
help="Leave locked worktrees alone (default: release the lock once pushed and clean)",
|
|
88
|
+
help="Preview cached refs only; no fetch or changes",
|
|
229
89
|
)
|
|
90
|
+
# Compatibility with chip and older wrappers. Neither pushing nor lock
|
|
91
|
+
# removal is ever part of syncing, regardless of these legacy flags.
|
|
92
|
+
add_bool_flag(parser, "--no-push", allow_value=False, help=argparse.SUPPRESS)
|
|
93
|
+
add_bool_flag(parser, "--keep-locked", help=argparse.SUPPRESS)
|
|
230
94
|
parser.add_argument(
|
|
231
95
|
"--author",
|
|
232
96
|
default="",
|
|
233
97
|
metavar="EMAIL",
|
|
234
|
-
help="
|
|
98
|
+
help="Author of new origin branches (default: git config user.email)",
|
|
235
99
|
)
|
|
236
100
|
parser.add_argument(
|
|
237
101
|
"--limit",
|
|
238
|
-
default="",
|
|
102
|
+
default="100",
|
|
239
103
|
metavar="N",
|
|
240
|
-
help="
|
|
104
|
+
help="Recent origin refs to scan for new branches (default: 100)",
|
|
241
105
|
)
|
|
242
106
|
args = parser.parse_args(argv)
|
|
243
|
-
|
|
107
|
+
limit = parse_count(args.limit, "--limit", minimum=1)
|
|
244
108
|
require_binary("git")
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
if inside
|
|
109
|
+
skip_git_hooks()
|
|
110
|
+
heading("๐ sync-all-branches", dry_run=args.dry_run)
|
|
111
|
+
if run_output(["git", "rev-parse", "--is-inside-work-tree"], check=False) != "true":
|
|
248
112
|
warning("โ Not inside a git working tree.")
|
|
249
113
|
return 1
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
note("โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ")
|
|
255
|
-
|
|
256
|
-
# Every later step runs from the main worktree: a worktree cannot be
|
|
257
|
-
# removed while we are standing inside it, and the delegated scripts
|
|
258
|
-
# expect the main clone.
|
|
259
|
-
main_wt_raw = main_worktree()
|
|
260
|
-
if not main_wt_raw:
|
|
261
|
-
warning("โ Could not determine main worktree.")
|
|
262
|
-
return 1
|
|
263
|
-
|
|
264
|
-
main_wt = Path(main_wt_raw)
|
|
265
|
-
# The shell version `cd`-ed here. Passing cwd= to each call instead keeps
|
|
266
|
-
# the process's own directory untouched, so a caller that sourced this in
|
|
267
|
-
# the same shell would not be moved out from under itself.
|
|
268
|
-
started_in = Path.cwd().resolve()
|
|
269
|
-
|
|
270
|
-
main_branch = find_main_branch(main_wt)
|
|
271
|
-
if not main_branch:
|
|
272
|
-
warning("โ Neither 'main' nor 'master' exists locally โ refusing to sync.")
|
|
114
|
+
root = Path(run_output(["git", "rev-parse", "--show-toplevel"]))
|
|
115
|
+
author = args.author or run_output(["git", "config", "user.email"], cwd=root, check=False)
|
|
116
|
+
if not author:
|
|
117
|
+
warning("โ No author email; pass --author=EMAIL or set git config user.email.")
|
|
273
118
|
return 1
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
#
|
|
277
|
-
#
|
|
278
|
-
|
|
279
|
-
info("๐ก Fetching + pruning remotes...")
|
|
119
|
+
original = current_branch(root) or run_output(["git", "rev-parse", "--short", "HEAD"], cwd=root)
|
|
120
|
+
info(f"๐ Keeping caller checkout: {original}")
|
|
121
|
+
# DELIBERATE DIVERGENCE: the old sync parked on main and delegated cleanup.
|
|
122
|
+
# Preserve active workspaces instead, and never discard a gone-upstream
|
|
123
|
+
# branch merely because its remote was deleted. Dry-run must not fetch.
|
|
280
124
|
if args.dry_run:
|
|
281
|
-
info("๐ต
|
|
125
|
+
info("๐ต Cached remote refs only; no fetch. Results reflect the last fetch.")
|
|
282
126
|
else:
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
# Steps 2-3 โ push every worktree, then collapse the pushed ones
|
|
287
|
-
# -----------------------------------------------------------------------
|
|
288
|
-
|
|
289
|
-
pushed, collapsed, kept = collapse_worktrees(
|
|
290
|
-
main_wt=main_wt,
|
|
291
|
-
push=not args.no_push,
|
|
292
|
-
keep_locked=args.keep_locked,
|
|
293
|
-
dry_run=args.dry_run,
|
|
294
|
-
)
|
|
295
|
-
|
|
296
|
-
# -----------------------------------------------------------------------
|
|
297
|
-
# Step 4 โ prune stale admin records
|
|
298
|
-
# -----------------------------------------------------------------------
|
|
299
|
-
|
|
300
|
-
if args.dry_run:
|
|
301
|
-
info("๐ต Would run: git worktree prune")
|
|
302
|
-
else:
|
|
303
|
-
run(["git", "worktree", "prune"], cwd=main_wt)
|
|
304
|
-
|
|
305
|
-
# -----------------------------------------------------------------------
|
|
306
|
-
# Step 5 โ park on main before anything tries to delete or rebase branches
|
|
307
|
-
# -----------------------------------------------------------------------
|
|
308
|
-
|
|
309
|
-
current = run_output(
|
|
310
|
-
["git", "symbolic-ref", "--quiet", "--short", "HEAD"], check=False, cwd=main_wt
|
|
311
|
-
).strip()
|
|
312
|
-
|
|
313
|
-
if current != main_branch:
|
|
314
|
-
# Only tracked changes block: untracked files (including worktrees
|
|
315
|
-
# nested under the main worktree) are not touched by checkout.
|
|
316
|
-
if run_output(["git", "status", "--porcelain", "--untracked-files=no"], cwd=main_wt):
|
|
317
|
-
warning("โ Main worktree has uncommitted changes โ commit or stash first. ๐งบ")
|
|
318
|
-
return 1
|
|
319
|
-
|
|
320
|
-
if args.dry_run:
|
|
321
|
-
info(f"๐ต Would switch to ๐ฟ {main_branch} (currently on {current or 'detached'})")
|
|
322
|
-
else:
|
|
323
|
-
info(f"๐ Switching to ๐ฟ {main_branch} (from {current or 'detached'})")
|
|
324
|
-
run(["git", "checkout", "--quiet", main_branch], cwd=main_wt)
|
|
325
|
-
else:
|
|
326
|
-
info(f"๐ Already on ๐ฟ {main_branch}")
|
|
327
|
-
|
|
328
|
-
# -----------------------------------------------------------------------
|
|
329
|
-
# Steps 6-8 โ delegate to the single-purpose scripts
|
|
330
|
-
# -----------------------------------------------------------------------
|
|
331
|
-
|
|
332
|
-
delegate_args = ["--dry-run"] if args.dry_run else []
|
|
333
|
-
|
|
334
|
-
# DELIBERATE DIVERGENCE: the summary is printed even when a delegate fails.
|
|
335
|
-
# The pushes and collapses above already happened; aborting without saying
|
|
336
|
-
# which ones would leave the user guessing what state the repo is in.
|
|
127
|
+
fetch_remotes()
|
|
128
|
+
sync = BranchSync(root, args.dry_run)
|
|
129
|
+
sync.update()
|
|
337
130
|
try:
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
checkout_args.append(f"--author={args.author}")
|
|
347
|
-
if args.limit:
|
|
348
|
-
checkout_args.append(f"--limit={args.limit}")
|
|
349
|
-
|
|
350
|
-
print()
|
|
351
|
-
if args.dry_run:
|
|
352
|
-
info(f"๐ต Would run: checkout-my-branches {' '.join(checkout_args)}".rstrip())
|
|
353
|
-
else:
|
|
354
|
-
delegate("checkout-my-branches.py", checkout_args, cwd=main_wt, dry_run=args.dry_run)
|
|
355
|
-
except BaseException:
|
|
356
|
-
report(
|
|
357
|
-
pushed=pushed, collapsed=collapsed, kept=kept, started_in=started_in, main_wt=main_wt
|
|
131
|
+
tracked = track_new(sync, author, limit)
|
|
132
|
+
except (subprocess.CalledProcessError, OSError) as exc:
|
|
133
|
+
sync.block("new origin branches", str(exc))
|
|
134
|
+
tracked = []
|
|
135
|
+
sync.summary(tracked)
|
|
136
|
+
if sync.blocked:
|
|
137
|
+
warning(
|
|
138
|
+
"Sync partially completed; blocked branches need attention. Remaining independent branches were attempted."
|
|
358
139
|
)
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
report(pushed=pushed, collapsed=collapsed, kept=kept, started_in=started_in, main_wt=main_wt)
|
|
362
|
-
|
|
363
|
-
log("๐ Sync complete.")
|
|
140
|
+
return 1
|
|
141
|
+
log("๐ Preview complete." if args.dry_run else "๐ Sync complete.")
|
|
364
142
|
return 0
|
|
365
143
|
|
|
366
144
|
|
|
@@ -14,56 +14,57 @@ Options:
|
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
|
-
import re
|
|
18
17
|
import sys
|
|
19
18
|
from pathlib import Path
|
|
20
19
|
|
|
21
20
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
22
21
|
|
|
22
|
+
from gitutils import (
|
|
23
|
+
current_branch,
|
|
24
|
+
fetch_remotes,
|
|
25
|
+
has_tracked_changes,
|
|
26
|
+
local_branches,
|
|
27
|
+
skip_git_hooks,
|
|
28
|
+
worktree_branch_map,
|
|
29
|
+
worktree_is_clean,
|
|
30
|
+
)
|
|
23
31
|
from utils import (
|
|
24
32
|
add_bool_flag,
|
|
25
33
|
build_parser,
|
|
34
|
+
heading,
|
|
26
35
|
info,
|
|
27
36
|
log,
|
|
28
|
-
|
|
37
|
+
parse_count,
|
|
29
38
|
require_binary,
|
|
30
39
|
run,
|
|
31
40
|
run_cli,
|
|
32
|
-
run_lines,
|
|
33
|
-
run_output,
|
|
34
41
|
success,
|
|
35
42
|
warning,
|
|
36
|
-
worktree_branch_map,
|
|
37
|
-
worktree_is_clean,
|
|
38
43
|
)
|
|
39
44
|
|
|
40
|
-
__version__ = "3.
|
|
45
|
+
__version__ = "3.2.0"
|
|
41
46
|
|
|
42
47
|
|
|
43
48
|
def branches_with_upstream() -> list[str]:
|
|
44
|
-
"""Local branches that have an upstream, most recently committed first.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
"""Local branches that have an upstream, most recently committed first."""
|
|
50
|
+
return [
|
|
51
|
+
branch for branch, upstream in local_branches("upstream", sort="-committerdate") if upstream
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def rebase(git: list[str]) -> bool:
|
|
56
|
+
"""Rebase onto the upstream with ``git`` (plain, or ``-C <worktree>``); abort on failure.
|
|
57
|
+
|
|
58
|
+
DELIBERATE DIVERGENCE: the shell version ran ``pull --rebase`` per branch,
|
|
59
|
+
a fetch for every branch on top of the one the run opened with. The remotes
|
|
60
|
+
are fetched once up front, so a bare ``rebase`` (onto ``@{upstream}``, with
|
|
61
|
+
the same fork-point handling ``pull --rebase`` uses) is the whole job.
|
|
52
62
|
"""
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
"--sort=-committerdate",
|
|
59
|
-
"--format=%(refname:short)%01%(upstream)",
|
|
60
|
-
"refs/heads/",
|
|
61
|
-
]
|
|
62
|
-
):
|
|
63
|
-
branch, _, upstream = line.partition("\x01")
|
|
64
|
-
if branch and upstream:
|
|
65
|
-
found.append(branch)
|
|
66
|
-
return found
|
|
63
|
+
if run([*git, "rebase", "--quiet"], check=False).returncode == 0:
|
|
64
|
+
return True
|
|
65
|
+
warning(" โ rebase failed โ aborting and moving on")
|
|
66
|
+
run([*git, "rebase", "--abort"], check=False, capture=True)
|
|
67
|
+
return False
|
|
67
68
|
|
|
68
69
|
|
|
69
70
|
def main(argv: list[str] | None = None) -> int:
|
|
@@ -71,8 +72,6 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
71
72
|
prog="update-local-branches",
|
|
72
73
|
description="โฌ๏ธ Rebase every local branch that has an upstream onto origin.",
|
|
73
74
|
)
|
|
74
|
-
# Kept as a string, not `type=int`: argparse's own int conversion would
|
|
75
|
-
# own the failure and exit before the shell version's message could print.
|
|
76
75
|
parser.add_argument(
|
|
77
76
|
"--limit",
|
|
78
77
|
default="0",
|
|
@@ -86,31 +85,28 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
86
85
|
)
|
|
87
86
|
args = parser.parse_args(argv)
|
|
88
87
|
|
|
89
|
-
|
|
90
|
-
warning(f"โ --limit must be a non-negative integer (got: {args.limit})")
|
|
91
|
-
return 1
|
|
92
|
-
limit = int(args.limit)
|
|
88
|
+
limit = parse_count(args.limit, "--limit")
|
|
93
89
|
|
|
94
90
|
require_binary("git")
|
|
91
|
+
skip_git_hooks()
|
|
95
92
|
|
|
96
|
-
|
|
97
|
-
note("โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ")
|
|
98
|
-
note(f"โฌ๏ธ update-local-branches{header_suffix}")
|
|
99
|
-
note("โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ")
|
|
93
|
+
heading("โฌ๏ธ update-local-branches", dry_run=args.dry_run)
|
|
100
94
|
|
|
101
|
-
original_branch =
|
|
102
|
-
["git", "symbolic-ref", "--quiet", "--short", "HEAD"], check=False
|
|
103
|
-
).strip()
|
|
95
|
+
original_branch = current_branch()
|
|
104
96
|
if not original_branch:
|
|
105
97
|
warning("โ Detached HEAD โ check out a branch first.")
|
|
106
98
|
return 1
|
|
107
99
|
|
|
108
|
-
|
|
100
|
+
# Tracked changes only: an untracked file survives checkout and rebase, and
|
|
101
|
+
# a linked worktree nested in this one is untracked - counting it made
|
|
102
|
+
# sync-all-branches abort right after parking had allowed the same tree.
|
|
103
|
+
if not args.dry_run and has_tracked_changes():
|
|
109
104
|
warning("โ Working tree is dirty. Commit or stash before running. ๐งบ")
|
|
110
105
|
return 1
|
|
111
106
|
|
|
112
|
-
|
|
113
|
-
|
|
107
|
+
# Prunes too, where this script's own fetch did not: it shares the one
|
|
108
|
+
# fetch sync-all-branches runs.
|
|
109
|
+
fetch_remotes()
|
|
114
110
|
|
|
115
111
|
branches = branches_with_upstream()
|
|
116
112
|
if limit > 0:
|
|
@@ -127,52 +123,46 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
127
123
|
return 0
|
|
128
124
|
|
|
129
125
|
succeeded: list[str] = []
|
|
126
|
+
skipped: list[str] = []
|
|
130
127
|
failed: list[str] = []
|
|
131
128
|
|
|
132
|
-
#
|
|
133
|
-
#
|
|
134
|
-
|
|
135
|
-
|
|
129
|
+
# Branches checked out in ANOTHER worktree: every pinned one but the branch
|
|
130
|
+
# this worktree has checked out. One `git worktree list` for the whole run,
|
|
131
|
+
# not one per branch as the shell's per-branch `worktree_for_branch` did.
|
|
132
|
+
elsewhere = worktree_branch_map()
|
|
133
|
+
elsewhere.pop(original_branch, None)
|
|
136
134
|
|
|
137
135
|
for branch in branches:
|
|
138
136
|
# A branch checked out in another worktree cannot be checked out here โ
|
|
139
137
|
# git refuses with "already used by worktree". That's a mechanical
|
|
140
138
|
# limit, not a conflict, so update it in place rather than counting it
|
|
141
139
|
# as a failure.
|
|
142
|
-
pinned
|
|
143
|
-
|
|
144
|
-
|
|
140
|
+
if pinned := elsewhere.get(branch):
|
|
141
|
+
# Uncommitted work in a worktree is the user's, left exactly as it
|
|
142
|
+
# is. Skipping it is not a failure, or one worktree in progress
|
|
143
|
+
# would stop every run of sync-all-branches at this step.
|
|
145
144
|
if not worktree_is_clean(pinned):
|
|
146
145
|
warning(f" โญ๏ธ {branch} โ worktree is dirty ({pinned}), skipping")
|
|
147
|
-
|
|
146
|
+
skipped.append(branch)
|
|
148
147
|
continue
|
|
149
148
|
|
|
150
149
|
log(f"โฌ๏ธ Rebasing {branch} in its worktree ({pinned})")
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
warning("
|
|
157
|
-
run(["git", "-C", pinned, "rebase", "--abort"], check=False, capture=True)
|
|
150
|
+
git, where = ["git", "-C", pinned], f" (in {pinned})"
|
|
151
|
+
else:
|
|
152
|
+
info(f"๐ Switching to ๐ฟ {branch}")
|
|
153
|
+
checkout = run(["git", "checkout", "--quiet", branch], check=False, capture=True)
|
|
154
|
+
if checkout.returncode != 0:
|
|
155
|
+
warning(" โ checkout failed, skipping")
|
|
158
156
|
failed.append(branch)
|
|
159
|
-
|
|
157
|
+
continue
|
|
160
158
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
if checkout.returncode != 0:
|
|
164
|
-
warning(" โ checkout failed, skipping")
|
|
165
|
-
failed.append(branch)
|
|
166
|
-
continue
|
|
159
|
+
log(f"โฌ๏ธ Rebasing {branch} onto upstream")
|
|
160
|
+
git, where = ["git"], ""
|
|
167
161
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
if pull.returncode == 0:
|
|
171
|
-
success(f" โ
{branch} up to date")
|
|
162
|
+
if rebase(git):
|
|
163
|
+
success(f" โ
{branch} up to date{where}")
|
|
172
164
|
succeeded.append(branch)
|
|
173
165
|
else:
|
|
174
|
-
warning(" โ rebase failed โ aborting and moving on")
|
|
175
|
-
run(["git", "rebase", "--abort"], check=False, capture=True)
|
|
176
166
|
failed.append(branch)
|
|
177
167
|
|
|
178
168
|
info(f"๐ Returning to ๐ฟ {original_branch}")
|
|
@@ -181,6 +171,10 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
181
171
|
print()
|
|
182
172
|
success("โโโ summary โโโ")
|
|
183
173
|
success(f"โ
Updated: {len(succeeded)}")
|
|
174
|
+
if skipped:
|
|
175
|
+
warning(f"โญ๏ธ Skipped: {len(skipped)} (dirty worktree)")
|
|
176
|
+
for branch in skipped:
|
|
177
|
+
warning(f" โข ๐ฟ {branch}")
|
|
184
178
|
if failed:
|
|
185
179
|
warning(f"โ Failed: {len(failed)}")
|
|
186
180
|
for branch in failed:
|