@codebend3r/che 1.0.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.
Files changed (53) hide show
  1. package/README.md +65 -25
  2. package/bin/art.py +123 -0
  3. package/bin/che.py +209 -568
  4. package/bin/commands.py +220 -231
  5. package/bin/doctor.py +220 -0
  6. package/bin/drives/eject-all-drives.py +21 -38
  7. package/bin/drives/mount-all-drives.py +18 -62
  8. package/bin/drives/ping-nas.py +77 -105
  9. package/bin/files/compress-folders.py +7 -11
  10. package/bin/files/delete-by-ext.py +10 -14
  11. package/bin/files/delete-empty-folders.py +33 -37
  12. package/bin/files/delete-smb-files.py +12 -21
  13. package/bin/files/files-under-size.py +5 -11
  14. package/bin/files/find-largest-files.py +21 -25
  15. package/bin/files/list-permission.py +40 -0
  16. package/bin/files/make-alpha-dir.py +2 -0
  17. package/bin/git/all-actions.py +350 -253
  18. package/bin/git/checkout-my-branches.py +42 -63
  19. package/bin/git/clean-stale-branches.py +23 -48
  20. package/bin/git/prune-worktrees.py +12 -51
  21. package/bin/git/sync-all-branches.py +54 -283
  22. package/bin/git/update-local-branches.py +67 -73
  23. package/bin/gitutils.py +263 -0
  24. package/bin/install.py +393 -1188
  25. package/bin/machine.py +345 -0
  26. package/bin/media.py +127 -0
  27. package/bin/menu.py +437 -0
  28. package/bin/nas.py +102 -0
  29. package/bin/rcfile.py +380 -0
  30. package/bin/shellgen.py +508 -428
  31. package/bin/system/btop-launch.py +17 -13
  32. package/bin/system/update-brew.py +57 -68
  33. package/bin/tui.py +156 -153
  34. package/bin/utils.py +165 -237
  35. package/bin/video/delete-duplicate-videos.py +64 -62
  36. package/bin/video/detect-green-magenta-videos.py +105 -95
  37. package/bin/video/find-movie-by-year.py +3 -0
  38. package/bin/video/find-video-mkv-issues.py +48 -65
  39. package/bin/video/fix-codecs.py +25 -30
  40. package/bin/video/largest-tv-shows.py +60 -57
  41. package/bin/video/remove-metadata.py +3 -6
  42. package/bin/video/rename-video-file.py +74 -84
  43. package/bin/video/scan-videos-audio-language.py +16 -33
  44. package/bin/video/show-codecs.py +20 -24
  45. package/bin/video/validate-video-files.py +5 -8
  46. package/bin/video/video-list.py +17 -20
  47. package/package.json +4 -10
  48. package/shell/che.bash +33 -33
  49. package/shell/che.fish +7 -7
  50. package/shell/che.zsh +33 -33
  51. package/shell/completions/_che +19 -19
  52. package/shell/completions/che.bash +15 -15
  53. package/shell/completions/che.fish +34 -50
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env python3
2
2
  """๐ŸŒฟ Check out recent remote branches authored by you that aren't yet local.
3
3
 
4
+ Each one becomes a local branch tracking its remote. HEAD stays where it is.
5
+
4
6
  Usage:
5
7
  checkout-my-branches.py [--author=EMAIL] [--limit=N]
6
8
 
@@ -12,44 +14,40 @@ Options:
12
14
 
13
15
  from __future__ import annotations
14
16
 
15
- import re
16
17
  import sys
17
18
  from pathlib import Path
18
19
 
19
20
  sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
20
21
 
22
+ from gitutils import fetch_remotes, for_each_ref, local_branches, skip_git_hooks
21
23
  from utils import (
22
24
  build_parser,
25
+ heading,
23
26
  info,
24
27
  log,
25
- note,
28
+ parse_count,
26
29
  require_binary,
27
30
  run,
28
31
  run_cli,
29
- run_lines,
30
32
  run_output,
31
33
  success,
32
34
  warning,
33
35
  )
34
36
 
35
- __version__ = "3.0.0"
37
+ __version__ = "3.1.0"
36
38
 
37
39
 
38
40
  def main(argv: list[str] | None = None) -> int:
39
- default_author = run_output(["git", "config", "user.email"], check=False).strip()
40
-
41
41
  parser = build_parser(
42
42
  prog="checkout-my-branches",
43
43
  description="๐ŸŒฟ Check out recent remote branches you authored that aren't yet local.",
44
44
  )
45
45
  parser.add_argument(
46
46
  "--author",
47
- default=default_author,
47
+ default="",
48
48
  metavar="EMAIL",
49
49
  help="Author email to match (default: git config user.email)",
50
50
  )
51
- # Kept as a string, not `type=int`: argparse's own int conversion would
52
- # own the failure and exit before the shell version's message could print.
53
51
  parser.add_argument(
54
52
  "--limit",
55
53
  default="100",
@@ -58,80 +56,61 @@ def main(argv: list[str] | None = None) -> int:
58
56
  )
59
57
  args = parser.parse_args(argv)
60
58
 
61
- if not args.author:
62
- warning("โŒ No author email โ€” pass --author=EMAIL or set git config user.email")
63
- return 1
64
-
65
- if not re.fullmatch(r"[0-9]+", args.limit) or int(args.limit) < 1:
66
- warning(f"โŒ --limit must be a positive integer (got: {args.limit})")
67
- return 1
68
- limit = int(args.limit)
59
+ limit = parse_count(args.limit, "--limit", minimum=1)
69
60
 
70
61
  require_binary("git")
62
+ # Read after the check for git, so a missing git says so rather than
63
+ # reporting that no author email is set.
64
+ author = args.author or run_output(["git", "config", "user.email"], check=False).strip()
65
+ if not author:
66
+ warning("โŒ No author email โ€” pass --author=EMAIL or set git config user.email")
67
+ return 1
71
68
 
72
- note("โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•")
73
- note("๐ŸŒฟ checkout-my-branches")
74
- note("โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•")
69
+ # Nothing here checks out, but fetch and branch still fire a repo's
70
+ # reference-transaction hook.
71
+ skip_git_hooks()
75
72
 
76
- if run_output(["git", "status", "--porcelain"]):
77
- warning("โŒ Working tree is dirty. Commit or stash before running. ๐Ÿงบ")
78
- return 1
73
+ heading("๐ŸŒฟ checkout-my-branches")
79
74
 
80
- info("๐Ÿ“ก Fetching remotes...")
81
- run(["git", "fetch", "--all", "--prune", "--quiet"])
75
+ fetch_remotes()
82
76
 
83
- info(f"๐Ÿ“ฆ Scanning {limit} most recent remote branches for author ๐Ÿ‘ค {args.author}...")
77
+ info(f"๐Ÿ“ฆ Scanning {limit} most recent remote branches for author ๐Ÿ‘ค {author}...")
84
78
 
85
79
  checked_out = 0
86
80
  skipped = 0
87
81
  not_mine = 0
88
82
 
89
- # One pass: author email + refname + symref target, newest first. %(symref)
90
- # is non-empty for symbolic refs like origin/HEAD, which must be skipped -
91
- # it is a pointer, not a branch anyone authored.
92
- #
93
- # Separator is \x01 rather than the shell version's '|': an author email or
94
- # a branch name may contain a pipe, and the shell version then mis-split
95
- # the record. Control characters are illegal in ref names, so \x01 is safe.
96
- lines = run_lines(
97
- [
98
- "git",
99
- "for-each-ref",
100
- "--sort=-committerdate",
101
- "--format=%(authoremail)%01%(refname:short)%01%(symref)",
102
- "refs/remotes/origin",
103
- ]
83
+ local = {branch for (branch,) in local_branches()}
84
+
85
+ # One pass: author email + branch name + symref target, newest first.
86
+ # %(symref) is non-empty for symbolic refs like origin/HEAD, which must be
87
+ # skipped - it is a pointer, not a branch anyone authored.
88
+ refs = for_each_ref(
89
+ "refs/remotes/origin",
90
+ "authoremail",
91
+ "refname:lstrip=3",
92
+ "symref",
93
+ sort="-committerdate",
104
94
  )
105
95
 
106
- for line in lines[:limit]:
107
- parts = line.split("\x01")
108
- if len(parts) < 3:
109
- continue
110
- author_email, remote_branch, symref = parts[0], parts[1], parts[2]
111
-
112
- if not remote_branch or symref:
96
+ for author_email, branch, symref in refs[:limit]:
97
+ if symref:
113
98
  continue
114
99
 
115
- author_email = author_email.removeprefix("<").removesuffix(">")
116
- if author_email != args.author:
100
+ if author_email.removeprefix("<").removesuffix(">") != author:
117
101
  not_mine += 1
118
102
  continue
119
103
 
120
- local_branch = remote_branch.removeprefix("origin/")
121
- if not local_branch or local_branch == remote_branch:
122
- continue # malformed entry โ€” don't touch
123
-
124
- exists = run(
125
- ["git", "show-ref", "--verify", "--quiet", f"refs/heads/{local_branch}"],
126
- check=False,
127
- capture=True,
128
- )
129
- if exists.returncode == 0:
130
- info(f"โญ๏ธ Already local: {local_branch}")
104
+ if branch in local:
105
+ info(f"โญ๏ธ Already local: {branch}")
131
106
  skipped += 1
132
107
  else:
133
- log(f"๐Ÿ†• Checking out: {local_branch}")
134
- run(["git", "checkout", "-b", local_branch, "--track", remote_branch])
108
+ log(f"๐Ÿ†• Checking out: {branch}")
109
+ # `git branch --track`, not `checkout -b`: switching to each new
110
+ # branch left HEAD on the last one, undoing the park on main that
111
+ # sync-all-branches runs this after. The full ref name cannot be
112
+ # mistaken for a local branch called `origin/<branch>`.
113
+ run(["git", "branch", "--track", branch, f"refs/remotes/origin/{branch}"])
135
114
  checked_out += 1
136
115
 
137
116
  print()
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env python3
2
2
  """๐Ÿงน Delete local branches whose upstream is gone (the remote branch was deleted).
3
3
 
4
- Protected branches and the current branch are never deleted.
4
+ Protected branches are never deleted, and neither is a branch checked out in a
5
+ worktree - the current one included. Those are reported instead.
5
6
 
6
7
  Usage:
7
8
  clean-stale-branches.py [--dry-run] [--protect=BRANCH1,BRANCH2,...]
@@ -23,62 +24,41 @@ from pathlib import Path
23
24
 
24
25
  sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
25
26
 
27
+ from gitutils import fetch_remotes, local_branches, skip_git_hooks, worktree_branch_map
26
28
  from utils import (
27
29
  add_bool_flag,
28
30
  build_parser,
31
+ dry_run_complete,
29
32
  env_bool,
33
+ heading,
30
34
  info,
31
- log,
32
- note,
33
35
  require_binary,
34
36
  run,
35
37
  run_cli,
36
- run_lines,
37
38
  success,
38
39
  warning,
39
- worktree_branch_map,
40
40
  )
41
41
 
42
- __version__ = "3.1.0"
42
+ __version__ = "3.2.0"
43
43
 
44
44
  DEFAULT_PROTECTED = ("main", "master", "dev", "develop", "staging")
45
45
 
46
46
 
47
- def current_branch() -> str:
48
- """The checked-out branch, or "" when HEAD is detached."""
49
- completed = run(
50
- ["git", "symbolic-ref", "--quiet", "--short", "HEAD"],
51
- check=False,
52
- capture=True,
53
- )
54
- return (completed.stdout or "").strip()
55
-
56
-
57
- def stale_branches(protected: set[str], skip: str) -> list[str]:
47
+ def stale_branches(protected: set[str]) -> list[str]:
58
48
  """Local branches whose upstream is marked ``[gone]``.
59
49
 
60
50
  ``[gone]`` means the remote branch was deleted, which is what a merged and
61
51
  tidied-up PR leaves behind. It does *not* mean merged - a closed-unmerged
62
52
  PR looks identical here, same as in the shell version.
53
+
54
+ The current branch gets no special case: its worktree pins it like any
55
+ other checked-out branch, so ``main`` reports it alongside those.
63
56
  """
64
- # Separator is \x01, not the shell version's '|': a branch may legally be
65
- # named `a|b[gone]`, which made the shell version split the record wrong
66
- # and delete branch `a`. Control characters are illegal in ref names.
67
- found = []
68
- for line in run_lines(
69
- ["git", "for-each-ref", "--format=%(refname:short)%01%(upstream:track)", "refs/heads/"]
70
- ):
71
- branch, _, track = line.partition("\x01")
72
- if not branch:
73
- continue
74
- if branch in protected:
75
- continue
76
- if branch == skip:
77
- note(f"โญ๏ธ Skipping current branch: {branch}")
78
- continue
79
- if "[gone]" in track:
80
- found.append(branch)
81
- return found
57
+ return [
58
+ branch
59
+ for branch, track in local_branches("upstream:track")
60
+ if branch not in protected and "[gone]" in track
61
+ ]
82
62
 
83
63
 
84
64
  def main(argv: list[str] | None = None) -> int:
@@ -101,22 +81,17 @@ def main(argv: list[str] | None = None) -> int:
101
81
  args = parser.parse_args(argv)
102
82
 
103
83
  require_binary("git")
84
+ skip_git_hooks()
104
85
 
105
86
  protected = set(DEFAULT_PROTECTED)
106
87
  protected.update(name for name in args.protect.split(",") if name)
107
88
 
108
- header_suffix = " ๐ŸŒต (dry run)" if args.dry_run else ""
109
- note("โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•")
110
- note(f"๐Ÿงน clean-stale-branches{header_suffix}")
111
- note("โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•")
89
+ heading("๐Ÿงน clean-stale-branches", dry_run=args.dry_run)
112
90
 
113
- info("๐Ÿ“ก Fetching + pruning remotes...")
114
- # Not wrapped: the shell version ran this bare under `set -e`, so a fetch
115
- # failure exited with git's own status (128), not a flattened 1.
116
- run(["git", "fetch", "--all", "--prune", "--quiet"])
91
+ fetch_remotes()
117
92
 
118
93
  info("๐Ÿ” Scanning local branches for gone upstreams...")
119
- stale = stale_branches(protected, current_branch())
94
+ stale = stale_branches(protected)
120
95
 
121
96
  if not stale:
122
97
  success("โœจ No stale branches. All clean! ๐ŸŽ‰")
@@ -124,8 +99,8 @@ def main(argv: list[str] | None = None) -> int:
124
99
 
125
100
  # A branch checked out in a worktree cannot be deleted โ€” git refuses with
126
101
  # "already used by worktree". Split those out so one pinned branch doesn't
127
- # abort the whole run; sync-all-branches collapses worktrees before it gets
128
- # here, so this only bites when the script is run on its own.
102
+ # abort the whole run. Worktrees are the user's, so they are reported and
103
+ # left alone, never removed to free the branch.
129
104
  # One `git worktree list` for the whole set, not one per branch as the
130
105
  # shell's per-branch `worktree_for_branch` did.
131
106
  pinned_by_branch = worktree_branch_map()
@@ -136,7 +111,7 @@ def main(argv: list[str] | None = None) -> int:
136
111
  warning(f"โญ๏ธ {len(pinned)} stale branch(es) are checked out in a worktree:")
137
112
  for branch, path in pinned:
138
113
  warning(f" โ€ข ๐ŸŒฟ {branch} (in {path})")
139
- info("๐Ÿ’ก Run sync-all-branches to push and collapse those worktrees first.")
114
+ info("๐Ÿ’ก Switch off or remove those worktrees, and the next run deletes their branches.")
140
115
 
141
116
  if not deletable:
142
117
  success("โœจ Nothing left to delete. ๐ŸŽ‰")
@@ -146,7 +121,7 @@ def main(argv: list[str] | None = None) -> int:
146
121
  warning(f"๐ŸŒต {len(deletable)} stale branch(es) would be deleted:")
147
122
  for branch in deletable:
148
123
  warning(f" โ€ข ๐Ÿชฆ {branch}")
149
- log("๐Ÿ’ก Dry run complete. Nothing was actually deleted.")
124
+ dry_run_complete("deleted")
150
125
  return 0
151
126
 
152
127
  for branch in deletable:
@@ -22,24 +22,22 @@ from pathlib import Path
22
22
 
23
23
  sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
24
24
 
25
+ from gitutils import park_main_worktree, skip_git_hooks, worktrees
25
26
  from utils import (
26
27
  add_bool_flag,
27
28
  build_parser,
29
+ dry_run_complete,
28
30
  env_bool,
29
- find_main_branch,
31
+ heading,
30
32
  info,
31
- log,
32
- note,
33
33
  require_binary,
34
34
  run,
35
35
  run_cli,
36
- run_output,
37
36
  success,
38
37
  warning,
39
- worktree_paths,
40
38
  )
41
39
 
42
- __version__ = "1.2.0"
40
+ __version__ = "1.3.0"
43
41
 
44
42
 
45
43
  def main(argv: list[str] | None = None) -> int:
@@ -61,60 +59,23 @@ def main(argv: list[str] | None = None) -> int:
61
59
  args = parser.parse_args(argv)
62
60
 
63
61
  require_binary("git")
62
+ # Parking the main worktree is a `git checkout`, which fires post-checkout.
63
+ skip_git_hooks()
64
64
 
65
- inside = run(["git", "rev-parse", "--is-inside-work-tree"], check=False, capture=True)
66
- if inside.returncode != 0:
67
- warning("โŒ Not inside a git working tree.")
68
- return 1
69
-
70
- header_suffix = " ๐ŸŒต (dry run)" if args.dry_run else ""
71
- note("โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•")
72
- note(f"๐ŸŒฟ prune-worktrees{header_suffix}")
73
- note("โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•")
65
+ heading("๐ŸŒฟ prune-worktrees", dry_run=args.dry_run)
74
66
 
75
67
  # Work from the main worktree: removing a worktree fails while standing
76
- # inside it, and starting from a known-good branch keeps this predictable.
77
- paths = worktree_paths()
78
- if not paths:
79
- warning("โŒ Could not determine main worktree.")
80
- return 1
81
-
82
- main_wt = Path(paths[0])
83
- linked = [p for p in paths[1:] if Path(p) != main_wt]
84
-
85
- main_branch = find_main_branch(main_wt)
86
- if not main_branch:
87
- warning("โŒ Neither 'main' nor 'master' exists locally โ€” refusing to prune.")
88
- return 1
89
-
90
- current = run_output(
91
- ["git", "symbolic-ref", "--quiet", "--short", "HEAD"], check=False, cwd=main_wt
92
- ).strip()
93
-
94
- if current != main_branch:
95
- # Only block on tracked-file changes: untracked files (including linked
96
- # worktrees nested under the main worktree) won't be touched by checkout.
97
- dirty = run_output(["git", "status", "--porcelain", "--untracked-files=no"], cwd=main_wt)
98
- if dirty:
99
- warning("โŒ Main worktree has uncommitted changes โ€” commit or stash before pruning. ๐Ÿงบ")
100
- return 1
101
- if args.dry_run:
102
- info(
103
- f"๐ŸŒต Would switch main worktree to ๐ŸŒฟ {main_branch} (currently on {current or 'detached'})"
104
- )
105
- else:
106
- info(f"๐Ÿ”€ Switching main worktree to ๐ŸŒฟ {main_branch} (from {current or 'detached'})")
107
- run(["git", "checkout", "--quiet", main_branch], cwd=main_wt)
108
- else:
109
- info(f"๐Ÿ“ Main worktree already on ๐ŸŒฟ {main_branch}")
68
+ # inside it.
69
+ main_wt = park_main_worktree("prune", dry_run=args.dry_run)
110
70
 
111
71
  info("๐Ÿ” Listing worktrees...")
72
+ linked = [worktree.path for worktree in worktrees(main_wt)[1:]]
112
73
 
113
74
  if not linked:
114
75
  success("โœจ No linked worktrees. Running prune anyway... ๐ŸŽ‰")
115
76
  if args.dry_run:
116
77
  run(["git", "worktree", "prune", "--dry-run", "--verbose"], check=False, cwd=main_wt)
117
- log("๐Ÿ’ก Dry run complete. Nothing was actually pruned.")
78
+ dry_run_complete("pruned")
118
79
  else:
119
80
  run(["git", "worktree", "prune", "--verbose"], cwd=main_wt)
120
81
  success("โœจ Pruned stale worktree records. ๐ŸŽ‰")
@@ -125,7 +86,7 @@ def main(argv: list[str] | None = None) -> int:
125
86
  for path in linked:
126
87
  warning(f" โ€ข ๐Ÿชฆ {path}")
127
88
  info("๐Ÿ“œ Would also run: git worktree prune --verbose")
128
- log("๐Ÿ’ก Dry run complete. Nothing was actually removed.")
89
+ dry_run_complete("removed")
129
90
  return 0
130
91
 
131
92
  remove_args = ["--force"] if args.force else []