batchfetch 1.3.6__tar.gz → 1.3.8__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: batchfetch
3
- Version: 1.3.6
3
+ Version: 1.3.8
4
4
  Summary: Efficiently clone and pull multiple Git repositories.
5
5
  Home-page: https://github.com/jamescherti/batchfetch
6
6
  Author: James Cherti
@@ -80,7 +80,7 @@ tasks:
80
80
  delete: true
81
81
  ```
82
82
 
83
- Execute the `batchfetch` command from the same directory as `batchfetch.yml` to make it clone or update the local copies of the repositories above.
83
+ Execute the `batchfetch` command from the same directory as `batchfetch.yaml` to make it clone or update the local copies of the repositories above.
84
84
 
85
85
  ## Command-line options
86
86
 
@@ -94,7 +94,7 @@ Efficiently clone/pull multiple Git repositories in parallel.
94
94
  positional arguments:
95
95
  target This is a target path that batchfetch is supposed to
96
96
  handle. When no target is specified, execute the tasks
97
- of all target paths defined in the batchfetch.yml list
97
+ of all target paths defined in the batchfetch.yaml list
98
98
  of tasks.
99
99
 
100
100
  options:
@@ -117,6 +117,7 @@ options:
117
117
  ```
118
118
 
119
119
  ## Features
120
+
120
121
  - Git Clone and Fetch/Merge: Clones the repositories and their submodules, ensuring that all the repositories are always up-to-date by fetching and merging changes.
121
122
  - Parallel Operations: Utilizes threads to simultaneously Git clone or pull multiple repositories, dramatically reducing wait times.
122
123
  - User-Friendly Interface: Provides simple and straightforward command-line options that make it easy to get started and effectively manage your repositories.
@@ -135,7 +136,7 @@ When *batchfetch* encounters an untracked file, it displays an error message to
135
136
 
136
137
  Here is an example of a *batchfetch.yaml* file that enables *batchfetch* to accept a list of untracked files:
137
138
 
138
- ``` yaml
139
+ ```yaml
139
140
  options:
140
141
  ignore_untracked:
141
142
  - ./test
@@ -161,7 +162,8 @@ Batchfetch is fast, not only because it runs Git commands in parallel, but also
161
162
  When the user has specifies a revision (branch or commit reference), Batchfetch only performs a `git fetch` if that revision does not exist locally. If the revision is already up to date, it simply proceeds to the next repository in the queue.
162
163
 
163
164
  That's why it is highly recommended to always specify the revision to speed up Batchfetch, if speed is important to you. Here is an example of a `batchfetch.yaml` file where the branch (`1.1.0`) or commit reference (`b9c6d9b6134b4981760893254f804a371ffbc899`) is specified:
164
- ``` yaml
165
+
166
+ ```yaml
165
167
  tasks:
166
168
  - git: https://github.com/jamescherti/outline-indent.el
167
169
  revision: "1.1.0"
@@ -171,12 +173,59 @@ tasks:
171
173
  revision: b9c6d9b6134b4981760893254f804a371ffbc899
172
174
  ```
173
175
 
176
+ ### How to customize Git clone, merge, and update strategies?
177
+
178
+ Batchfetch allows you to define advanced options for interacting with Git repositories. These parameters can be declared globally under the `options` block to apply to all tasks, or individually within a specific task to override the global defaults.
179
+
180
+ * `git_clone_args`: A list of extra arguments to pass to `git clone`.
181
+ * `git_merge_args`: A list of extra arguments to pass to `git merge`.
182
+ * `git_update_strategy`: Specifies how updates should be applied. Valid options are `"merge"` (default), `"rebase"`, or `"reset"`.
183
+ * `git_pull`: A boolean indicating whether to run Git pull/fetch operations. Set to `false` to disable updating entirely (default: `true`).
184
+
185
+ Here is an example demonstrating both global and local configurations:
186
+
187
+ ```yaml
188
+ ---
189
+
190
+ options:
191
+ # Apply these defaults to all tasks
192
+ git_update_strategy: reset
193
+ git_clone_args:
194
+ - --recurse-submodules
195
+
196
+ tasks:
197
+ - git: https://github.com/jamescherti/compile-angel.el
198
+ git_pull: false # Do not attempt to update this repo
199
+
200
+ - git: https://github.com/jamescherti/outline-indent.el
201
+ # Override global strategy specifically for this task
202
+ git_update_strategy: merge
203
+ git_merge_args:
204
+ - --no-ff
205
+
206
+ ```
207
+
208
+ ### How to configure additional Git remotes?
209
+
210
+ You can define additional remotes for a Git repository using the `remote` option. Pass a list of key-value pairs where the key is the remote name and the value is the corresponding URL. Batchfetch will automatically ensure these remotes are present and point to the correct URLs.
211
+
212
+ ```yaml
213
+ ---
214
+ tasks:
215
+ - git: https://github.com/jamescherti/easysession.el
216
+ path: emacs/easysession
217
+ remote:
218
+ upstream: https://github.com/name/repo
219
+ upstream2: https://github.com/name/repo2
220
+ ```
221
+
174
222
  ### How to execute a command before and after a task?
175
223
 
176
224
  To execute a command both before and after a specific task, you can define the `exec_before` and `exec_after` directives within the task configuration. These directives specify commands to be executed at the respective stages of the task lifecycle.
177
225
 
178
226
  Here is an example:
179
- ``` yaml
227
+
228
+ ```yaml
180
229
  ---
181
230
  tasks:
182
231
  - git: https://github.com/jamescherti/easysession.el
@@ -187,11 +236,12 @@ tasks:
187
236
 
188
237
  ### How to make batchfetch handle only one path?
189
238
 
190
- To configure `batchfetch` to handle a specific path, you can define your tasks in a `batchfetch.yml` file and pass the desired path as an argument to the `batchfetch` command.
239
+ To configure `batchfetch` to handle a specific path, you can define your tasks in a `batchfetch.yaml` file and pass the desired path as an argument to the `batchfetch` command.
191
240
 
192
- #### Example `batchfetch.yml` file:
241
+ #### Example `batchfetch.yaml` file:
193
242
 
194
243
  In the following example, the `easysession` task clones two Git repositories:
244
+
195
245
  ```yaml
196
246
  ---
197
247
  tasks:
@@ -208,7 +258,7 @@ To make `batchfetch` clone only `easysession`, pass its path as an argument:
208
258
  batchfetch easysession
209
259
  ```
210
260
 
211
- This will execute only the task corresponding to the `easysession` path, skipping all others in the `batchfetch.yml` file.
261
+ This will execute only the task corresponding to the `easysession` path, skipping all others in the `batchfetch.yaml` file.
212
262
 
213
263
  ### How can I configure batchfetch to load a file other than batchfetch.yaml?
214
264
 
@@ -227,7 +277,7 @@ batchfetch
227
277
 
228
278
  ## License
229
279
 
230
- Copyright (C) 2024-2025 [James Cherti](https://www.jamescherti.com)
280
+ Copyright (C) 2024-2026 [James Cherti](https://www.jamescherti.com)
231
281
 
232
282
  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.
233
283
 
@@ -46,7 +46,7 @@ tasks:
46
46
  delete: true
47
47
  ```
48
48
 
49
- Execute the `batchfetch` command from the same directory as `batchfetch.yml` to make it clone or update the local copies of the repositories above.
49
+ Execute the `batchfetch` command from the same directory as `batchfetch.yaml` to make it clone or update the local copies of the repositories above.
50
50
 
51
51
  ## Command-line options
52
52
 
@@ -60,7 +60,7 @@ Efficiently clone/pull multiple Git repositories in parallel.
60
60
  positional arguments:
61
61
  target This is a target path that batchfetch is supposed to
62
62
  handle. When no target is specified, execute the tasks
63
- of all target paths defined in the batchfetch.yml list
63
+ of all target paths defined in the batchfetch.yaml list
64
64
  of tasks.
65
65
 
66
66
  options:
@@ -83,6 +83,7 @@ options:
83
83
  ```
84
84
 
85
85
  ## Features
86
+
86
87
  - Git Clone and Fetch/Merge: Clones the repositories and their submodules, ensuring that all the repositories are always up-to-date by fetching and merging changes.
87
88
  - Parallel Operations: Utilizes threads to simultaneously Git clone or pull multiple repositories, dramatically reducing wait times.
88
89
  - User-Friendly Interface: Provides simple and straightforward command-line options that make it easy to get started and effectively manage your repositories.
@@ -101,7 +102,7 @@ When *batchfetch* encounters an untracked file, it displays an error message to
101
102
 
102
103
  Here is an example of a *batchfetch.yaml* file that enables *batchfetch* to accept a list of untracked files:
103
104
 
104
- ``` yaml
105
+ ```yaml
105
106
  options:
106
107
  ignore_untracked:
107
108
  - ./test
@@ -127,7 +128,8 @@ Batchfetch is fast, not only because it runs Git commands in parallel, but also
127
128
  When the user has specifies a revision (branch or commit reference), Batchfetch only performs a `git fetch` if that revision does not exist locally. If the revision is already up to date, it simply proceeds to the next repository in the queue.
128
129
 
129
130
  That's why it is highly recommended to always specify the revision to speed up Batchfetch, if speed is important to you. Here is an example of a `batchfetch.yaml` file where the branch (`1.1.0`) or commit reference (`b9c6d9b6134b4981760893254f804a371ffbc899`) is specified:
130
- ``` yaml
131
+
132
+ ```yaml
131
133
  tasks:
132
134
  - git: https://github.com/jamescherti/outline-indent.el
133
135
  revision: "1.1.0"
@@ -137,12 +139,59 @@ tasks:
137
139
  revision: b9c6d9b6134b4981760893254f804a371ffbc899
138
140
  ```
139
141
 
142
+ ### How to customize Git clone, merge, and update strategies?
143
+
144
+ Batchfetch allows you to define advanced options for interacting with Git repositories. These parameters can be declared globally under the `options` block to apply to all tasks, or individually within a specific task to override the global defaults.
145
+
146
+ * `git_clone_args`: A list of extra arguments to pass to `git clone`.
147
+ * `git_merge_args`: A list of extra arguments to pass to `git merge`.
148
+ * `git_update_strategy`: Specifies how updates should be applied. Valid options are `"merge"` (default), `"rebase"`, or `"reset"`.
149
+ * `git_pull`: A boolean indicating whether to run Git pull/fetch operations. Set to `false` to disable updating entirely (default: `true`).
150
+
151
+ Here is an example demonstrating both global and local configurations:
152
+
153
+ ```yaml
154
+ ---
155
+
156
+ options:
157
+ # Apply these defaults to all tasks
158
+ git_update_strategy: reset
159
+ git_clone_args:
160
+ - --recurse-submodules
161
+
162
+ tasks:
163
+ - git: https://github.com/jamescherti/compile-angel.el
164
+ git_pull: false # Do not attempt to update this repo
165
+
166
+ - git: https://github.com/jamescherti/outline-indent.el
167
+ # Override global strategy specifically for this task
168
+ git_update_strategy: merge
169
+ git_merge_args:
170
+ - --no-ff
171
+
172
+ ```
173
+
174
+ ### How to configure additional Git remotes?
175
+
176
+ You can define additional remotes for a Git repository using the `remote` option. Pass a list of key-value pairs where the key is the remote name and the value is the corresponding URL. Batchfetch will automatically ensure these remotes are present and point to the correct URLs.
177
+
178
+ ```yaml
179
+ ---
180
+ tasks:
181
+ - git: https://github.com/jamescherti/easysession.el
182
+ path: emacs/easysession
183
+ remote:
184
+ upstream: https://github.com/name/repo
185
+ upstream2: https://github.com/name/repo2
186
+ ```
187
+
140
188
  ### How to execute a command before and after a task?
141
189
 
142
190
  To execute a command both before and after a specific task, you can define the `exec_before` and `exec_after` directives within the task configuration. These directives specify commands to be executed at the respective stages of the task lifecycle.
143
191
 
144
192
  Here is an example:
145
- ``` yaml
193
+
194
+ ```yaml
146
195
  ---
147
196
  tasks:
148
197
  - git: https://github.com/jamescherti/easysession.el
@@ -153,11 +202,12 @@ tasks:
153
202
 
154
203
  ### How to make batchfetch handle only one path?
155
204
 
156
- To configure `batchfetch` to handle a specific path, you can define your tasks in a `batchfetch.yml` file and pass the desired path as an argument to the `batchfetch` command.
205
+ To configure `batchfetch` to handle a specific path, you can define your tasks in a `batchfetch.yaml` file and pass the desired path as an argument to the `batchfetch` command.
157
206
 
158
- #### Example `batchfetch.yml` file:
207
+ #### Example `batchfetch.yaml` file:
159
208
 
160
209
  In the following example, the `easysession` task clones two Git repositories:
210
+
161
211
  ```yaml
162
212
  ---
163
213
  tasks:
@@ -174,7 +224,7 @@ To make `batchfetch` clone only `easysession`, pass its path as an argument:
174
224
  batchfetch easysession
175
225
  ```
176
226
 
177
- This will execute only the task corresponding to the `easysession` path, skipping all others in the `batchfetch.yml` file.
227
+ This will execute only the task corresponding to the `easysession` path, skipping all others in the `batchfetch.yaml` file.
178
228
 
179
229
  ### How can I configure batchfetch to load a file other than batchfetch.yaml?
180
230
 
@@ -193,7 +243,7 @@ batchfetch
193
243
 
194
244
  ## License
195
245
 
196
- Copyright (C) 2024-2025 [James Cherti](https://www.jamescherti.com)
246
+ Copyright (C) 2024-2026 [James Cherti](https://www.jamescherti.com)
197
247
 
198
248
  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.
199
249
 
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env python
2
2
  #
3
- # Copyright (C) 2024-2025 James Cherti
3
+ # Copyright (C) 2024-2026 James Cherti
4
4
  # URL: https://github.com/jamescherti/batchfetch
5
5
  #
6
6
  # This program is free software: you can redistribute it and/or modify it under
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env python
2
2
  #
3
- # Copyright (C) 2024-2025 James Cherti
3
+ # Copyright (C) 2024-2026 James Cherti
4
4
  # URL: https://github.com/jamescherti/batchfetch
5
5
  #
6
6
  # This program is free software: you can redistribute it and/or modify it under
@@ -16,12 +16,14 @@
16
16
  # You should have received a copy of the GNU General Public License along with
17
17
  # this program. If not, see <https://www.gnu.org/licenses/>.
18
18
  #
19
- "Base class used to run tasks in parallel."
19
+ """Base class used to run tasks in parallel."""
20
+
21
+ from __future__ import annotations
20
22
 
21
23
  import os
22
24
  from copy import deepcopy
23
25
  from pathlib import Path
24
- from typing import Any, Dict, List
26
+ from typing import Any
25
27
 
26
28
  from schema import Optional, Or, Schema
27
29
 
@@ -29,7 +31,7 @@ from .helpers import run_indent_str
29
31
 
30
32
 
31
33
  class BatchFetchError(Exception):
32
- """Exception raised by Downloader()."""
34
+ """Exception raised by batchfetch functions."""
33
35
 
34
36
 
35
37
  class DataAlreadyInitialized(Exception):
@@ -37,21 +39,25 @@ class DataAlreadyInitialized(Exception):
37
39
 
38
40
 
39
41
  class TaskBase:
40
- def __init__(self, data: Dict[str, Any], options: Dict[str, Any]):
41
- self.global_options_schema: Dict[Any, Any] = {}
42
- self.global_options_values: Dict[str, Any] = options
42
+ """Base class representing a general task to be executed."""
43
+
44
+ def __init__(self, data: dict[str, Any], options: dict[str, Any]) -> None:
45
+ """Initialize the task with specific data and options."""
46
+ self.global_options_schema: dict[Any, Any] = {}
47
+ self.global_options_values: dict[str, Any] = options
43
48
 
44
- self.task_schema: Dict[Any, Any] = {}
45
- self.task_default_values: Dict[str, Any] = {}
49
+ self.task_schema: dict[Any, Any] = {}
50
+ self.task_default_values: dict[str, Any] = {}
46
51
 
47
52
  self._item_values = data
48
53
 
49
54
  # Variables
50
- self.values: Dict[str, Any] = {}
51
- self.options: Dict[str, Any] = {}
55
+ self.values: dict[str, Any] = {}
56
+ self.options: dict[str, Any] = {}
52
57
  self._values_initialized = False
53
58
 
54
- def _initialize_data(self):
59
+ def _initialize_data(self) -> None:
60
+ """Initialize the options and data values for the task."""
55
61
  if self._values_initialized:
56
62
  raise DataAlreadyInitialized
57
63
 
@@ -76,10 +82,12 @@ class TaskBase:
76
82
  "changed": False,
77
83
  }
78
84
 
79
- def validate_schema(self):
85
+ def validate_schema(self) -> None:
86
+ """Validate the schema of the task data and options."""
80
87
  self._initialize_data()
81
88
 
82
- def __getitem__(self, key):
89
+ def __getitem__(self, key: str) -> Any:
90
+ """Retrieve an item from the task values or options."""
83
91
  self._initialize_data()
84
92
 
85
93
  if key in self.values:
@@ -88,14 +96,15 @@ class TaskBase:
88
96
  if key in self.options:
89
97
  return self.options[key]
90
98
 
91
- raise KeyError(f"The item '{key}' was not found in '{self.values}")
99
+ raise KeyError(f"The item '{key}' was not found in '{self.values}'")
92
100
 
93
101
 
94
102
  class TaskBatchFetch(TaskBase):
95
103
  """Plugin downloader base class."""
96
104
 
97
- def __init__(self, data: Dict[str, Any], options: Dict[str, Any]):
98
- new_options: Dict[str, Any] = {"exec_before": [],
105
+ def __init__(self, data: dict[str, Any], options: dict[str, Any]) -> None:
106
+ """Initialize the batchfetch task plugin."""
107
+ new_options: dict[str, Any] = {"exec_before": [],
99
108
  "exec_after": [],
100
109
  "ignore_untracked": []}
101
110
  new_options.update(options)
@@ -107,13 +116,13 @@ class TaskBatchFetch(TaskBase):
107
116
  self.env = os.environ.copy()
108
117
 
109
118
  # Default
110
- self.global_options_schema: Dict[Any, Any] = {
119
+ self.global_options_schema: dict[Any, Any] = {
111
120
  Optional("exec_before"): Or([str], str),
112
121
  Optional("exec_after"): Or([str], str),
113
122
  Optional("ignore_untracked"): Or([str], str),
114
123
  }
115
124
 
116
- self.task_schema: Dict[Any, Any] = {
125
+ self.task_schema: dict[Any, Any] = {
117
126
  Optional("path"): str,
118
127
  Optional("delete"): bool,
119
128
 
@@ -124,12 +133,13 @@ class TaskBatchFetch(TaskBase):
124
133
  Optional("ignore_untracked"): Or([str], str),
125
134
  }
126
135
 
127
- self.task_default_values: Dict[str, Any] = {
136
+ self.task_default_values: dict[str, Any] = {
128
137
  # Optional items
129
138
  "delete": False,
130
139
  }
131
140
 
132
- def _initialize_data(self):
141
+ def _initialize_data(self) -> None:
142
+ """Safely initialize data for the batchfetch task."""
133
143
  try:
134
144
  super()._initialize_data()
135
145
  except DataAlreadyInitialized:
@@ -138,14 +148,16 @@ class TaskBatchFetch(TaskBase):
138
148
  # Mark values as initialized
139
149
  self._values_initialized = True
140
150
 
141
- def _local_task_exec(self, *args, **kwargs):
151
+ def _local_task_exec(self, *args: Any, **kwargs: Any) -> None:
152
+ """Execute a local task command and capture its output."""
142
153
  stdout = run_indent_str(env=self.env, spaces=self.indent,
143
154
  *args, **kwargs)
144
155
  if not stdout.endswith("\n"):
145
156
  stdout += "\n"
146
157
  self.add_output(stdout)
147
158
 
148
- def _exec_before(self, cwd: os.PathLike = Path(".")):
159
+ def _exec_before(self, cwd: os.PathLike = Path(".")) -> None:
160
+ """Execute pre-task commands defined in options and task items."""
149
161
  self._initialize_data()
150
162
  if self["delete"]:
151
163
  return
@@ -162,9 +174,10 @@ class TaskBatchFetch(TaskBase):
162
174
  if cmd:
163
175
  self._local_task_exec(cmd, cwd=str(cwd))
164
176
 
165
- def _exec_after(self, cwd: os.PathLike = Path(".")):
177
+ def _exec_after(self, cwd: os.PathLike = Path(".")) -> None:
178
+ """Execute post-task commands if the task was changed successfully."""
166
179
  self._initialize_data()
167
- if self["delete"] or not self.is_changed():
180
+ if self["delete"] or self.is_error() or not self.is_changed():
168
181
  return
169
182
 
170
183
  # Local
@@ -180,32 +193,39 @@ class TaskBatchFetch(TaskBase):
180
193
  self._local_task_exec(cmd, cwd=str(cwd))
181
194
 
182
195
  def is_changed(self) -> bool:
196
+ """Return True if the task execution resulted in changes."""
183
197
  self._initialize_data()
184
198
  return bool(self.values["result"]["changed"])
185
199
 
186
- def set_changed(self, changed: bool):
200
+ def set_changed(self, changed: bool) -> None:
201
+ """Set the changed status of the task."""
187
202
  self._initialize_data()
188
203
  self.values["result"]["changed"] = changed
189
204
 
190
205
  def get_changed(self) -> bool:
206
+ """Return the changed status of the task without initializing data."""
191
207
  try:
192
208
  return bool(self.values["result"]["changed"])
193
209
  except KeyError:
194
210
  return False
195
211
 
196
212
  def is_error(self) -> bool:
213
+ """Return True if the task encountered an error."""
197
214
  self._initialize_data()
198
215
  return bool(self.values["result"]["error"])
199
216
 
200
- def set_error(self, error: bool):
217
+ def set_error(self, error: bool) -> None:
218
+ """Set the error status of the task."""
201
219
  self._initialize_data()
202
220
  self.values["result"]["error"] = error
203
221
 
204
- def set_output(self, output: str):
222
+ def set_output(self, output: str) -> None:
223
+ """Set the output string of the task."""
205
224
  self._initialize_data()
206
225
  self.values["result"]["output"] = output
207
226
 
208
- def add_output(self, output: str):
227
+ def add_output(self, output: str) -> None:
228
+ """Append a string to the output of the task."""
209
229
  self._initialize_data()
210
230
  self.values["result"]["output"] += output
211
231
 
@@ -214,6 +234,7 @@ class TaskBatchFetch(TaskBase):
214
234
  self._initialize_data()
215
235
  return str(self.values["result"]["output"])
216
236
 
217
- def update(self):
237
+ def update(self) -> dict[str, Any]:
238
+ """Update the task data and return the current values."""
218
239
  self._initialize_data()
219
240
  return self.values