git-rexec 1.0.0__py3-none-any.whl

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,513 @@
1
+ #!python
2
+ #
3
+ # Optional dependencies:
4
+ # - colorama
5
+ # - setproctitle
6
+ #
7
+ # Author: James Cherti
8
+ # URL: https://github.com/jamescherti/git-rexec
9
+ #
10
+ # Copyright (C) 2019-2026 James Cherti
11
+ #
12
+ # This program is free software: you can redistribute it and/or modify it under
13
+ # the terms of the GNU General Public License as published by the Free Software
14
+ # Foundation, either version 3 of the License, or (at your option) any later
15
+ # version.
16
+ #
17
+ # This program is distributed in the hope that it will be useful, but WITHOUT
18
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
19
+ # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
20
+ # details.
21
+ #
22
+ # You should have received a copy of the GNU General Public License along with
23
+ # this program. If not, see <https://www.gnu.org/licenses/>.
24
+ #
25
+ """Find Git repositories and execute commands against them in parallel.
26
+
27
+ This script allows for finding Git repositories within a directory structure
28
+ and executing commands against them. It supports conditional filtering,
29
+ background execution, and sequential foreground execution.
30
+ """
31
+
32
+ import argparse
33
+ import os
34
+ import shlex
35
+ import shutil
36
+ import subprocess
37
+ import sys
38
+ import textwrap
39
+ from concurrent.futures import Future, ThreadPoolExecutor, as_completed
40
+ from dataclasses import dataclass
41
+ from multiprocessing import cpu_count
42
+ from pathlib import Path
43
+ from typing import Optional
44
+
45
+ try:
46
+ from colorama import Fore
47
+ from colorama import init as colorama_init
48
+ HAS_COLORAMA: bool = True
49
+ except ImportError:
50
+ HAS_COLORAMA = False
51
+
52
+ class Fore: # type: ignore
53
+ YELLOW: str = ""
54
+ RED: str = ""
55
+ RESET: str = ""
56
+
57
+
58
+ @dataclass
59
+ class CommandResult:
60
+ """Store the result of a subprocess command execution.
61
+
62
+ :param command: The command that was executed.
63
+ :param returncode: The exit status of the command.
64
+ :param stdout: Standard output content.
65
+ :param stderr: Standard error content.
66
+ """
67
+ command: list[str]
68
+ returncode: int
69
+ stdout: str = ""
70
+ stderr: str = ""
71
+
72
+
73
+ @dataclass
74
+ class RepoContext:
75
+ """Represent a processed Git repository and its execution states.
76
+
77
+ :param path: The absolute path to the repository root.
78
+ :param parallel_result: The result of the parallel execution, if any.
79
+ """
80
+ path: Path
81
+ parallel_result: Optional[CommandResult] = None
82
+
83
+
84
+ def git_toplevel(repo_path: Path) -> Optional[Path]:
85
+ """Return the absolute path to the top-level directory of a Git repository.
86
+
87
+ :param repo_path: Path inside the Git repository.
88
+ :return: Path to the repository's top-level directory, or None if not a
89
+ Git repo.
90
+ """
91
+ try:
92
+ proc: subprocess.CompletedProcess[str] = subprocess.run(
93
+ ["git", "-C", str(repo_path), "rev-parse", "--show-toplevel"],
94
+ capture_output=True,
95
+ text=True,
96
+ check=True
97
+ )
98
+ return Path(proc.stdout.strip()).resolve()
99
+ except subprocess.CalledProcessError:
100
+ return None
101
+
102
+
103
+ def git_fd_find_repo(path: Path, max_workers: int) -> set[Path]:
104
+ """Find all Git repositories in 'path' using fd.
105
+
106
+ :param path: The root directory to search for Git repositories.
107
+ :param max_workers: Maximum number of threads for fd.
108
+ :return: set of paths to Git repositories.
109
+ """
110
+ path_str: str = str(path)
111
+ try:
112
+ # Use fd to find .git directories efficiently
113
+ proc: subprocess.CompletedProcess[str] = subprocess.run(
114
+ [
115
+ 'fd', '--type', 'd', '--hidden',
116
+ '--no-ignore', '^\\.git$',
117
+ '--absolute-path',
118
+ '--threads', str(max_workers),
119
+ path_str
120
+ ],
121
+ capture_output=True,
122
+ text=True,
123
+ check=False
124
+ )
125
+ except FileNotFoundError as err:
126
+ print(f"Error: {err}", file=sys.stderr)
127
+ sys.exit(1)
128
+
129
+ # Use a set comprehension to remove duplicates and normalize paths
130
+ # efficiently
131
+ return {
132
+ Path(git_dir).parent
133
+ for git_dir in proc.stdout.splitlines()
134
+ }
135
+
136
+
137
+ def git_py_find_repo(root: Path) -> set[Path]:
138
+ """Recursively find all Git repositories under the root path using Python.
139
+
140
+ :param root: Directory to search under.
141
+ :return: set of paths to Git repositories.
142
+ """
143
+ return {
144
+ git_dir.absolute().parent
145
+ for git_dir in root.rglob(".git")
146
+ if git_dir.is_dir()
147
+ }
148
+
149
+
150
+ def run_command_get_output(repo_path: Path, cmd_list: list[str],
151
+ capture: bool = True) -> CommandResult:
152
+ """Execute a shell command within a specific directory.
153
+
154
+ :param repo_path: The directory in which to execute the command.
155
+ :param cmd_list: The command list to execute.
156
+ :param capture: Whether to capture stdout/stderr.
157
+ :return: The result of the command execution.
158
+ """
159
+ if not cmd_list:
160
+ return CommandResult(command=[], returncode=0)
161
+
162
+ try:
163
+ proc: subprocess.CompletedProcess[str] = subprocess.run(
164
+ cmd_list,
165
+ cwd=repo_path,
166
+ capture_output=capture,
167
+ text=True,
168
+ check=False
169
+ )
170
+ return CommandResult(
171
+ command=cmd_list,
172
+ returncode=proc.returncode,
173
+ stdout=proc.stdout if proc.stdout else "",
174
+ stderr=proc.stderr if proc.stderr else ""
175
+ )
176
+ except FileNotFoundError:
177
+ return CommandResult(
178
+ command=cmd_list,
179
+ returncode=127,
180
+ stderr=f"Error: Command not found: '{cmd_list[0]}'\n"
181
+ )
182
+
183
+
184
+ def format_parallel_output(
185
+ repo_path: Path, result: CommandResult, quiet: bool = False) -> str:
186
+ """Format the output of a parallel command for display.
187
+
188
+ :param repo_path: The repository path.
189
+ :param result: The execution result.
190
+ :param quiet: Suppress informational headers.
191
+ :return: Formatted string ready for printing.
192
+ """
193
+ raw_output: str = result.stdout + result.stderr
194
+ if not raw_output and result.returncode == 0:
195
+ return ""
196
+
197
+ formatted: str = raw_output.replace("\t", " ").rstrip()
198
+ if formatted:
199
+ formatted += "\n"
200
+
201
+ if quiet:
202
+ return formatted
203
+
204
+ header: str = f"{Fore.YELLOW}[EXEC-P] {repo_path}"
205
+ cmd_str: str = " ".join(result.command)
206
+ header += f": {cmd_str}{Fore.RESET}\n"
207
+
208
+ indent: str = " " * 4
209
+ indented_body: str = textwrap.indent(formatted, prefix=indent)
210
+ return header + indented_body
211
+
212
+
213
+ def process_repo(repo_path: Path, exec_parallel_cmd: Optional[list[str]],
214
+ if_exec_cmd: Optional[list[str]]) -> Optional[RepoContext]:
215
+ """Process a single repository: check conditions and run parallel commands.
216
+
217
+ :param repo_path: The repository path.
218
+ :param exec_parallel_cmd: Command list to execute in background/parallel.
219
+ :param if_exec_cmd: Command list to check before processing (filter).
220
+ :return: RepoContext if processed successfully, None if filtered out.
221
+ """
222
+ # Filter: if-exec
223
+ if_exec_cmd_list: Optional[list[str]] = if_exec_cmd
224
+ if if_exec_cmd_list:
225
+ # We discard output for the filter check, only caring about exit code
226
+ filter_res: CommandResult = run_command_get_output(
227
+ repo_path, if_exec_cmd_list, capture=True
228
+ )
229
+ if filter_res.returncode != 0:
230
+ return None
231
+
232
+ # Action: exec-parallel
233
+ parallel_result: Optional[CommandResult] = None
234
+ exec_parallel_cmd_list: Optional[list[str]] = exec_parallel_cmd
235
+ if exec_parallel_cmd_list:
236
+ parallel_result = run_command_get_output(
237
+ repo_path, exec_parallel_cmd_list, capture=True
238
+ )
239
+
240
+ return RepoContext(path=repo_path, parallel_result=parallel_result)
241
+
242
+
243
+ def discover_repos(directory: Path,
244
+ max_workers: int,
245
+ exclude_dirs: list[str]) -> set[Path]:
246
+ """Discover Git repositories starting from 'directory' and apply exclusions.
247
+
248
+ :param directory: The root directory for search.
249
+ :param max_workers: Maximum number of threads/workers.
250
+ :param exclude_dirs: Directories to exclude.
251
+ :return: A set of repository root paths.
252
+ """
253
+ repos: set[Path] = set()
254
+
255
+ # Discover all repositories
256
+ toplevel: Optional[Path] = git_toplevel(directory)
257
+ if toplevel:
258
+ repos = {toplevel}
259
+ elif shutil.which("fd"):
260
+ repos = git_fd_find_repo(directory, max_workers)
261
+ else:
262
+ repos = git_py_find_repo(directory)
263
+
264
+ # Filter out excluded directories
265
+ if not exclude_dirs:
266
+ return repos
267
+
268
+ list_ex_paths: list[Path] = [Path(ex_dir).resolve()
269
+ for ex_dir in exclude_dirs]
270
+ return {
271
+ repo for repo in repos
272
+ if not any(repo.resolve().is_relative_to(ex_path)
273
+ for ex_path in list_ex_paths)
274
+ }
275
+
276
+
277
+ def execute_parallel_tasks(
278
+ repos: set[Path],
279
+ exec_parallel: Optional[list[str]],
280
+ if_exec: Optional[list[str]],
281
+ max_workers: int
282
+ ) -> list[RepoContext]:
283
+ """Run discovery and parallel execution tasks.
284
+
285
+ :param repos: set of repositories to process.
286
+ :param exec_parallel: Command list for background execution.
287
+ :param if_exec: Command list for conditional filtering.
288
+ :param max_workers: Maximum number of threads for execution.
289
+ :return: list of processed repository contexts.
290
+ """
291
+ results: list[RepoContext] = []
292
+
293
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
294
+ futures: dict[Future[Optional[RepoContext]], Path] = {
295
+ executor.submit(
296
+ process_repo, repo, exec_parallel, if_exec
297
+ ): repo for repo in repos
298
+ }
299
+
300
+ for future in as_completed(futures):
301
+ repo_path: Path = futures[future]
302
+ try:
303
+ result: Optional[RepoContext] = future.result()
304
+ if result:
305
+ results.append(result)
306
+ except Exception as exc:
307
+ print(f"Error processing repository {repo_path}: {exc}",
308
+ file=sys.stderr)
309
+
310
+ return results
311
+
312
+
313
+ def parse_args() -> argparse.Namespace:
314
+ """Parse command-line arguments.
315
+
316
+ :return: Parsed arguments.
317
+ """
318
+ parser: argparse.ArgumentParser = argparse.ArgumentParser(
319
+ description="Find git repos and execute commands.",
320
+ formatter_class=argparse.RawDescriptionHelpFormatter
321
+ )
322
+ parser.add_argument(
323
+ "-C", "--directory",
324
+ type=Path,
325
+ default=Path("."),
326
+ help="Root directory to search (defaults to current directory)"
327
+ )
328
+ parser.add_argument(
329
+ "--exclude-dir",
330
+ action="append",
331
+ default=[],
332
+ help="Exclude a specific directory and all of its subdirectories"
333
+ )
334
+ parser.add_argument(
335
+ "-p", "--parallel",
336
+ action="store_true",
337
+ help="Execute the command in parallel using threads",
338
+ default=False
339
+ )
340
+ parser.add_argument(
341
+ "-i", "--if-exec",
342
+ type=str,
343
+ help="Execute commands only if this check returns exit code 0.",
344
+ default=None
345
+ )
346
+ parser.add_argument(
347
+ "-j", "--jobs",
348
+ type=int,
349
+ dest="max_workers",
350
+ help="Maximum number of processors/workers to use",
351
+ default=(cpu_count() or 1)
352
+ )
353
+ parser.add_argument(
354
+ "-q", "--quiet",
355
+ action="store_true",
356
+ help=("Quiet mode. Suppresses the informational tracking headers "
357
+ "([EXEC] and [EXEC-P]) that prefix execution output."),
358
+ default=False
359
+ )
360
+ parser.add_argument(
361
+ "exec_cmd",
362
+ type=str,
363
+ nargs="*",
364
+ help="The command to execute. You can use -- to pass options."
365
+ )
366
+
367
+ args: argparse.Namespace = parser.parse_args()
368
+
369
+ return args
370
+
371
+
372
+ def print_error_summary(errors: list[tuple[Path, CommandResult]]) -> int:
373
+ """Print a summary of execution errors.
374
+
375
+ :param errors: list of tuples containing path and result.
376
+ :return: The final exit code (1 if errors exist, else 0).
377
+ """
378
+ if not errors:
379
+ return 0
380
+
381
+ print()
382
+ print(f"{Fore.RED}Errors:{Fore.RESET}")
383
+
384
+ final_errno: int = 0
385
+ for repo_path, result in errors:
386
+ cmd_display: str = " ".join(result.command)
387
+ if result.returncode != 0:
388
+ final_errno = 1
389
+ if result.returncode == 127:
390
+ msg: str = "Command not found"
391
+ else:
392
+ msg = f"errno {result.returncode}"
393
+
394
+ print(
395
+ f"{Fore.RED} - {repo_path}: {msg}: "
396
+ f"{cmd_display}{Fore.RESET}"
397
+ )
398
+
399
+ return final_errno
400
+
401
+
402
+ def main() -> None:
403
+ """Execute the main command-line interface."""
404
+ # Optional: setproctitle
405
+ try:
406
+ # pylint: disable=import-outside-toplevel
407
+ from setproctitle import setproctitle
408
+ setproctitle(Path(sys.argv[0]).name) # type: ignore
409
+ except ImportError:
410
+ # Optional dependency 'setproctitle' is not installed.
411
+ pass
412
+
413
+ if HAS_COLORAMA:
414
+ colorama_init()
415
+
416
+ # Disable git prompting
417
+ os.environ["GIT_TERMINAL_PROMPT"] = "0"
418
+
419
+ args: argparse.Namespace = parse_args()
420
+
421
+ # Verify that the execution command exists
422
+ if args.exec_cmd and not shutil.which(args.exec_cmd[0]):
423
+ print(
424
+ f"Error: Command not found: '{args.exec_cmd[0]}'",
425
+ file=sys.stderr)
426
+ sys.exit(127)
427
+
428
+ # Discover Repositories
429
+ repos: set[Path] = discover_repos(args.directory.absolute(),
430
+ args.max_workers,
431
+ args.exclude_dir)
432
+
433
+ # setup background command list if needed
434
+ exec_parallel_cmd: Optional[list[str]] = None
435
+ if args.parallel and args.exec_cmd:
436
+ exec_parallel_cmd = args.exec_cmd
437
+
438
+ if_exec_cmd: Optional[list[str]] = None
439
+ if args.if_exec:
440
+ if_exec_cmd = shlex.split(args.if_exec)
441
+
442
+ # Parallel Processing (Filter + Background Exec)
443
+ processed_repos: list[RepoContext] = execute_parallel_tasks(
444
+ repos,
445
+ exec_parallel_cmd,
446
+ if_exec_cmd,
447
+ args.max_workers
448
+ )
449
+
450
+ execution_errors: list[tuple[Path, CommandResult]] = []
451
+
452
+ # Main Loop: Display results and run sequential commands
453
+ for context in processed_repos:
454
+ repo_path: Path = context.path
455
+
456
+ # Handle Background Execution Results
457
+ if context.parallel_result:
458
+ output_display: str = format_parallel_output(
459
+ repo_path, context.parallel_result, args.quiet)
460
+ if output_display:
461
+ print(output_display, end="")
462
+
463
+ if context.parallel_result.returncode != 0:
464
+ execution_errors.append((repo_path, context.parallel_result))
465
+
466
+ # Handle Foreground Execution
467
+ if args.exec_cmd and not args.parallel:
468
+ if not args.quiet:
469
+ print(
470
+ f"{Fore.YELLOW}[EXEC] {repo_path}: "
471
+ f"{subprocess.list2cmdline(args.exec_cmd)}"
472
+ f"{Fore.RESET}"
473
+ )
474
+
475
+ # Run interactively/sequentially (capture=False allows interaction)
476
+ # However, for consistency with error tracking, we might want to
477
+ # capture. Usually --exec implies seeing output immediately.
478
+ try:
479
+ # We use subprocess.check_call to allow direct stdout/stderr
480
+ # flow unless we want to capture for error summary. To match
481
+ # previous logic, we let it flow to stdout.
482
+ subprocess.check_call(
483
+ args.exec_cmd,
484
+ cwd=repo_path
485
+ )
486
+ except subprocess.CalledProcessError as err:
487
+ # Construct a dummy result for the error summary
488
+ failed_res: CommandResult = CommandResult(
489
+ command=args.exec_cmd,
490
+ returncode=err.returncode
491
+ )
492
+ execution_errors.append((repo_path, failed_res))
493
+ except FileNotFoundError:
494
+ failed_res: CommandResult = CommandResult(
495
+ command=args.exec_cmd,
496
+ returncode=127
497
+ )
498
+ execution_errors.append((repo_path, failed_res))
499
+
500
+ # If no commands were run, just list the repo
501
+ if not args.exec_cmd:
502
+ print(repo_path)
503
+
504
+ # Final Error Summary
505
+ sys.exit(print_error_summary(execution_errors))
506
+
507
+
508
+ if __name__ == "__main__":
509
+ try:
510
+ main()
511
+ except (KeyboardInterrupt, BrokenPipeError):
512
+ print("\nInterrupting...", file=sys.stderr)
513
+ sys.exit(1)
@@ -0,0 +1,123 @@
1
+ Metadata-Version: 2.4
2
+ Name: git-rexec
3
+ Version: 1.0.0
4
+ Summary: Locate Git repositories and execute commands against them in parallel.
5
+ Author: James Cherti
6
+ License: GPL-3.0-or-later
7
+ Project-URL: Homepage, https://github.com/jamescherti/git-rexec
8
+ Keywords: git,parallel,executor,automation
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Environment :: Console
13
+ Classifier: Topic :: Software Development :: Version Control :: Git
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+ Provides-Extra: extras
17
+ Requires-Dist: colorama; extra == "extras"
18
+ Requires-Dist: setproctitle; extra == "extras"
19
+
20
+ # git-rexec: Find Git Repositories and Execute Commands Against Them, either Sequentially or in Parallel
21
+
22
+ The [git-rexec](https://github.com/jamescherti/git-rexec/) command-line tool that recursively locates Git repositories within a directory and executes commands against them, either sequentially or in parallel.
23
+
24
+ Here are examples demonstrating how to use `git-rexec`:
25
+ - Execute `git status -s` across all discovered Git repositories (found by searching recursively under the current working directory) in parallel (`-p` or `--parallel`):
26
+ ```
27
+ git-rexec -p -- git status -s
28
+ ```
29
+
30
+ - Fetch updates across all discovered repositories, limiting the concurrency to 5 background jobs (`-j 5`), which helps avoid network congestion or server rate limits when communicating with upstream Git remotes:
31
+ ```bash
32
+ git-rexec -j 5 --parallel -- git fetch
33
+ ```
34
+
35
+ - Target a specific base directory (`~/projects`) using the `-C` flag to recursively discover repositories within it, while explicitly excluding a specific subfolder (`~/projects/archive`). This example executes `git status -s` in parallel for all discovered repositories except those within the excluded path:
36
+ ```bash
37
+ git-rexec -C ~/projects --exclude-dir ~/projects/archive --parallel -- git status -s
38
+ ```
39
+
40
+ - Evaluate whether a `README.md` file exists in the repository (`sh -c "test -f README.md"`). If the condition returns an exit status of 0 (success), it counts the number of lines in that file (`wc -l README.md`):
41
+ ```bash
42
+ git-rexec --if-exec 'sh -c "test -f README.md"' --parallel -- wc -l README.md
43
+ ```
44
+
45
+ If this helps your workflow, please support the project by **⭐ starring git-rexec on GitHub** and sharing it on your website, blog, Mastodon, Reddit, X, LinkedIn, or other social media platforms to help more Git users discover its benefits.
46
+
47
+ ## Features
48
+
49
+ - Recursively discover Git repositories starting from a specified root directory.
50
+ - Execute shell commands across multiple repositories in parallel using worker threads.
51
+ - Filter target repositories based on the exit code of a conditional check (`--if-exec`).
52
+ - Exclude specific directories from the search path.
53
+ - Optional: Can leverage `fd` for fast directory traversal if installed, falling back to standard Python path resolution otherwise.
54
+
55
+ ## Installation
56
+
57
+ ### Method 1: Manual Installation (System-wide)
58
+
59
+ Download the `git-rexec` script, make it executable, and copy it to a directory in your system PATH (e.g., `/usr/local/bin`):
60
+
61
+ ```bash
62
+ sudo cp git-rexec /usr/local/bin/
63
+ ```
64
+
65
+ ### Method 2: Installation via pip
66
+
67
+ Install the package directly from the Git repository using `pip`:
68
+
69
+ ```bash
70
+ pip install --user git+https://github.com/jamescherti/git-rexec
71
+ ```
72
+
73
+ ## Dependencies
74
+
75
+ ### System Dependencies
76
+
77
+ - `git`: Required for repository validation and execution.
78
+ - `fd` (Optional): Highly recommended for faster repository discovery.
79
+
80
+ ### Python Dependencies (Optional)
81
+
82
+ - `colorama`: Provides color-coded terminal output.
83
+ - `setproctitle`: Sets the process title for process monitoring tools.
84
+
85
+ You can install the optional Python dependencies via pip:
86
+
87
+ ```bash
88
+ pip install colorama setproctitle
89
+
90
+ ```
91
+
92
+ ## Usage
93
+
94
+ ```bash
95
+ git-rexec [OPTIONS] [exec_cmd ...]
96
+
97
+ ```
98
+
99
+ *(Assuming the `git-rexec` script is executable and in your PATH.)*
100
+
101
+ ### Positional Arguments
102
+
103
+ - `exec_cmd`: The shell command to execute within each discovered Git repository. You can use `--` to pass options directly to the command. If omitted, the script simply prints the paths of the discovered repositories.
104
+
105
+ ### Options
106
+
107
+ - `-C, --directory <path>`: The root directory to start searching for Git repositories. Defaults to the current working directory (`.`).
108
+ - `--exclude-dir <path>`: Exclude a specific directory and all of its subdirectories from the search. This option can be provided multiple times.
109
+ - `-p, --parallel`: Execute the command in parallel using threads.
110
+ - `-i, --if-exec <command>`: Execute the main command only if this check command returns an exit code of `0`.
111
+ - `-j, --jobs <int>`: The maximum number of concurrent workers/processors to use for parallel execution. Defaults to the number of CPU cores available.
112
+ - `-h, --help`: Show the help message and exit.
113
+ - `-q, --quiet`: Quiet mode. Suppresses the informational tracking headers (`[EXEC]` and `[EXEC-P]`) that prefix execution output. In sequential mode, it hides the `[EXEC]` repository delimiter line entirely; in parallel mode (`-p`), it strips the yellow `[EXEC-P]` header track and removes the four-space indentation, printing only the raw, unindented stdout and stderr streams. This flag has no effect when no execution command is supplied, allowing discovered repository paths to print normally.
114
+
115
+ ## License
116
+
117
+ This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
118
+
119
+ Copyright (C) 2019-2026 [James Cherti](https://www.jamescherti.com).
120
+
121
+ ## Links
122
+
123
+ - [git-rexec @GitHub](https://github.com/jamescherti/git-rexec)
@@ -0,0 +1,5 @@
1
+ git_rexec-1.0.0.data/scripts/git-rexec,sha256=tH-t58Rd7-EwnmhfWFKcSHgjaspAzeliJapYOd-5jJM,16530
2
+ git_rexec-1.0.0.dist-info/METADATA,sha256=EazUM3ohURab0sywUaf0Lrf71N47ayE-gpYq6FLDgYo,5841
3
+ git_rexec-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
4
+ git_rexec-1.0.0.dist-info/top_level.txt,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
5
+ git_rexec-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+