git-rexec 1.0.0__tar.gz → 1.0.1__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.
@@ -0,0 +1,6 @@
1
+ /coverage.json
2
+ /build
3
+ /dist
4
+ /*.egg-info
5
+ *.pyc
6
+ */__pycache__
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: git-rexec
3
- Version: 1.0.0
3
+ Version: 1.0.1
4
4
  Summary: Locate Git repositories and execute commands against them in parallel.
5
5
  Author: James Cherti
6
6
  License: GPL-3.0-or-later
@@ -19,7 +19,7 @@ Requires-Dist: setproctitle; extra == "extras"
19
19
 
20
20
  # git-rexec: Find Git Repositories and Execute Commands Against Them, either Sequentially or in Parallel
21
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.
22
+ 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.
23
23
 
24
24
  Here are examples demonstrating how to use `git-rexec`:
25
25
  - Execute `git status -s` across all discovered Git repositories (found by searching recursively under the current working directory) in parallel (`-p` or `--parallel`):
@@ -32,6 +32,12 @@ Here are examples demonstrating how to use `git-rexec`:
32
32
  git-rexec -j 5 --parallel -- git fetch
33
33
  ```
34
34
 
35
+
36
+ - 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:
37
+ ```bash
38
+ git-rexec -s -p -- git status
39
+ ```
40
+
35
41
  - 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
42
  ```bash
37
43
  git-rexec -C ~/projects --exclude-dir ~/projects/archive --parallel -- git status -s
@@ -47,10 +53,10 @@ If this helps your workflow, please support the project by **⭐ starring git-re
47
53
  ## Features
48
54
 
49
55
  - Recursively discover Git repositories starting from a specified root directory.
56
+ - Optional flag to include sub-repositories (e.g., Git worktrees and submodules) in the execution target list.
50
57
  - Execute shell commands across multiple repositories in parallel using worker threads.
51
58
  - Filter target repositories based on the exit code of a conditional check (`--if-exec`).
52
59
  - 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
60
 
55
61
  ## Installation
56
62
 
@@ -67,7 +73,7 @@ sudo cp git-rexec /usr/local/bin/
67
73
  Install the package directly from the Git repository using `pip`:
68
74
 
69
75
  ```bash
70
- pip install --user git+https://github.com/jamescherti/git-rexec
76
+ pip install --user git-rexec
71
77
  ```
72
78
 
73
79
  ## Dependencies
@@ -75,7 +81,6 @@ pip install --user git+https://github.com/jamescherti/git-rexec
75
81
  ### System Dependencies
76
82
 
77
83
  - `git`: Required for repository validation and execution.
78
- - `fd` (Optional): Highly recommended for faster repository discovery.
79
84
 
80
85
  ### Python Dependencies (Optional)
81
86
 
@@ -86,14 +91,12 @@ You can install the optional Python dependencies via pip:
86
91
 
87
92
  ```bash
88
93
  pip install colorama setproctitle
89
-
90
94
  ```
91
95
 
92
96
  ## Usage
93
97
 
94
98
  ```bash
95
99
  git-rexec [OPTIONS] [exec_cmd ...]
96
-
97
100
  ```
98
101
 
99
102
  *(Assuming the `git-rexec` script is executable and in your PATH.)*
@@ -109,6 +112,7 @@ git-rexec [OPTIONS] [exec_cmd ...]
109
112
  - `-p, --parallel`: Execute the command in parallel using threads.
110
113
  - `-i, --if-exec <command>`: Execute the main command only if this check command returns an exit code of `0`.
111
114
  - `-j, --jobs <int>`: The maximum number of concurrent workers/processors to use for parallel execution. Defaults to the number of CPU cores available.
115
+ - `-s, --include-sub-repos`: Include sub-repositories (e.g., Git worktrees and submodules) in the search target list.
112
116
  - `-h, --help`: Show the help message and exit.
113
117
  - `-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
118
 
@@ -1,6 +1,6 @@
1
1
  # git-rexec: Find Git Repositories and Execute Commands Against Them, either Sequentially or in Parallel
2
2
 
3
- 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.
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
4
 
5
5
  Here are examples demonstrating how to use `git-rexec`:
6
6
  - Execute `git status -s` across all discovered Git repositories (found by searching recursively under the current working directory) in parallel (`-p` or `--parallel`):
@@ -13,6 +13,12 @@ Here are examples demonstrating how to use `git-rexec`:
13
13
  git-rexec -j 5 --parallel -- git fetch
14
14
  ```
15
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
+
16
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:
17
23
  ```bash
18
24
  git-rexec -C ~/projects --exclude-dir ~/projects/archive --parallel -- git status -s
@@ -28,10 +34,10 @@ If this helps your workflow, please support the project by **⭐ starring git-re
28
34
  ## Features
29
35
 
30
36
  - Recursively discover Git repositories starting from a specified root directory.
37
+ - Optional flag to include sub-repositories (e.g., Git worktrees and submodules) in the execution target list.
31
38
  - Execute shell commands across multiple repositories in parallel using worker threads.
32
39
  - Filter target repositories based on the exit code of a conditional check (`--if-exec`).
33
40
  - Exclude specific directories from the search path.
34
- - Optional: Can leverage `fd` for fast directory traversal if installed, falling back to standard Python path resolution otherwise.
35
41
 
36
42
  ## Installation
37
43
 
@@ -48,7 +54,7 @@ sudo cp git-rexec /usr/local/bin/
48
54
  Install the package directly from the Git repository using `pip`:
49
55
 
50
56
  ```bash
51
- pip install --user git+https://github.com/jamescherti/git-rexec
57
+ pip install --user git-rexec
52
58
  ```
53
59
 
54
60
  ## Dependencies
@@ -56,7 +62,6 @@ pip install --user git+https://github.com/jamescherti/git-rexec
56
62
  ### System Dependencies
57
63
 
58
64
  - `git`: Required for repository validation and execution.
59
- - `fd` (Optional): Highly recommended for faster repository discovery.
60
65
 
61
66
  ### Python Dependencies (Optional)
62
67
 
@@ -67,14 +72,12 @@ You can install the optional Python dependencies via pip:
67
72
 
68
73
  ```bash
69
74
  pip install colorama setproctitle
70
-
71
75
  ```
72
76
 
73
77
  ## Usage
74
78
 
75
79
  ```bash
76
80
  git-rexec [OPTIONS] [exec_cmd ...]
77
-
78
81
  ```
79
82
 
80
83
  *(Assuming the `git-rexec` script is executable and in your PATH.)*
@@ -90,6 +93,7 @@ git-rexec [OPTIONS] [exec_cmd ...]
90
93
  - `-p, --parallel`: Execute the command in parallel using threads.
91
94
  - `-i, --if-exec <command>`: Execute the main command only if this check command returns an exit code of `0`.
92
95
  - `-j, --jobs <int>`: The maximum number of concurrent workers/processors to use for parallel execution. Defaults to the number of CPU cores available.
96
+ - `-s, --include-sub-repos`: Include sub-repositories (e.g., Git worktrees and submodules) in the search target list.
93
97
  - `-h, --help`: Show the help message and exit.
94
98
  - `-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.
95
99
 
@@ -6,6 +6,7 @@
6
6
  #
7
7
  # Author: James Cherti
8
8
  # URL: https://github.com/jamescherti/git-rexec
9
+ # Version: 1.0.1
9
10
  #
10
11
  # Copyright (C) 2019-2026 James Cherti
11
12
  #
@@ -100,51 +101,95 @@ def git_toplevel(repo_path: Path) -> Optional[Path]:
100
101
  return None
101
102
 
102
103
 
103
- def git_fd_find_repo(path: Path, max_workers: int) -> set[Path]:
104
- """Find all Git repositories in 'path' using fd.
104
+ def git_py_find_repo(root: Path) -> tuple[set[Path], set[Path]]:
105
+ """Recursively find all Git repositories under the root path using Python.
106
+
107
+ :param root: Directory to search under.
108
+ :return: A tuple containing standard repository paths and sub-repository
109
+ paths.
110
+ """
111
+ repos: set[Path] = set()
112
+ sub_repos: set[Path] = set()
105
113
 
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.
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:
120
+ continue
121
+
122
+ # Clear dirnames to prevent os.walk from descending into
123
+ # subdirectories
124
+ dirnames.clear()
125
+
126
+ return repos, sub_repos
127
+
128
+ def get_sub_repos(repo_path: Path) -> set[Path]:
129
+ """Extract worktrees and submodules from a Git repository.
130
+
131
+ :param repo_path: The path to the standard repository.
132
+ :return: A set of absolute paths to worktrees and submodules.
109
133
  """
110
- path_str: str = str(path)
134
+ sub_repos: set[Path] = set()
135
+
136
+ # Discover worktrees
137
+ # pylint: disable=too-many-try-statements
111
138
  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
- ],
139
+ wt_proc: subprocess.CompletedProcess[str] = subprocess.run(
140
+ ["git", "-C", str(repo_path), "worktree", "list", "--porcelain"],
121
141
  capture_output=True,
122
142
  text=True,
123
- check=False
143
+ check=True,
124
144
  )
125
- except FileNotFoundError as err:
126
- print(f"Error: {err}", file=sys.stderr)
127
- sys.exit(1)
128
145
 
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
- }
146
+ current_wt: Optional[Path] = None
147
+ is_bare: bool = False
135
148
 
149
+ for line in wt_proc.stdout.splitlines():
150
+ line = line.strip()
136
151
 
137
- def git_py_find_repo(root: Path) -> set[Path]:
138
- """Recursively find all Git repositories under the root path using Python.
152
+ # Git porcelain separates blocks with a blank line
153
+ if not line:
154
+ if current_wt and not is_bare \
155
+ and current_wt != repo_path.resolve():
156
+ sub_repos.add(current_wt)
157
+ # Reset state for the next block
158
+ current_wt = None
159
+ is_bare = False
160
+ continue
161
+
162
+ if line.startswith("worktree "):
163
+ current_wt = Path(line[9:]).resolve()
164
+ elif line == "bare":
165
+ is_bare = True
166
+
167
+ # Process the final block in case the output lacks a trailing newline
168
+ if current_wt and not is_bare and current_wt != repo_path.resolve():
169
+ sub_repos.add(current_wt)
170
+
171
+ except subprocess.CalledProcessError:
172
+ pass
173
+
174
+ # Discover submodules
175
+ try:
176
+ sm_proc: subprocess.CompletedProcess[str] = subprocess.run(
177
+ ["git", "-C", str(repo_path),
178
+ "submodule", "foreach", "--quiet", "pwd"],
179
+ capture_output=True,
180
+ text=True,
181
+ check=True,
182
+ )
183
+
184
+ for line in sm_proc.stdout.splitlines():
185
+ sm_path: Path = Path(line.strip()).resolve()
186
+ sub_repos.add(sm_path)
187
+
188
+ except subprocess.CalledProcessError:
189
+ pass
190
+
191
+ return sub_repos
139
192
 
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
193
 
149
194
 
150
195
  def run_command_get_output(repo_path: Path, cmd_list: list[str],
@@ -242,24 +287,39 @@ def process_repo(repo_path: Path, exec_parallel_cmd: Optional[list[str]],
242
287
 
243
288
  def discover_repos(directory: Path,
244
289
  max_workers: int,
245
- exclude_dirs: list[str]) -> set[Path]:
246
- """Discover Git repositories starting from 'directory' and apply exclusions.
290
+ exclude_dirs: list[str],
291
+ include_sub_repos: bool) -> set[Path]:
292
+ """Discover Git repositories starting from directory and apply exclusions.
247
293
 
248
294
  :param directory: The root directory for search.
249
295
  :param max_workers: Maximum number of threads/workers.
250
296
  :param exclude_dirs: Directories to exclude.
297
+ :param include_sub_repos: Whether to include sub-repositories.
251
298
  :return: A set of repository root paths.
252
299
  """
253
300
  repos: set[Path] = set()
301
+ worktrees: set[Path] = set()
254
302
 
255
303
  # Discover all repositories
256
304
  toplevel: Optional[Path] = git_toplevel(directory)
257
305
  if toplevel:
258
306
  repos = {toplevel}
259
- elif shutil.which("fd"):
260
- repos = git_fd_find_repo(directory, max_workers)
261
307
  else:
262
- repos = git_py_find_repo(directory)
308
+ repos, worktrees = git_py_find_repo(directory)
309
+
310
+ # Extract and append sub-repositories concurrently if requested
311
+ if include_sub_repos:
312
+ sub_repos: set[Path] = set()
313
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
314
+ futures: list[Future[set[Path]]] = [
315
+ executor.submit(get_sub_repos, repo)
316
+ for repo in repos | worktrees
317
+ ]
318
+ for future in as_completed(futures):
319
+ result: set[Path] = future.result()
320
+ sub_repos.update(result)
321
+
322
+ repos.update(sub_repos)
263
323
 
264
324
  # Filter out excluded directories
265
325
  if not exclude_dirs:
@@ -303,7 +363,7 @@ def execute_parallel_tasks(
303
363
  result: Optional[RepoContext] = future.result()
304
364
  if result:
305
365
  results.append(result)
306
- except Exception as exc:
366
+ except (OSError, ValueError) as exc:
307
367
  print(f"Error processing repository {repo_path}: {exc}",
308
368
  file=sys.stderr)
309
369
 
@@ -357,6 +417,12 @@ def parse_args() -> argparse.Namespace:
357
417
  "([EXEC] and [EXEC-P]) that prefix execution output."),
358
418
  default=False
359
419
  )
420
+ parser.add_argument(
421
+ "-s", "--include-sub-repos",
422
+ action="store_true",
423
+ help="Include sub-repositories (e.g., Git worktrees and submodules)",
424
+ default=False
425
+ )
360
426
  parser.add_argument(
361
427
  "exec_cmd",
362
428
  type=str,
@@ -428,7 +494,8 @@ def main() -> None:
428
494
  # Discover Repositories
429
495
  repos: set[Path] = discover_repos(args.directory.absolute(),
430
496
  args.max_workers,
431
- args.exclude_dir)
497
+ args.exclude_dir,
498
+ args.include_sub_repos)
432
499
 
433
500
  # setup background command list if needed
434
501
  exec_parallel_cmd: Optional[list[str]] = None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: git-rexec
3
- Version: 1.0.0
3
+ Version: 1.0.1
4
4
  Summary: Locate Git repositories and execute commands against them in parallel.
5
5
  Author: James Cherti
6
6
  License: GPL-3.0-or-later
@@ -19,7 +19,7 @@ Requires-Dist: setproctitle; extra == "extras"
19
19
 
20
20
  # git-rexec: Find Git Repositories and Execute Commands Against Them, either Sequentially or in Parallel
21
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.
22
+ 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.
23
23
 
24
24
  Here are examples demonstrating how to use `git-rexec`:
25
25
  - Execute `git status -s` across all discovered Git repositories (found by searching recursively under the current working directory) in parallel (`-p` or `--parallel`):
@@ -32,6 +32,12 @@ Here are examples demonstrating how to use `git-rexec`:
32
32
  git-rexec -j 5 --parallel -- git fetch
33
33
  ```
34
34
 
35
+
36
+ - 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:
37
+ ```bash
38
+ git-rexec -s -p -- git status
39
+ ```
40
+
35
41
  - 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
42
  ```bash
37
43
  git-rexec -C ~/projects --exclude-dir ~/projects/archive --parallel -- git status -s
@@ -47,10 +53,10 @@ If this helps your workflow, please support the project by **⭐ starring git-re
47
53
  ## Features
48
54
 
49
55
  - Recursively discover Git repositories starting from a specified root directory.
56
+ - Optional flag to include sub-repositories (e.g., Git worktrees and submodules) in the execution target list.
50
57
  - Execute shell commands across multiple repositories in parallel using worker threads.
51
58
  - Filter target repositories based on the exit code of a conditional check (`--if-exec`).
52
59
  - 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
60
 
55
61
  ## Installation
56
62
 
@@ -67,7 +73,7 @@ sudo cp git-rexec /usr/local/bin/
67
73
  Install the package directly from the Git repository using `pip`:
68
74
 
69
75
  ```bash
70
- pip install --user git+https://github.com/jamescherti/git-rexec
76
+ pip install --user git-rexec
71
77
  ```
72
78
 
73
79
  ## Dependencies
@@ -75,7 +81,6 @@ pip install --user git+https://github.com/jamescherti/git-rexec
75
81
  ### System Dependencies
76
82
 
77
83
  - `git`: Required for repository validation and execution.
78
- - `fd` (Optional): Highly recommended for faster repository discovery.
79
84
 
80
85
  ### Python Dependencies (Optional)
81
86
 
@@ -86,14 +91,12 @@ You can install the optional Python dependencies via pip:
86
91
 
87
92
  ```bash
88
93
  pip install colorama setproctitle
89
-
90
94
  ```
91
95
 
92
96
  ## Usage
93
97
 
94
98
  ```bash
95
99
  git-rexec [OPTIONS] [exec_cmd ...]
96
-
97
100
  ```
98
101
 
99
102
  *(Assuming the `git-rexec` script is executable and in your PATH.)*
@@ -109,6 +112,7 @@ git-rexec [OPTIONS] [exec_cmd ...]
109
112
  - `-p, --parallel`: Execute the command in parallel using threads.
110
113
  - `-i, --if-exec <command>`: Execute the main command only if this check command returns an exit code of `0`.
111
114
  - `-j, --jobs <int>`: The maximum number of concurrent workers/processors to use for parallel execution. Defaults to the number of CPU cores available.
115
+ - `-s, --include-sub-repos`: Include sub-repositories (e.g., Git worktrees and submodules) in the search target list.
112
116
  - `-h, --help`: Show the help message and exit.
113
117
  - `-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
118
 
@@ -1,3 +1,4 @@
1
+ .gitignore
1
2
  README.md
2
3
  git-rexec
3
4
  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.0"
7
+ version = "1.0.1"
8
8
  description = "Locate Git repositories and execute commands against them in parallel."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
File without changes