@codebend3r/che 1.1.0 โ 1.2.0
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 +38 -24
- package/bin/art.py +22 -20
- package/bin/che.py +208 -587
- package/bin/commands.py +220 -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 +54 -283
- 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,34 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""๐ Sync
|
|
3
|
-
|
|
4
|
-
A worktree is a temporary workspace. Once its commits are on origin, the
|
|
5
|
-
directory holds nothing the remote does not, so it is removed and the branch
|
|
6
|
-
stays behind as a plain local branch that the later steps can update normally.
|
|
7
|
-
That also clears the "already used by worktree" pin, which is what otherwise
|
|
8
|
-
makes main un-checkoutable and stale branches un-deletable.
|
|
2
|
+
"""๐ Sync every branch: delete merged ones, rebase the rest, check out new ones.
|
|
9
3
|
|
|
10
4
|
Steps, in order:
|
|
11
|
-
1.
|
|
12
|
-
2.
|
|
13
|
-
3.
|
|
14
|
-
4.
|
|
15
|
-
5.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
5
|
+
1. Switch the main clone to the main branch
|
|
6
|
+
2. Fetch + prune every remote, once for the whole run
|
|
7
|
+
3. clean-stale-branches (delete branches whose upstream is gone)
|
|
8
|
+
4. update-local-branches (rebase every branch onto its upstream)
|
|
9
|
+
5. checkout-my-branches (track your remote branches that aren't local)
|
|
10
|
+
|
|
11
|
+
Only branches change. It never pushes, never adds, removes or prunes a
|
|
12
|
+
worktree, and never runs a repo's git hooks: pushing and worktrees are the
|
|
13
|
+
user's call, and a lefthook or husky pre-commit or pre-push gates the user's
|
|
14
|
+
own work, not this bookkeeping. A branch checked out in a linked worktree is
|
|
15
|
+
rebased in place there; a stale one is reported, not deleted. The main clone
|
|
16
|
+
ends on main: new branches are created without being checked out.
|
|
22
17
|
|
|
23
18
|
Usage:
|
|
24
|
-
sync-all-branches.py [--dry-run] [--
|
|
25
|
-
[--author=EMAIL] [--limit=N]
|
|
19
|
+
sync-all-branches.py [--dry-run] [--author=EMAIL] [--limit=N]
|
|
26
20
|
|
|
27
21
|
Options:
|
|
28
|
-
--dry-run Report what would be
|
|
29
|
-
--no-push Never push; only collapse worktrees that are already pushed
|
|
30
|
-
--keep-locked Leave locked worktrees alone (default: release the lock once
|
|
31
|
-
the worktree is pushed and clean)
|
|
22
|
+
--dry-run Report what would be deleted / synced, change nothing
|
|
32
23
|
--author=EMAIL Passed through to checkout-my-branches
|
|
33
24
|
--limit=N Passed through to checkout-my-branches
|
|
34
25
|
--help Show help
|
|
@@ -39,194 +30,60 @@ Env:
|
|
|
39
30
|
|
|
40
31
|
from __future__ import annotations
|
|
41
32
|
|
|
33
|
+
import argparse
|
|
42
34
|
import sys
|
|
43
35
|
from pathlib import Path
|
|
44
36
|
|
|
45
37
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
46
38
|
|
|
39
|
+
from gitutils import fetch_remotes, park_main_worktree, skip_git_hooks
|
|
47
40
|
from utils import (
|
|
48
41
|
add_bool_flag,
|
|
49
|
-
branch_is_pushed,
|
|
50
42
|
build_parser,
|
|
51
43
|
env_bool,
|
|
52
|
-
|
|
44
|
+
heading,
|
|
53
45
|
info,
|
|
54
46
|
log,
|
|
55
|
-
main_worktree,
|
|
56
|
-
note,
|
|
57
47
|
require_binary,
|
|
58
48
|
run,
|
|
59
49
|
run_cli,
|
|
60
|
-
run_output,
|
|
61
|
-
success,
|
|
62
|
-
warning,
|
|
63
|
-
worktree_branch_map,
|
|
64
|
-
worktree_is_clean,
|
|
65
50
|
)
|
|
66
51
|
|
|
67
|
-
__version__ = "1.0
|
|
52
|
+
__version__ = "2.1.0"
|
|
68
53
|
|
|
69
54
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
70
55
|
|
|
71
56
|
|
|
72
|
-
def delegate(script: str, args: list[str], *, cwd: Path
|
|
57
|
+
def delegate(script: str, args: list[str], *, cwd: Path) -> None:
|
|
73
58
|
"""Run a sibling script in the main worktree, aborting the sync if it fails.
|
|
74
59
|
|
|
75
60
|
The shell version invoked these under ``set -e``, so a non-zero exit from
|
|
76
61
|
any of them ended the run. ``check=True`` reproduces that: a branch that
|
|
77
62
|
could not be rebased is a reason to stop and look, not to sail on.
|
|
78
63
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
run(
|
|
83
|
-
[sys.executable, str(SCRIPT_DIR / script), *args],
|
|
84
|
-
cwd=cwd,
|
|
85
|
-
env={"DRY_RUN": "true" if dry_run else "false"},
|
|
86
|
-
)
|
|
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.
|
|
64
|
+
The blank line before each one is flushed: a child writes straight to the
|
|
65
|
+
shared stdout, so an unflushed separator would surface after its output
|
|
66
|
+
whenever the sync is piped.
|
|
101
67
|
"""
|
|
102
|
-
|
|
103
|
-
|
|
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}")
|
|
123
|
-
continue
|
|
124
|
-
|
|
125
|
-
# Push whatever is not on origin yet, so the directory becomes redundant.
|
|
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}")
|
|
152
|
-
continue
|
|
153
|
-
|
|
154
|
-
if dry_run:
|
|
155
|
-
info(f" ๐ต Would collapse: {path} (branch {branch} kept)")
|
|
156
|
-
collapsed.append(f"{branch} ({path})")
|
|
157
|
-
continue
|
|
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})")
|
|
171
|
-
else:
|
|
172
|
-
kept.append(f"{branch} ({path}) โ removal failed (locked?)")
|
|
173
|
-
warning(f" โ Could not remove {path}")
|
|
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}")
|
|
68
|
+
print(flush=True)
|
|
69
|
+
run([sys.executable, str(SCRIPT_DIR / script), *args], cwd=cwd)
|
|
206
70
|
|
|
207
71
|
|
|
208
72
|
def main(argv: list[str] | None = None) -> int:
|
|
209
73
|
parser = build_parser(
|
|
210
74
|
prog="sync-all-branches",
|
|
211
|
-
description="๐
|
|
75
|
+
description="๐ Delete merged branches, rebase the rest, check out new ones.",
|
|
212
76
|
)
|
|
213
77
|
add_bool_flag(
|
|
214
78
|
parser,
|
|
215
79
|
"--dry-run",
|
|
216
80
|
default=env_bool("DRY_RUN", False),
|
|
217
|
-
help="Report what would be
|
|
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)",
|
|
81
|
+
help="Report what would be deleted / synced, change nothing",
|
|
229
82
|
)
|
|
83
|
+
# Accepted and ignored, from when the sync pushed and collapsed worktrees:
|
|
84
|
+
# chip's "Update from Origin" still passes --no-push and must keep running.
|
|
85
|
+
add_bool_flag(parser, "--no-push", allow_value=False, help=argparse.SUPPRESS)
|
|
86
|
+
add_bool_flag(parser, "--keep-locked", help=argparse.SUPPRESS)
|
|
230
87
|
parser.add_argument(
|
|
231
88
|
"--author",
|
|
232
89
|
default="",
|
|
@@ -242,123 +99,37 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
242
99
|
args = parser.parse_args(argv)
|
|
243
100
|
|
|
244
101
|
require_binary("git")
|
|
102
|
+
skip_git_hooks()
|
|
245
103
|
|
|
246
|
-
|
|
247
|
-
if inside.returncode != 0:
|
|
248
|
-
warning("โ Not inside a git working tree.")
|
|
249
|
-
return 1
|
|
250
|
-
|
|
251
|
-
header_suffix = " ๐ต (dry run)" if args.dry_run else ""
|
|
252
|
-
note("โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ")
|
|
253
|
-
note(f"๐ sync-all-branches{header_suffix}")
|
|
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()
|
|
104
|
+
heading("๐ sync-all-branches", dry_run=args.dry_run)
|
|
269
105
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
106
|
+
# Every later step runs from the main worktree, because the delegated
|
|
107
|
+
# scripts expect the main clone. Passing cwd= to each call rather than
|
|
108
|
+
# `cd`-ing keeps the process's own directory untouched.
|
|
109
|
+
main_wt = park_main_worktree("sync", dry_run=args.dry_run)
|
|
274
110
|
|
|
275
|
-
#
|
|
276
|
-
#
|
|
277
|
-
|
|
111
|
+
# The one fetch of the run. Each delegated script fetches when run on its
|
|
112
|
+
# own; here they inherit the marker fetch_remotes leaves and skip theirs.
|
|
113
|
+
fetch_remotes()
|
|
278
114
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
else
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
# -----------------------------------------------------------------------
|
|
286
|
-
# Steps 2-3 โ push every worktree, then collapse the pushed ones
|
|
287
|
-
# -----------------------------------------------------------------------
|
|
115
|
+
# Dry-run travels as an explicit --dry-run=true|false, never as DRY_RUN in
|
|
116
|
+
# the environment, so a child can never disagree with the parent about
|
|
117
|
+
# which mode this is - whatever DRY_RUN it inherited.
|
|
118
|
+
mode = [f"--dry-run={'true' if args.dry_run else 'false'}"]
|
|
119
|
+
delegate("clean-stale-branches.py", mode, cwd=main_wt)
|
|
120
|
+
delegate("update-local-branches.py", mode, cwd=main_wt)
|
|
288
121
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
)
|
|
295
|
-
|
|
296
|
-
# -----------------------------------------------------------------------
|
|
297
|
-
# Step 4 โ prune stale admin records
|
|
298
|
-
# -----------------------------------------------------------------------
|
|
122
|
+
checkout_args = []
|
|
123
|
+
if args.author:
|
|
124
|
+
checkout_args.append(f"--author={args.author}")
|
|
125
|
+
if args.limit:
|
|
126
|
+
checkout_args.append(f"--limit={args.limit}")
|
|
299
127
|
|
|
300
128
|
if args.dry_run:
|
|
301
|
-
|
|
129
|
+
print(flush=True)
|
|
130
|
+
info(f"๐ต Would run: checkout-my-branches {' '.join(checkout_args)}".rstrip())
|
|
302
131
|
else:
|
|
303
|
-
|
|
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.
|
|
337
|
-
try:
|
|
338
|
-
print()
|
|
339
|
-
delegate("clean-stale-branches.py", delegate_args, cwd=main_wt, dry_run=args.dry_run)
|
|
340
|
-
|
|
341
|
-
print()
|
|
342
|
-
delegate("update-local-branches.py", delegate_args, cwd=main_wt, dry_run=args.dry_run)
|
|
343
|
-
|
|
344
|
-
checkout_args = []
|
|
345
|
-
if args.author:
|
|
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
|
|
358
|
-
)
|
|
359
|
-
raise
|
|
360
|
-
|
|
361
|
-
report(pushed=pushed, collapsed=collapsed, kept=kept, started_in=started_in, main_wt=main_wt)
|
|
132
|
+
delegate("checkout-my-branches.py", checkout_args, cwd=main_wt)
|
|
362
133
|
|
|
363
134
|
log("๐ Sync complete.")
|
|
364
135
|
return 0
|
|
@@ -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:
|