git-rexec 1.0.2__tar.gz → 1.0.4__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: git-rexec
3
- Version: 1.0.2
3
+ Version: 1.0.4
4
4
  Summary: Locate Git repositories and execute commands against them in parallel.
5
5
  Author: James Cherti
6
6
  License-Expression: GPL-3.0-or-later
@@ -21,36 +21,36 @@ Requires-Dist: setproctitle; extra == "extras"
21
21
  The [git-rexec](https://github.com/jamescherti/git-rexec/) command-line tool recursively locates Git repositories within a directory and executes commands against them, either sequentially or in parallel.
22
22
 
23
23
  Here are examples demonstrating how to use `git-rexec`:
24
- - Execute `git status -s` across all discovered Git repositories (found by searching recursively under the current working directory) in parallel (`-p` or `--parallel`):
25
- ```
26
- git-rexec -p -- git status -s
27
- ```
28
24
 
29
- - 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:
30
- ```bash
31
- git-rexec -j 5 --parallel -- git fetch
32
- ```
25
+ Example 1: 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
+ ```
33
29
 
30
+ Example 2: 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
34
 
35
- - Include sub-repositories (e.g., Git worktrees and submodules) alongside standard Git repositories during discovery (`-s` or `--include-sub-repos`), and execute `git status` against them:
36
- ```bash
37
- git-rexec -s -p -- git status
38
- ```
35
+ Example 3: Include sub-repositories (e.g., Git worktrees and submodules) alongside standard Git repositories during discovery (`-s` or `--include-sub-repos`), and execute `git status` against them:
36
+ ```bash
37
+ git-rexec -s -p -- git status
38
+ ```
39
39
 
40
- - 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:
41
- ```bash
42
- git-rexec -C ~/projects --exclude-dir ~/projects/archive --parallel -- git status -s
43
- ```
40
+ Example 4: 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:
41
+ ```bash
42
+ git-rexec -C ~/projects --exclude-dir ~/projects/archive --parallel -- git status -s
43
+ ```
44
44
 
45
- - 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`):
46
- ```bash
47
- git-rexec --if-exec 'sh -c "test -f README.md"' --parallel -- wc -l README.md
48
- ```
45
+ Example 5: 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`):
46
+ ```bash
47
+ git-rexec --if-exec 'sh -c "test -f README.md"' --parallel -- wc -l README.md
48
+ ```
49
49
 
50
- - Print the paths of all discovered Git repositories:
51
- ```bash
52
- git-rexec --print
53
- ```
50
+ Example 6: Print the paths of all discovered Git repositories:
51
+ ```bash
52
+ git-rexec --print
53
+ ```
54
54
 
55
55
  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.
56
56
 
@@ -101,7 +101,7 @@ pip install colorama setproctitle
101
101
  ## Usage
102
102
 
103
103
  ```bash
104
- git-rexec [OPTIONS] [exec_cmd ...]
104
+ git-rexec [OPTIONS] -- [exec_cmd ...]
105
105
  ```
106
106
 
107
107
  *(Assuming the `git-rexec` script is executable and in your PATH.)*
@@ -112,14 +112,31 @@ git-rexec [OPTIONS] [exec_cmd ...]
112
112
 
113
113
  ### Options
114
114
 
115
- - `-C, --directory <path>`: The root directory to start searching for Git repositories. Defaults to the current working directory (`.`).
116
- - `--exclude-dir <path>`: Exclude a specific directory and all of its subdirectories from the search. This option can be provided multiple times.
117
- - `-p, --parallel`: Execute the command in parallel using threads.
118
- - `-i, --if-exec <command>`: Execute the main command only if this check command returns an exit code of `0`.
119
- - `-j, --jobs <int>`: The maximum number of concurrent workers/processors to use for parallel execution. Defaults to the number of CPU cores available.
120
- - `-s, --include-sub-repos`: Include sub-repositories (e.g., Git worktrees and submodules) in the search target list.
121
- - `-h, --help`: Show the help message and exit.
122
- - `-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.
115
+ ```
116
+ usage: git-rexec [-h] [-C DIRECTORY] [--exclude-dir EXCLUDE_DIR] [-p] [-i IF_EXEC] [-j MAX_WORKERS] [-q] [-s] [--print] [--print0] [exec_cmd ...]
117
+
118
+ Find Git repositories and execute commands against them in parallel.
119
+
120
+ positional arguments:
121
+ exec_cmd The command to execute. You can use -- to pass options.
122
+
123
+ options:
124
+ -h, --help show this help message and exit
125
+ -C, --directory DIRECTORY
126
+ Root directory to search (defaults to current directory)
127
+ --exclude-dir EXCLUDE_DIR
128
+ Exclude a specific directory and all of its subdirectories
129
+ -p, --parallel Execute the command in parallel using threads
130
+ -i, --if-exec IF_EXEC
131
+ Execute commands only if this check returns exit code 0.
132
+ -j, --jobs MAX_WORKERS
133
+ Maximum number of processors/workers to use
134
+ -q, --quiet Quiet mode. Suppresses the informational log prefixes ([EXEC] and [EXEC-P]) that precede execution output.
135
+ -s, --include-sub-repos
136
+ Include sub-repositories (e.g., Git worktrees and submodules)
137
+ --print Print the paths (only when no command is provided)
138
+ --print0 Separate the paths with a null character (only when no command is provided)
139
+ ```
123
140
 
124
141
  ## License
125
142
 
@@ -0,0 +1,131 @@
1
+ # git-rexec: Find Git Repositories and Execute Commands Against Them, either Sequentially or in Parallel
2
+
3
+ The [git-rexec](https://github.com/jamescherti/git-rexec/) command-line tool recursively locates Git repositories within a directory and executes commands against them, either sequentially or in parallel.
4
+
5
+ Here are examples demonstrating how to use `git-rexec`:
6
+
7
+ Example 1: Execute `git status -s` across all discovered Git repositories (found by searching recursively under the current working directory) in parallel (`-p` or `--parallel`):
8
+ ```
9
+ git-rexec -p -- git status -s
10
+ ```
11
+
12
+ Example 2: 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:
13
+ ```bash
14
+ git-rexec -j 5 --parallel -- git fetch
15
+ ```
16
+
17
+ Example 3: Include sub-repositories (e.g., Git worktrees and submodules) alongside standard Git repositories during discovery (`-s` or `--include-sub-repos`), and execute `git status` against them:
18
+ ```bash
19
+ git-rexec -s -p -- git status
20
+ ```
21
+
22
+ Example 4: 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:
23
+ ```bash
24
+ git-rexec -C ~/projects --exclude-dir ~/projects/archive --parallel -- git status -s
25
+ ```
26
+
27
+ Example 5: 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`):
28
+ ```bash
29
+ git-rexec --if-exec 'sh -c "test -f README.md"' --parallel -- wc -l README.md
30
+ ```
31
+
32
+ Example 6: Print the paths of all discovered Git repositories:
33
+ ```bash
34
+ git-rexec --print
35
+ ```
36
+
37
+ 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.
38
+
39
+ ## Features
40
+
41
+ - Recursively discover Git repositories starting from a specified root directory.
42
+ - Optional flag to include sub-repositories (e.g., Git worktrees and submodules) in the execution target list.
43
+ - Execute shell commands across multiple repositories in parallel using worker threads.
44
+ - Filter target repositories based on the exit code of a conditional check (`--if-exec`).
45
+ - Exclude specific directories from the search path.
46
+ - Export discovered repository paths for integration with other shell tools (using `--print` or `--print0`).
47
+
48
+ ## Installation
49
+
50
+ ### Method 1: Manual Installation (System-wide)
51
+
52
+ Download the `git-rexec` script, make it executable, and copy it to a directory in your system PATH (e.g., `/usr/local/bin`):
53
+
54
+ ```bash
55
+ sudo cp git-rexec /usr/local/bin/
56
+ ```
57
+
58
+ ### Method 2: Installation via pip
59
+
60
+ Install the package directly from the Git repository using `pip`:
61
+
62
+ ```bash
63
+ pip install --user git-rexec
64
+ ```
65
+
66
+ ## Dependencies
67
+
68
+ ### System Dependencies
69
+
70
+ - `git`: Required for repository validation and execution.
71
+
72
+ ### Python Dependencies (Optional)
73
+
74
+ - `colorama`: Provides color-coded terminal output.
75
+ - `setproctitle`: Sets the process title for process monitoring tools.
76
+
77
+ You can install the optional Python dependencies via pip:
78
+
79
+ ```bash
80
+ pip install colorama setproctitle
81
+ ```
82
+
83
+ ## Usage
84
+
85
+ ```bash
86
+ git-rexec [OPTIONS] -- [exec_cmd ...]
87
+ ```
88
+
89
+ *(Assuming the `git-rexec` script is executable and in your PATH.)*
90
+
91
+ ### Positional Arguments
92
+
93
+ - `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.
94
+
95
+ ### Options
96
+
97
+ ```
98
+ usage: git-rexec [-h] [-C DIRECTORY] [--exclude-dir EXCLUDE_DIR] [-p] [-i IF_EXEC] [-j MAX_WORKERS] [-q] [-s] [--print] [--print0] [exec_cmd ...]
99
+
100
+ Find Git repositories and execute commands against them in parallel.
101
+
102
+ positional arguments:
103
+ exec_cmd The command to execute. You can use -- to pass options.
104
+
105
+ options:
106
+ -h, --help show this help message and exit
107
+ -C, --directory DIRECTORY
108
+ Root directory to search (defaults to current directory)
109
+ --exclude-dir EXCLUDE_DIR
110
+ Exclude a specific directory and all of its subdirectories
111
+ -p, --parallel Execute the command in parallel using threads
112
+ -i, --if-exec IF_EXEC
113
+ Execute commands only if this check returns exit code 0.
114
+ -j, --jobs MAX_WORKERS
115
+ Maximum number of processors/workers to use
116
+ -q, --quiet Quiet mode. Suppresses the informational log prefixes ([EXEC] and [EXEC-P]) that precede execution output.
117
+ -s, --include-sub-repos
118
+ Include sub-repositories (e.g., Git worktrees and submodules)
119
+ --print Print the paths (only when no command is provided)
120
+ --print0 Separate the paths with a null character (only when no command is provided)
121
+ ```
122
+
123
+ ## License
124
+
125
+ 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.
126
+
127
+ Copyright (C) 2019-2026 [James Cherti](https://www.jamescherti.com).
128
+
129
+ ## Links
130
+
131
+ - [git-rexec @GitHub](https://github.com/jamescherti/git-rexec)
@@ -6,7 +6,7 @@
6
6
  #
7
7
  # Author: James Cherti
8
8
  # URL: https://github.com/jamescherti/git-rexec
9
- # Version: 1.0.2
9
+ # Version: 1.0.4
10
10
  #
11
11
  # Copyright (C) 2019-2026 James Cherti
12
12
  #
@@ -30,6 +30,8 @@ and executing commands against them. It supports conditional filtering,
30
30
  background execution, and sequential foreground execution.
31
31
  """
32
32
 
33
+ from __future__ import annotations
34
+
33
35
  import argparse
34
36
  import os
35
37
  import shlex
@@ -41,16 +43,18 @@ from concurrent.futures import Future, ThreadPoolExecutor, as_completed
41
43
  from dataclasses import dataclass
42
44
  from multiprocessing import cpu_count
43
45
  from pathlib import Path
44
- from typing import Optional
45
46
 
46
47
  try:
47
48
  from colorama import Fore
48
49
  from colorama import init as colorama_init
50
+
49
51
  HAS_COLORAMA: bool = True
50
52
  except ImportError:
51
53
  HAS_COLORAMA = False
52
54
 
53
55
  class Fore: # type: ignore
56
+ """Provide fallback empty strings for missing colorama Fore attributes."""
57
+
54
58
  YELLOW: str = ""
55
59
  RED: str = ""
56
60
  RESET: str = ""
@@ -65,6 +69,7 @@ class CommandResult:
65
69
  :param stdout: Standard output content.
66
70
  :param stderr: Standard error content.
67
71
  """
72
+
68
73
  command: list[str]
69
74
  returncode: int
70
75
  stdout: str = ""
@@ -78,11 +83,12 @@ class RepoContext:
78
83
  :param path: The absolute path to the repository root.
79
84
  :param parallel_result: The result of the parallel execution, if any.
80
85
  """
86
+
81
87
  path: Path
82
- parallel_result: Optional[CommandResult] = None
88
+ parallel_result: CommandResult | None = None
83
89
 
84
90
 
85
- def git_toplevel(repo_path: Path) -> Optional[Path]:
91
+ def git_toplevel(repo_path: Path) -> Path | None:
86
92
  """Return the absolute path to the top-level directory of a Git repository.
87
93
 
88
94
  :param repo_path: Path inside the Git repository.
@@ -90,39 +96,78 @@ def git_toplevel(repo_path: Path) -> Optional[Path]:
90
96
  Git repo.
91
97
  """
92
98
  try:
93
- proc: subprocess.CompletedProcess[str] = subprocess.run(
94
- ["git", "-C", str(repo_path), "rev-parse", "--show-toplevel"],
95
- capture_output=True,
96
- text=True,
97
- check=True
98
- )
99
+ proc: subprocess.CompletedProcess[str] = \
100
+ subprocess.run(["git", "-C", str(repo_path), "rev-parse",
101
+ "--show-toplevel"],
102
+ capture_output=True,
103
+ text=True,
104
+ check=True)
99
105
  return Path(proc.stdout.strip()).resolve()
100
106
  except subprocess.CalledProcessError:
101
107
  return None
102
108
 
103
109
 
104
- def git_py_find_repo(root: Path) -> tuple[set[Path], set[Path]]:
110
+ def git_find_repo(root: Path, exclude_paths: list[Path] | None = None) \
111
+ -> tuple[set[Path], set[Path]]:
105
112
  """Recursively find all Git repositories under the root path using Python.
106
113
 
107
114
  :param root: Directory to search under.
115
+ :param exclude_paths: Directories to exclude to optimize traversal.
108
116
  :return: A tuple containing standard repository paths and sub-repository
109
117
  paths.
110
118
  """
119
+ root_res: Path = root.resolve()
120
+
121
+ # Paths are already resolved by discover_repos
122
+ ex_resolved: set[Path] = set(exclude_paths) if exclude_paths else set()
123
+
124
+ # Do not begin traversal if the root itself is excluded
111
125
  repos: set[Path] = set()
112
126
  sub_repos: set[Path] = set()
113
-
114
- for dirpath, dirnames, filenames in os.walk(root):
115
- if ".git" in dirnames:
116
- repos.add(Path(dirpath).absolute())
117
- elif ".git" in filenames:
118
- sub_repos.add(Path(dirpath).absolute())
119
- else:
127
+ if ex_resolved and any((root_res == ex_path or
128
+ root_res.is_relative_to(ex_path))
129
+ for ex_path in ex_resolved):
130
+ return repos, sub_repos
131
+
132
+ # Store Path objects internally to simplify maintenance
133
+ dirs_to_visit: list[Path] = [root_res]
134
+ while dirs_to_visit:
135
+ current_dir: Path = dirs_to_visit.pop()
136
+
137
+ try:
138
+ with os.scandir(current_dir) as iter_scandir:
139
+ dirs_in_current: list[Path] = []
140
+ is_repo: bool = False
141
+
142
+ for entry in iter_scandir:
143
+ if entry.name == ".git":
144
+ is_repo = True
145
+ if entry.is_dir():
146
+ repos.add(current_dir)
147
+ elif entry.is_file():
148
+ sub_repos.add(current_dir)
149
+
150
+ # Found the repository, stop evaluating this directory
151
+ break
152
+
153
+ try:
154
+ # Using follow_symlinks=False avoids an extra
155
+ # is_symlink() call
156
+ if entry.is_dir(follow_symlinks=False):
157
+ entry_path: Path = Path(entry.path)
158
+ if ex_resolved and entry_path in ex_resolved:
159
+ continue
160
+ dirs_in_current.append(entry_path)
161
+ except OSError:
162
+ continue
163
+
164
+ if not is_repo:
165
+ # Append collected directories only if we are not inside a
166
+ # repository
167
+ dirs_to_visit.extend(dirs_in_current)
168
+ except OSError:
120
169
  continue
121
170
 
122
- # Clear dirnames to prevent os.walk from descending into
123
- # subdirectories
124
- dirnames.clear()
125
-
126
171
  return repos, sub_repos
127
172
 
128
173
 
@@ -133,18 +178,19 @@ def get_sub_repos(repo_path: Path) -> set[Path]:
133
178
  :return: A set of absolute paths to worktrees and submodules.
134
179
  """
135
180
  sub_repos: set[Path] = set()
181
+ repo_path_resolved: Path = repo_path.resolve()
136
182
 
137
183
  # Discover worktrees
138
184
  # pylint: disable=too-many-try-statements
139
185
  try:
140
- wt_proc: subprocess.CompletedProcess[str] = subprocess.run(
141
- ["git", "-C", str(repo_path), "worktree", "list", "--porcelain"],
142
- capture_output=True,
143
- text=True,
144
- check=True,
145
- )
146
-
147
- current_wt: Optional[Path] = None
186
+ wt_proc: subprocess.CompletedProcess[str] = \
187
+ subprocess.run(["git", "-C", str(repo_path),
188
+ "worktree", "list", "--porcelain"],
189
+ capture_output=True,
190
+ text=True,
191
+ check=True)
192
+
193
+ current_wt: Path | None = None
148
194
  is_bare: bool = False
149
195
 
150
196
  for line in wt_proc.stdout.splitlines():
@@ -152,8 +198,8 @@ def get_sub_repos(repo_path: Path) -> set[Path]:
152
198
 
153
199
  # Git porcelain separates blocks with a blank line
154
200
  if not line:
155
- if current_wt and not is_bare \
156
- and current_wt != repo_path.resolve():
201
+ if (current_wt and
202
+ not is_bare and current_wt != repo_path_resolved):
157
203
  sub_repos.add(current_wt)
158
204
  # Reset state for the next block
159
205
  current_wt = None
@@ -166,7 +212,7 @@ def get_sub_repos(repo_path: Path) -> set[Path]:
166
212
  is_bare = True
167
213
 
168
214
  # Process the final block in case the output lacks a trailing newline
169
- if current_wt and not is_bare and current_wt != repo_path.resolve():
215
+ if current_wt and not is_bare and current_wt != repo_path_resolved:
170
216
  sub_repos.add(current_wt)
171
217
 
172
218
  except subprocess.CalledProcessError:
@@ -174,13 +220,12 @@ def get_sub_repos(repo_path: Path) -> set[Path]:
174
220
 
175
221
  # Discover submodules
176
222
  try:
177
- sm_proc: subprocess.CompletedProcess[str] = subprocess.run(
178
- ["git", "-C", str(repo_path),
179
- "submodule", "foreach", "--quiet", "pwd"],
180
- capture_output=True,
181
- text=True,
182
- check=True,
183
- )
223
+ sm_proc: subprocess.CompletedProcess[str] = \
224
+ subprocess.run(["git", "-C", str(repo_path), "submodule",
225
+ "foreach", "--quiet", "pwd",],
226
+ capture_output=True,
227
+ text=True,
228
+ check=True)
184
229
 
185
230
  for line in sm_proc.stdout.splitlines():
186
231
  sm_path: Path = Path(line.strip()).resolve()
@@ -192,7 +237,8 @@ def get_sub_repos(repo_path: Path) -> set[Path]:
192
237
  return sub_repos
193
238
 
194
239
 
195
- def run_command_get_output(repo_path: Path, cmd_list: list[str],
240
+ def run_command_get_output(repo_path: Path,
241
+ cmd_list: list[str],
196
242
  capture: bool = True) -> CommandResult:
197
243
  """Execute a shell command within a specific directory.
198
244
 
@@ -205,19 +251,13 @@ def run_command_get_output(repo_path: Path, cmd_list: list[str],
205
251
  return CommandResult(command=[], returncode=0)
206
252
 
207
253
  try:
208
- proc: subprocess.CompletedProcess[str] = subprocess.run(
209
- cmd_list,
210
- cwd=repo_path,
211
- capture_output=capture,
212
- text=True,
213
- check=False
214
- )
215
- return CommandResult(
216
- command=cmd_list,
217
- returncode=proc.returncode,
218
- stdout=proc.stdout if proc.stdout else "",
219
- stderr=proc.stderr if proc.stderr else ""
220
- )
254
+ proc: subprocess.CompletedProcess[str] = \
255
+ subprocess.run(cmd_list, cwd=repo_path, capture_output=capture,
256
+ text=True, check=False,)
257
+ return CommandResult(command=cmd_list,
258
+ returncode=proc.returncode,
259
+ stdout=proc.stdout if proc.stdout else "",
260
+ stderr=proc.stderr if proc.stderr else "")
221
261
  except FileNotFoundError:
222
262
  return CommandResult(
223
263
  command=cmd_list,
@@ -226,8 +266,8 @@ def run_command_get_output(repo_path: Path, cmd_list: list[str],
226
266
  )
227
267
 
228
268
 
229
- def format_parallel_output(
230
- repo_path: Path, result: CommandResult, quiet: bool = False) -> str:
269
+ def format_parallel_output(repo_path: Path,
270
+ result: CommandResult, quiet: bool = False) -> str:
231
271
  """Format the output of a parallel command for display.
232
272
 
233
273
  :param repo_path: The repository path.
@@ -255,8 +295,9 @@ def format_parallel_output(
255
295
  return header + indented_body
256
296
 
257
297
 
258
- def process_repo(repo_path: Path, exec_parallel_cmd: Optional[list[str]],
259
- if_exec_cmd: Optional[list[str]]) -> Optional[RepoContext]:
298
+ def process_repo(repo_path: Path,
299
+ exec_parallel_cmd: list[str] | None,
300
+ if_exec_cmd: list[str] | None) -> RepoContext | None:
260
301
  """Process a single repository: check conditions and run parallel commands.
261
302
 
262
303
  :param repo_path: The repository path.
@@ -265,22 +306,23 @@ def process_repo(repo_path: Path, exec_parallel_cmd: Optional[list[str]],
265
306
  :return: RepoContext if processed successfully, None if filtered out.
266
307
  """
267
308
  # Filter: if-exec
268
- if_exec_cmd_list: Optional[list[str]] = if_exec_cmd
309
+ if_exec_cmd_list: list[str] | None = if_exec_cmd
269
310
  if if_exec_cmd_list:
270
311
  # We discard output for the filter check, only caring about exit code
271
- filter_res: CommandResult = run_command_get_output(
272
- repo_path, if_exec_cmd_list, capture=True
273
- )
312
+ filter_res: CommandResult = run_command_get_output(repo_path,
313
+ if_exec_cmd_list,
314
+ capture=True)
274
315
  if filter_res.returncode != 0:
275
316
  return None
276
317
 
277
318
  # Action: exec-parallel
278
- parallel_result: Optional[CommandResult] = None
279
- exec_parallel_cmd_list: Optional[list[str]] = exec_parallel_cmd
319
+ parallel_result: CommandResult | None = None
320
+ exec_parallel_cmd_list: list[str] | None = exec_parallel_cmd
280
321
  if exec_parallel_cmd_list:
281
- parallel_result = run_command_get_output(
282
- repo_path, exec_parallel_cmd_list, capture=True
283
- )
322
+ parallel_result = \
323
+ run_command_get_output(repo_path,
324
+ exec_parallel_cmd_list,
325
+ capture=True)
284
326
 
285
327
  return RepoContext(path=repo_path, parallel_result=parallel_result)
286
328
 
@@ -300,12 +342,18 @@ def discover_repos(directory: Path,
300
342
  repos: set[Path] = set()
301
343
  worktrees: set[Path] = set()
302
344
 
345
+ list_ex_paths: list[Path] = (
346
+ [Path(ex_dir).resolve() for ex_dir in exclude_dirs]
347
+ if exclude_dirs
348
+ else []
349
+ )
350
+
303
351
  # Discover all repositories
304
- toplevel: Optional[Path] = git_toplevel(directory)
352
+ toplevel: Path | None = git_toplevel(directory)
305
353
  if toplevel:
306
354
  repos = {toplevel}
307
355
  else:
308
- repos, worktrees = git_py_find_repo(directory)
356
+ repos, worktrees = git_find_repo(directory, list_ex_paths)
309
357
 
310
358
  # Extract and append sub-repositories concurrently if requested
311
359
  if include_sub_repos:
@@ -321,25 +369,22 @@ def discover_repos(directory: Path,
321
369
 
322
370
  repos.update(sub_repos)
323
371
 
324
- # Filter out excluded directories
325
- if not exclude_dirs:
372
+ # Filter out excluded directories (applies dynamically to sub-repos too)
373
+ if not list_ex_paths:
326
374
  return repos
327
375
 
328
- list_ex_paths: list[Path] = [Path(ex_dir).resolve()
329
- for ex_dir in exclude_dirs]
330
376
  return {
331
- repo for repo in repos
332
- if not any(repo.resolve().is_relative_to(ex_path)
377
+ repo
378
+ for repo in repos
379
+ if not any(repo.is_relative_to(ex_path)
333
380
  for ex_path in list_ex_paths)
334
381
  }
335
382
 
336
383
 
337
- def execute_parallel_tasks(
338
- repos: set[Path],
339
- exec_parallel: Optional[list[str]],
340
- if_exec: Optional[list[str]],
341
- max_workers: int
342
- ) -> list[RepoContext]:
384
+ def execute_parallel_tasks(repos: set[Path],
385
+ exec_parallel: list[str] | None,
386
+ if_exec: list[str] | None,
387
+ max_workers: int) -> list[RepoContext]:
343
388
  """Run discovery and parallel execution tasks.
344
389
 
345
390
  :param repos: set of repositories to process.
@@ -351,16 +396,15 @@ def execute_parallel_tasks(
351
396
  results: list[RepoContext] = []
352
397
 
353
398
  with ThreadPoolExecutor(max_workers=max_workers) as executor:
354
- futures: dict[Future[Optional[RepoContext]], Path] = {
355
- executor.submit(
356
- process_repo, repo, exec_parallel, if_exec
357
- ): repo for repo in repos
399
+ futures: dict[Future[RepoContext | None], Path] = {
400
+ executor.submit(process_repo, repo, exec_parallel, if_exec): repo
401
+ for repo in repos
358
402
  }
359
403
 
360
404
  for future in as_completed(futures):
361
405
  repo_path: Path = futures[future]
362
406
  try:
363
- result: Optional[RepoContext] = future.result()
407
+ result: RepoContext | None = future.result()
364
408
  if result:
365
409
  results.append(result)
366
410
  except (OSError, ValueError) as exc:
@@ -376,71 +420,82 @@ def parse_args() -> argparse.Namespace:
376
420
  :return: Parsed arguments.
377
421
  """
378
422
  parser: argparse.ArgumentParser = argparse.ArgumentParser(
379
- description="Find git repos and execute commands.",
380
- formatter_class=argparse.RawDescriptionHelpFormatter
423
+ description=("Find Git repositories and execute commands against "
424
+ "them in parallel."),
425
+ formatter_class=argparse.RawDescriptionHelpFormatter,
381
426
  )
382
427
  parser.add_argument(
383
- "-C", "--directory",
428
+ "-C",
429
+ "--directory",
384
430
  type=Path,
385
431
  default=Path("."),
386
- help="Root directory to search (defaults to current directory)"
432
+ help="Root directory to search (defaults to current directory)",
387
433
  )
388
434
  parser.add_argument(
389
435
  "--exclude-dir",
390
436
  action="append",
391
437
  default=[],
392
- help="Exclude a specific directory and all of its subdirectories"
438
+ help="Exclude a specific directory and all of its subdirectories",
393
439
  )
394
440
  parser.add_argument(
395
- "-p", "--parallel",
441
+ "-p",
442
+ "--parallel",
396
443
  action="store_true",
397
444
  help="Execute the command in parallel using threads",
398
- default=False
445
+ default=False,
399
446
  )
400
447
  parser.add_argument(
401
- "-i", "--if-exec",
448
+ "-i",
449
+ "--if-exec",
402
450
  type=str,
403
451
  help="Execute commands only if this check returns exit code 0.",
404
- default=None
452
+ default=None,
405
453
  )
406
454
  parser.add_argument(
407
- "-j", "--jobs",
455
+ "-j",
456
+ "--jobs",
408
457
  type=int,
409
458
  dest="max_workers",
410
459
  help="Maximum number of processors/workers to use",
411
- default=(cpu_count() or 1)
460
+ default=(cpu_count() or 1),
412
461
  )
413
462
  parser.add_argument(
414
- "-q", "--quiet",
463
+ "-q",
464
+ "--quiet",
415
465
  action="store_true",
416
- help=("Quiet mode. Suppresses the informational tracking headers "
417
- "([EXEC] and [EXEC-P]) that prefix execution output."),
418
- default=False
466
+ help=(
467
+ "Quiet mode. Suppresses the informational log prefixes "
468
+ "([EXEC] and [EXEC-P]) that precede execution output."
469
+ ),
470
+ default=False,
419
471
  )
420
472
  parser.add_argument(
421
- "-s", "--include-sub-repos",
473
+ "-s",
474
+ "--include-sub-repos",
422
475
  action="store_true",
423
476
  help="Include sub-repositories (e.g., Git worktrees and submodules)",
424
- default=False
477
+ default=False,
425
478
  )
426
479
  parser.add_argument(
427
480
  "--print",
428
481
  action="store_true",
429
482
  help="Print the paths (only when no command is provided)",
430
- default=False
483
+ default=False,
431
484
  )
432
485
  parser.add_argument(
433
486
  "--print0",
434
487
  action="store_true",
435
- help=("Separate the paths with a null character (only when no "
436
- "command is provided)"),
437
- default=False
488
+ help=(
489
+ "Separate the paths with a null character (only when no "
490
+ "command is provided)"
491
+ ),
492
+ default=False,
438
493
  )
439
494
  parser.add_argument(
440
495
  "exec_cmd",
441
496
  type=str,
442
497
  nargs="*",
443
- help="The command to execute. You can use -- to pass options."
498
+ help="The command to execute. You can use -- to pass options.",
444
499
  )
445
500
 
446
501
  args: argparse.Namespace = parser.parse_args()
@@ -484,6 +539,7 @@ def main() -> None:
484
539
  try:
485
540
  # pylint: disable=import-outside-toplevel
486
541
  from setproctitle import setproctitle
542
+
487
543
  setproctitle(Path(sys.argv[0]).name) # type: ignore
488
544
  except ImportError:
489
545
  # Optional dependency 'setproctitle' is not installed.
@@ -500,13 +556,12 @@ def main() -> None:
500
556
  # Check for empty action
501
557
  if not args.exec_cmd and not args.print and not args.print0:
502
558
  print("Nothing to do.", file=sys.stderr)
503
- sys.exit(0)
559
+ sys.exit(2)
504
560
 
505
561
  # Verify that the execution command exists
506
562
  if args.exec_cmd and not shutil.which(args.exec_cmd[0]):
507
- print(
508
- f"Error: Command not found: '{args.exec_cmd[0]}'",
509
- file=sys.stderr)
563
+ print(f"Error: Command not found: '{args.exec_cmd[0]}'",
564
+ file=sys.stderr)
510
565
  sys.exit(127)
511
566
 
512
567
  # Discover Repositories
@@ -516,21 +571,20 @@ def main() -> None:
516
571
  args.include_sub_repos)
517
572
 
518
573
  # setup background command list if needed
519
- exec_parallel_cmd: Optional[list[str]] = None
574
+ exec_parallel_cmd: list[str] | None = None
520
575
  if args.parallel and args.exec_cmd:
521
576
  exec_parallel_cmd = args.exec_cmd
522
577
 
523
- if_exec_cmd: Optional[list[str]] = None
578
+ if_exec_cmd: list[str] | None = None
524
579
  if args.if_exec:
525
580
  if_exec_cmd = shlex.split(args.if_exec)
526
581
 
527
582
  # Parallel Processing (Filter + Background Exec)
528
- processed_repos: list[RepoContext] = execute_parallel_tasks(
529
- repos,
530
- exec_parallel_cmd,
531
- if_exec_cmd,
532
- args.max_workers
533
- )
583
+ processed_repos: list[RepoContext] = \
584
+ execute_parallel_tasks(repos,
585
+ exec_parallel_cmd,
586
+ if_exec_cmd,
587
+ args.max_workers)
534
588
 
535
589
  execution_errors: list[tuple[Path, CommandResult]] = []
536
590
 
@@ -540,8 +594,10 @@ def main() -> None:
540
594
 
541
595
  # Handle Background Execution Results
542
596
  if context.parallel_result:
543
- output_display: str = format_parallel_output(
544
- repo_path, context.parallel_result, args.quiet)
597
+ output_display: str = \
598
+ format_parallel_output(repo_path,
599
+ context.parallel_result,
600
+ args.quiet)
545
601
  if output_display:
546
602
  print(output_display, end="")
547
603
 
@@ -551,11 +607,9 @@ def main() -> None:
551
607
  # Handle Foreground Execution
552
608
  if args.exec_cmd and not args.parallel:
553
609
  if not args.quiet:
554
- print(
555
- f"{Fore.YELLOW}[EXEC] {repo_path}: "
556
- f"{subprocess.list2cmdline(args.exec_cmd)}"
557
- f"{Fore.RESET}"
558
- )
610
+ print(f"{Fore.YELLOW}[EXEC] {repo_path}: "
611
+ f"{shlex.join(args.exec_cmd)}"
612
+ f"{Fore.RESET}")
559
613
 
560
614
  # Run interactively/sequentially (capture=False allows interaction)
561
615
  # However, for consistency with error tracking, we might want to
@@ -564,23 +618,18 @@ def main() -> None:
564
618
  # We use subprocess.check_call to allow direct stdout/stderr
565
619
  # flow unless we want to capture for error summary. To match
566
620
  # previous logic, we let it flow to stdout.
567
- subprocess.check_call(
568
- args.exec_cmd,
569
- cwd=repo_path
570
- )
621
+ subprocess.check_call(args.exec_cmd, cwd=repo_path)
571
622
  except subprocess.CalledProcessError as err:
572
623
  # Construct a dummy result for the error summary
573
- failed_res: CommandResult = CommandResult(
574
- command=args.exec_cmd,
575
- returncode=err.returncode
576
- )
577
- execution_errors.append((repo_path, failed_res))
624
+ execution_errors.append(
625
+ (repo_path,
626
+ CommandResult(command=args.exec_cmd,
627
+ returncode=err.returncode)))
578
628
  except FileNotFoundError:
579
- failed_res: CommandResult = CommandResult(
580
- command=args.exec_cmd,
581
- returncode=127
582
- )
583
- execution_errors.append((repo_path, failed_res))
629
+ execution_errors.append(
630
+ (repo_path,
631
+ CommandResult(command=args.exec_cmd,
632
+ returncode=127)))
584
633
 
585
634
  # If no commands were run, just list the repo
586
635
  if not args.exec_cmd:
@@ -596,6 +645,7 @@ def main() -> None:
596
645
  if __name__ == "__main__":
597
646
  try:
598
647
  main()
599
- except (KeyboardInterrupt, BrokenPipeError):
600
- print("\nInterrupting...", file=sys.stderr)
601
- sys.exit(1)
648
+ except KeyboardInterrupt:
649
+ sys.exit(130)
650
+ except BrokenPipeError:
651
+ sys.exit(141)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: git-rexec
3
- Version: 1.0.2
3
+ Version: 1.0.4
4
4
  Summary: Locate Git repositories and execute commands against them in parallel.
5
5
  Author: James Cherti
6
6
  License-Expression: GPL-3.0-or-later
@@ -21,36 +21,36 @@ Requires-Dist: setproctitle; extra == "extras"
21
21
  The [git-rexec](https://github.com/jamescherti/git-rexec/) command-line tool recursively locates Git repositories within a directory and executes commands against them, either sequentially or in parallel.
22
22
 
23
23
  Here are examples demonstrating how to use `git-rexec`:
24
- - Execute `git status -s` across all discovered Git repositories (found by searching recursively under the current working directory) in parallel (`-p` or `--parallel`):
25
- ```
26
- git-rexec -p -- git status -s
27
- ```
28
24
 
29
- - 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:
30
- ```bash
31
- git-rexec -j 5 --parallel -- git fetch
32
- ```
25
+ Example 1: 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
+ ```
33
29
 
30
+ Example 2: 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
34
 
35
- - Include sub-repositories (e.g., Git worktrees and submodules) alongside standard Git repositories during discovery (`-s` or `--include-sub-repos`), and execute `git status` against them:
36
- ```bash
37
- git-rexec -s -p -- git status
38
- ```
35
+ Example 3: Include sub-repositories (e.g., Git worktrees and submodules) alongside standard Git repositories during discovery (`-s` or `--include-sub-repos`), and execute `git status` against them:
36
+ ```bash
37
+ git-rexec -s -p -- git status
38
+ ```
39
39
 
40
- - 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:
41
- ```bash
42
- git-rexec -C ~/projects --exclude-dir ~/projects/archive --parallel -- git status -s
43
- ```
40
+ Example 4: 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:
41
+ ```bash
42
+ git-rexec -C ~/projects --exclude-dir ~/projects/archive --parallel -- git status -s
43
+ ```
44
44
 
45
- - 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`):
46
- ```bash
47
- git-rexec --if-exec 'sh -c "test -f README.md"' --parallel -- wc -l README.md
48
- ```
45
+ Example 5: 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`):
46
+ ```bash
47
+ git-rexec --if-exec 'sh -c "test -f README.md"' --parallel -- wc -l README.md
48
+ ```
49
49
 
50
- - Print the paths of all discovered Git repositories:
51
- ```bash
52
- git-rexec --print
53
- ```
50
+ Example 6: Print the paths of all discovered Git repositories:
51
+ ```bash
52
+ git-rexec --print
53
+ ```
54
54
 
55
55
  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.
56
56
 
@@ -101,7 +101,7 @@ pip install colorama setproctitle
101
101
  ## Usage
102
102
 
103
103
  ```bash
104
- git-rexec [OPTIONS] [exec_cmd ...]
104
+ git-rexec [OPTIONS] -- [exec_cmd ...]
105
105
  ```
106
106
 
107
107
  *(Assuming the `git-rexec` script is executable and in your PATH.)*
@@ -112,14 +112,31 @@ git-rexec [OPTIONS] [exec_cmd ...]
112
112
 
113
113
  ### Options
114
114
 
115
- - `-C, --directory <path>`: The root directory to start searching for Git repositories. Defaults to the current working directory (`.`).
116
- - `--exclude-dir <path>`: Exclude a specific directory and all of its subdirectories from the search. This option can be provided multiple times.
117
- - `-p, --parallel`: Execute the command in parallel using threads.
118
- - `-i, --if-exec <command>`: Execute the main command only if this check command returns an exit code of `0`.
119
- - `-j, --jobs <int>`: The maximum number of concurrent workers/processors to use for parallel execution. Defaults to the number of CPU cores available.
120
- - `-s, --include-sub-repos`: Include sub-repositories (e.g., Git worktrees and submodules) in the search target list.
121
- - `-h, --help`: Show the help message and exit.
122
- - `-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.
115
+ ```
116
+ usage: git-rexec [-h] [-C DIRECTORY] [--exclude-dir EXCLUDE_DIR] [-p] [-i IF_EXEC] [-j MAX_WORKERS] [-q] [-s] [--print] [--print0] [exec_cmd ...]
117
+
118
+ Find Git repositories and execute commands against them in parallel.
119
+
120
+ positional arguments:
121
+ exec_cmd The command to execute. You can use -- to pass options.
122
+
123
+ options:
124
+ -h, --help show this help message and exit
125
+ -C, --directory DIRECTORY
126
+ Root directory to search (defaults to current directory)
127
+ --exclude-dir EXCLUDE_DIR
128
+ Exclude a specific directory and all of its subdirectories
129
+ -p, --parallel Execute the command in parallel using threads
130
+ -i, --if-exec IF_EXEC
131
+ Execute commands only if this check returns exit code 0.
132
+ -j, --jobs MAX_WORKERS
133
+ Maximum number of processors/workers to use
134
+ -q, --quiet Quiet mode. Suppresses the informational log prefixes ([EXEC] and [EXEC-P]) that precede execution output.
135
+ -s, --include-sub-repos
136
+ Include sub-repositories (e.g., Git worktrees and submodules)
137
+ --print Print the paths (only when no command is provided)
138
+ --print0 Separate the paths with a null character (only when no command is provided)
139
+ ```
123
140
 
124
141
  ## License
125
142
 
@@ -1,4 +1,3 @@
1
- .gitignore
2
1
  README.md
3
2
  git-rexec
4
3
  pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "git-rexec"
7
- version = "1.0.2"
7
+ version = "1.0.4"
8
8
  description = "Locate Git repositories and execute commands against them in parallel."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,6 +0,0 @@
1
- /coverage.json
2
- /build
3
- /dist
4
- /*.egg-info
5
- *.pyc
6
- */__pycache__
git_rexec-1.0.2/README.md DELETED
@@ -1,114 +0,0 @@
1
- # git-rexec: Find Git Repositories and Execute Commands Against Them, either Sequentially or in Parallel
2
-
3
- The [git-rexec](https://github.com/jamescherti/git-rexec/) command-line tool recursively locates Git repositories within a directory and executes commands against them, either sequentially or in parallel.
4
-
5
- Here are examples demonstrating how to use `git-rexec`:
6
- - Execute `git status -s` across all discovered Git repositories (found by searching recursively under the current working directory) in parallel (`-p` or `--parallel`):
7
- ```
8
- git-rexec -p -- git status -s
9
- ```
10
-
11
- - 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:
12
- ```bash
13
- git-rexec -j 5 --parallel -- git fetch
14
- ```
15
-
16
-
17
- - Include sub-repositories (e.g., Git worktrees and submodules) alongside standard Git repositories during discovery (`-s` or `--include-sub-repos`), and execute `git status` against them:
18
- ```bash
19
- git-rexec -s -p -- git status
20
- ```
21
-
22
- - 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:
23
- ```bash
24
- git-rexec -C ~/projects --exclude-dir ~/projects/archive --parallel -- git status -s
25
- ```
26
-
27
- - 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`):
28
- ```bash
29
- git-rexec --if-exec 'sh -c "test -f README.md"' --parallel -- wc -l README.md
30
- ```
31
-
32
- - Print the paths of all discovered Git repositories:
33
- ```bash
34
- git-rexec --print
35
- ```
36
-
37
- 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.
38
-
39
- ## Features
40
-
41
- - Recursively discover Git repositories starting from a specified root directory.
42
- - Optional flag to include sub-repositories (e.g., Git worktrees and submodules) in the execution target list.
43
- - Execute shell commands across multiple repositories in parallel using worker threads.
44
- - Filter target repositories based on the exit code of a conditional check (`--if-exec`).
45
- - Exclude specific directories from the search path.
46
- - Export discovered repository paths for integration with other shell tools (using `--print` or `--print0`).
47
-
48
- ## Installation
49
-
50
- ### Method 1: Manual Installation (System-wide)
51
-
52
- Download the `git-rexec` script, make it executable, and copy it to a directory in your system PATH (e.g., `/usr/local/bin`):
53
-
54
- ```bash
55
- sudo cp git-rexec /usr/local/bin/
56
- ```
57
-
58
- ### Method 2: Installation via pip
59
-
60
- Install the package directly from the Git repository using `pip`:
61
-
62
- ```bash
63
- pip install --user git-rexec
64
- ```
65
-
66
- ## Dependencies
67
-
68
- ### System Dependencies
69
-
70
- - `git`: Required for repository validation and execution.
71
-
72
- ### Python Dependencies (Optional)
73
-
74
- - `colorama`: Provides color-coded terminal output.
75
- - `setproctitle`: Sets the process title for process monitoring tools.
76
-
77
- You can install the optional Python dependencies via pip:
78
-
79
- ```bash
80
- pip install colorama setproctitle
81
- ```
82
-
83
- ## Usage
84
-
85
- ```bash
86
- git-rexec [OPTIONS] [exec_cmd ...]
87
- ```
88
-
89
- *(Assuming the `git-rexec` script is executable and in your PATH.)*
90
-
91
- ### Positional Arguments
92
-
93
- - `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.
94
-
95
- ### Options
96
-
97
- - `-C, --directory <path>`: The root directory to start searching for Git repositories. Defaults to the current working directory (`.`).
98
- - `--exclude-dir <path>`: Exclude a specific directory and all of its subdirectories from the search. This option can be provided multiple times.
99
- - `-p, --parallel`: Execute the command in parallel using threads.
100
- - `-i, --if-exec <command>`: Execute the main command only if this check command returns an exit code of `0`.
101
- - `-j, --jobs <int>`: The maximum number of concurrent workers/processors to use for parallel execution. Defaults to the number of CPU cores available.
102
- - `-s, --include-sub-repos`: Include sub-repositories (e.g., Git worktrees and submodules) in the search target list.
103
- - `-h, --help`: Show the help message and exit.
104
- - `-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.
105
-
106
- ## License
107
-
108
- 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.
109
-
110
- Copyright (C) 2019-2026 [James Cherti](https://www.jamescherti.com).
111
-
112
- ## Links
113
-
114
- - [git-rexec @GitHub](https://github.com/jamescherti/git-rexec)
File without changes