@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
@@ -0,0 +1,263 @@
1
+ """Git helpers for the scripts under ``bin/git/``.
2
+
3
+ Split out of ``utils.py``, which keeps the logging, argument parsing and
4
+ subprocess plumbing every script shares. Scripts pick it up the same way::
5
+
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
10
+ from gitutils import skip_git_hooks, worktree_branch_map
11
+
12
+ Standard library only, like everything else at the ``bin/`` root.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import os
18
+ import sys
19
+ from pathlib import Path
20
+ from typing import NamedTuple
21
+
22
+ from utils import info, run, run_output, warning
23
+
24
+
25
+ def skip_git_hooks() -> None:
26
+ """Stop every git this process runs, and every script it spawns, running hooks.
27
+
28
+ The branch scripts only check out, rebase and delete branches. A repo's
29
+ hooks (lefthook, husky) gate the user's own commits and pushes, yet
30
+ ``git checkout`` fires post-checkout and ``git rebase`` fires pre-rebase,
31
+ prepare-commit-msg, post-commit and post-rewrite as it replays each
32
+ commit. A hooks path of /dev/null holds no hooks at all.
33
+
34
+ It is set through GIT_CONFIG_COUNT (git 2.31+) rather than ``-c`` on each
35
+ call so no git invocation can miss it and a delegated script inherits it.
36
+ Repeat calls are no-ops, so a script and the scripts it delegates to can
37
+ all call it.
38
+ """
39
+ count = int(os.environ.get("GIT_CONFIG_COUNT") or 0)
40
+ for index in range(count):
41
+ if (
42
+ os.environ.get(f"GIT_CONFIG_KEY_{index}") == "core.hooksPath"
43
+ and os.environ.get(f"GIT_CONFIG_VALUE_{index}") == os.devnull
44
+ ):
45
+ return
46
+ os.environ[f"GIT_CONFIG_KEY_{count}"] = "core.hooksPath"
47
+ os.environ[f"GIT_CONFIG_VALUE_{count}"] = os.devnull
48
+ os.environ["GIT_CONFIG_COUNT"] = str(count + 1)
49
+
50
+
51
+ # Set once the remotes are fetched, and inherited by every script delegated to
52
+ # after that, so a sync fetches once rather than once per step.
53
+ _FETCHED = "CHE_REMOTES_FETCHED"
54
+
55
+
56
+ def fetch_remotes() -> None:
57
+ """``git fetch --all --prune``, once per run however many scripts ask.
58
+
59
+ sync-all-branches fetches, then delegates to three scripts that each fetch
60
+ when run on their own. Like :func:`skip_git_hooks`, the marker lives in the
61
+ environment, so a delegated script inherits it and skips its own fetch.
62
+ """
63
+ if os.environ.get(_FETCHED):
64
+ return
65
+ info("📡 Fetching + pruning remotes...")
66
+ # Not wrapped: the shell version ran this bare under `set -e`, so a fetch
67
+ # failure exited with git's own status (128), not a flattened 1.
68
+ run(["git", "fetch", "--all", "--prune", "--quiet"])
69
+ os.environ[_FETCHED] = "1"
70
+
71
+
72
+ # ---------------------------------------------------------------------------
73
+ # Refs
74
+ # ---------------------------------------------------------------------------
75
+
76
+
77
+ def current_branch(cwd: str | Path | None = None) -> str:
78
+ """The branch checked out in ``cwd``'s worktree, or "" when HEAD is detached.
79
+
80
+ The full ref minus ``refs/heads/``, not ``--short``, which reads ``heads/x``
81
+ whenever a tag ``x`` exists (see :func:`local_branches`).
82
+ """
83
+ ref = run_output(["git", "symbolic-ref", "--quiet", "HEAD"], check=False, cwd=cwd)
84
+ return ref.removeprefix("refs/heads/")
85
+
86
+
87
+ def has_tracked_changes(cwd: str | Path | None = None) -> bool:
88
+ """True when ``cwd``'s worktree has uncommitted changes to tracked files.
89
+
90
+ Untracked files do not count: checkout and rebase leave them alone, and a
91
+ linked worktree nested inside the main one shows up as untracked.
92
+ """
93
+ return bool(run_output(["git", "status", "--porcelain", "--untracked-files=no"], cwd=cwd))
94
+
95
+
96
+ def for_each_ref(
97
+ pattern: str, *fields: str, sort: str = "", cwd: str | Path | None = None
98
+ ) -> list[list[str]]:
99
+ """``git for-each-ref`` over ``pattern``, one list of ``fields`` per ref.
100
+
101
+ Each field is a format atom without its ``%(...)``, so
102
+ ``for_each_ref("refs/remotes/origin", "refname:lstrip=3", "symref")``
103
+ yields ``[branch, symref]`` pairs. ``sort`` is passed as ``--sort``.
104
+
105
+ DELIBERATE DIVERGENCE: the shell versions joined fields with ``|``. A
106
+ branch name may legally contain a pipe, and so may an author email, so a
107
+ record like ``a|b[gone]`` split wrong - reporting an upstream a branch did
108
+ not have, or deleting branch ``a``. The fields are joined with \\x01
109
+ instead: control characters are illegal in ref names, so it cannot collide.
110
+ """
111
+ argv = ["git", "for-each-ref", "--format=" + "%01".join(f"%({field})" for field in fields)]
112
+ if sort:
113
+ argv.append(f"--sort={sort}")
114
+ # Another divergence: the shell scripts read refs through `< <(...)`, whose
115
+ # failure does not trip `set -e`, so a failing for-each-ref looked like "no
116
+ # branches" and the script exited 0. Here a failure raises.
117
+ output = run_output([*argv, pattern], cwd=cwd)
118
+ return [line.split("\x01") for line in output.split("\n") if line]
119
+
120
+
121
+ def local_branches(*fields: str, sort: str = "", cwd: str | Path | None = None) -> list[list[str]]:
122
+ """Every local branch as ``[name, *fields]``, through :func:`for_each_ref`.
123
+
124
+ The name is ``refname:lstrip=2``, not ``refname:short``: the short form
125
+ becomes ``heads/x`` whenever ``x`` is ambiguous (a tag named like the
126
+ branch, or a local branch called ``origin/x``), and ``git branch -D
127
+ heads/x`` then fails while ``git checkout heads/x`` detaches HEAD.
128
+ """
129
+ return for_each_ref("refs/heads/", "refname:lstrip=2", *fields, sort=sort, cwd=cwd)
130
+
131
+
132
+ # ---------------------------------------------------------------------------
133
+ # Worktrees
134
+ #
135
+ # A branch checked out in a linked worktree cannot be checked out, deleted or
136
+ # reset from the main clone - git refuses with "already used by worktree".
137
+ # That is a mechanical limit, not a conflict, so every script that touches
138
+ # branches needs to know which ones are pinned and where.
139
+ # ---------------------------------------------------------------------------
140
+
141
+
142
+ class Worktree(NamedTuple):
143
+ path: str
144
+ branch: str # "" when detached (or bare): it pins no branch
145
+
146
+
147
+ def worktrees(cwd: str | Path | None = None) -> list[Worktree]:
148
+ """Every worktree, in ``git worktree list --porcelain`` order.
149
+
150
+ The first entry is always the main worktree; callers drop it themselves so
151
+ the distinction stays explicit at the call site.
152
+
153
+ The shell version parsed this with ``awk '{print $2}'``, which splits on
154
+ whitespace and so truncated any worktree path containing a space. Taking
155
+ the whole remainder of the line keeps such paths intact.
156
+ """
157
+ found: list[Worktree] = []
158
+ for line in run_output(["git", "worktree", "list", "--porcelain"], cwd=cwd).splitlines():
159
+ if line.startswith("worktree "):
160
+ found.append(Worktree(line.removeprefix("worktree "), ""))
161
+ elif line.startswith("branch refs/heads/"):
162
+ # Every record opens with its `worktree` line, so this belongs to
163
+ # the last one appended.
164
+ found[-1] = found[-1]._replace(branch=line.removeprefix("branch refs/heads/"))
165
+ return found
166
+
167
+
168
+ def worktree_branch_map(cwd: str | Path | None = None) -> dict[str, str]:
169
+ """Map ``branch -> worktree path`` for every worktree with a branch checked out.
170
+
171
+ Detached worktrees are skipped (they pin no branch). The main worktree IS
172
+ included, because its branch is pinned for exactly the same reason.
173
+
174
+ DELIBERATE DIVERGENCE: ``utils.sh`` emitted "<branch>\\t<path>" lines that
175
+ every caller re-parsed. A dict is the same data without the round trip, and
176
+ it cannot be truncated by a path containing a tab.
177
+ """
178
+ mapping: dict[str, str] = {}
179
+ for worktree in worktrees(cwd):
180
+ if worktree.branch:
181
+ # First writer wins: a branch can only be pinned by one worktree,
182
+ # so a duplicate would mean git contradicted itself.
183
+ mapping.setdefault(worktree.branch, worktree.path)
184
+ return mapping
185
+
186
+
187
+ def worktree_is_clean(path: str | Path) -> bool:
188
+ """True when the worktree at ``path`` has no modified or untracked files.
189
+
190
+ Ignored build output (node_modules, .next) does not count as dirty, because
191
+ ``git status --porcelain`` does not list ignored paths.
192
+ """
193
+ completed = run(["git", "-C", str(path), "status", "--porcelain"], check=False, capture=True)
194
+ # A non-zero exit means the path is not a working tree at all. Treating
195
+ # that as "clean" would let a caller delete a directory it never inspected.
196
+ if completed.returncode != 0:
197
+ return False
198
+ return not (completed.stdout or "").strip()
199
+
200
+
201
+ def find_main_branch(cwd: str | Path | None = None) -> str:
202
+ """The local main branch, preferring ``main`` over ``master``, or "" for neither.
203
+
204
+ Takes an explicit ``cwd`` because callers run it after moving to the main
205
+ worktree; relying on the process working directory would make the answer
206
+ depend on where the script happened to be invoked from.
207
+ """
208
+ for candidate in ("main", "master"):
209
+ exists = run(
210
+ ["git", "show-ref", "--verify", "--quiet", f"refs/heads/{candidate}"],
211
+ check=False,
212
+ capture=True,
213
+ cwd=cwd,
214
+ )
215
+ if exists.returncode == 0:
216
+ return candidate
217
+ return ""
218
+
219
+
220
+ def park_main_worktree(action: str, *, dry_run: bool) -> Path:
221
+ """Put the main worktree on main (or master) before ``action``; return its path.
222
+
223
+ The main worktree, not the one the script was run from: removing a worktree
224
+ fails while standing in it, and the branch scripts expect the main clone.
225
+ Starting from a known-good branch keeps every later step predictable.
226
+
227
+ Exits 1 outside a working tree, when there is neither a ``main`` nor a
228
+ ``master``, or when the main worktree has uncommitted changes to tracked
229
+ files (see :func:`has_tracked_changes`). ``dry_run`` reports the switch
230
+ instead of making it.
231
+ """
232
+ # "true" on stdout, not just exit 0: inside a bare repo or a .git directory
233
+ # rev-parse succeeds and prints "false".
234
+ if run_output(["git", "rev-parse", "--is-inside-work-tree"], check=False) != "true":
235
+ warning("❌ Not inside a git working tree.")
236
+ sys.exit(1)
237
+
238
+ main_wt = Path(worktrees()[0].path)
239
+
240
+ main_branch = find_main_branch(main_wt)
241
+ if not main_branch:
242
+ warning(f"❌ Neither 'main' nor 'master' exists locally — refusing to {action}.")
243
+ sys.exit(1)
244
+
245
+ current = current_branch(main_wt)
246
+ if current == main_branch:
247
+ info(f"📍 Main worktree already on 🌿 {main_branch}")
248
+ return main_wt
249
+
250
+ if has_tracked_changes(main_wt):
251
+ warning(
252
+ f"❌ Main worktree has uncommitted changes — commit or stash before you {action}. 🧺"
253
+ )
254
+ sys.exit(1)
255
+
256
+ if dry_run:
257
+ info(
258
+ f"🌵 Would switch main worktree to 🌿 {main_branch} (currently on {current or 'detached'})"
259
+ )
260
+ else:
261
+ info(f"🔀 Switching main worktree to 🌿 {main_branch} (from {current or 'detached'})")
262
+ run(["git", "checkout", "--quiet", main_branch], cwd=main_wt)
263
+ return main_wt