batchfetch 1.3.7__tar.gz → 1.3.9__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.
- batchfetch-1.3.9/.github/FUNDING.yml +1 -0
- batchfetch-1.3.9/.gitignore +138 -0
- {batchfetch-1.3.7 → batchfetch-1.3.9}/PKG-INFO +21 -21
- {batchfetch-1.3.7 → batchfetch-1.3.9}/README.md +20 -20
- {batchfetch-1.3.7 → batchfetch-1.3.9}/batchfetch/batchfetch_base.py +21 -2
- {batchfetch-1.3.7 → batchfetch-1.3.9}/batchfetch/batchfetch_cli.py +14 -6
- {batchfetch-1.3.7 → batchfetch-1.3.9}/batchfetch/batchfetch_git.py +114 -26
- {batchfetch-1.3.7 → batchfetch-1.3.9}/batchfetch.egg-info/PKG-INFO +21 -21
- {batchfetch-1.3.7 → batchfetch-1.3.9}/batchfetch.egg-info/SOURCES.txt +6 -1
- batchfetch-1.3.9/run_tests.sh +23 -0
- {batchfetch-1.3.7 → batchfetch-1.3.9}/setup.py +1 -1
- batchfetch-1.3.9/tests/data/test-md5sum.txt +3 -0
- batchfetch-1.3.9/tests/data/test-run_simple.sh +5 -0
- {batchfetch-1.3.7 → batchfetch-1.3.9}/LICENSE +0 -0
- {batchfetch-1.3.7 → batchfetch-1.3.9}/batchfetch/__init__.py +0 -0
- {batchfetch-1.3.7 → batchfetch-1.3.9}/batchfetch/helpers.py +0 -0
- {batchfetch-1.3.7 → batchfetch-1.3.9}/batchfetch.egg-info/dependency_links.txt +0 -0
- {batchfetch-1.3.7 → batchfetch-1.3.9}/batchfetch.egg-info/entry_points.txt +0 -0
- {batchfetch-1.3.7 → batchfetch-1.3.9}/batchfetch.egg-info/requires.txt +0 -0
- {batchfetch-1.3.7 → batchfetch-1.3.9}/batchfetch.egg-info/top_level.txt +0 -0
- {batchfetch-1.3.7 → batchfetch-1.3.9}/setup.cfg +0 -0
- {batchfetch-1.3.7 → batchfetch-1.3.9}/tests/test_helpers.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
github: jamescherti
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py,cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
#Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
|
98
|
+
__pypackages__/
|
|
99
|
+
|
|
100
|
+
# Celery stuff
|
|
101
|
+
celerybeat-schedule
|
|
102
|
+
celerybeat.pid
|
|
103
|
+
|
|
104
|
+
# SageMath parsed files
|
|
105
|
+
*.sage.py
|
|
106
|
+
|
|
107
|
+
# Environments
|
|
108
|
+
.env
|
|
109
|
+
.venv
|
|
110
|
+
env/
|
|
111
|
+
venv/
|
|
112
|
+
ENV/
|
|
113
|
+
env.bak/
|
|
114
|
+
venv.bak/
|
|
115
|
+
|
|
116
|
+
# Spyder project settings
|
|
117
|
+
.spyderproject
|
|
118
|
+
.spyproject
|
|
119
|
+
|
|
120
|
+
# Rope project settings
|
|
121
|
+
.ropeproject
|
|
122
|
+
|
|
123
|
+
# mkdocs documentation
|
|
124
|
+
/site
|
|
125
|
+
|
|
126
|
+
# mypy
|
|
127
|
+
.mypy_cache/
|
|
128
|
+
.dmypy.json
|
|
129
|
+
dmypy.json
|
|
130
|
+
|
|
131
|
+
# Pyre type checker
|
|
132
|
+
.pyre/
|
|
133
|
+
|
|
134
|
+
# pytype static type analyzer
|
|
135
|
+
.pytype/
|
|
136
|
+
|
|
137
|
+
# Cython debug symbols
|
|
138
|
+
cython_debug/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: batchfetch
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.9
|
|
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.
|
|
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.
|
|
97
|
+
of all target paths defined in the batchfetch.yaml list
|
|
98
98
|
of tasks.
|
|
99
99
|
|
|
100
100
|
options:
|
|
@@ -119,7 +119,7 @@ options:
|
|
|
119
119
|
## Features
|
|
120
120
|
|
|
121
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.
|
|
122
|
-
- Parallel Operations:
|
|
122
|
+
- Parallel Operations: Uses threads to simultaneously Git clone or pull multiple repositories, reducing wait times.
|
|
123
123
|
- User-Friendly Interface: Provides simple and straightforward command-line options that make it easy to get started and effectively manage your repositories.
|
|
124
124
|
- Custom Configuration: Allows the use of a YAML configuration file to specify and manage the repositories you interact with, enabling repeatable setups and consistent environments.
|
|
125
125
|
- Detect files that should not be present in directories managed by batchfetch, known as untracked files.
|
|
@@ -173,20 +173,6 @@ tasks:
|
|
|
173
173
|
revision: b9c6d9b6134b4981760893254f804a371ffbc899
|
|
174
174
|
```
|
|
175
175
|
|
|
176
|
-
### How to configure additional Git remotes?
|
|
177
|
-
|
|
178
|
-
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.
|
|
179
|
-
|
|
180
|
-
```yaml
|
|
181
|
-
---
|
|
182
|
-
tasks:
|
|
183
|
-
- git: https://github.com/jamescherti/easysession.el
|
|
184
|
-
path: emacs/easysession
|
|
185
|
-
remote:
|
|
186
|
-
upstream: https://github.com/name/repo
|
|
187
|
-
upstream2: https://github.com/name/repo2
|
|
188
|
-
```
|
|
189
|
-
|
|
190
176
|
### How to customize Git clone, merge, and update strategies?
|
|
191
177
|
|
|
192
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.
|
|
@@ -219,6 +205,20 @@ tasks:
|
|
|
219
205
|
|
|
220
206
|
```
|
|
221
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
|
+
|
|
222
222
|
### How to execute a command before and after a task?
|
|
223
223
|
|
|
224
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.
|
|
@@ -236,9 +236,9 @@ tasks:
|
|
|
236
236
|
|
|
237
237
|
### How to make batchfetch handle only one path?
|
|
238
238
|
|
|
239
|
-
To configure `batchfetch` to handle a specific path, you can define your tasks in a `batchfetch.
|
|
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.
|
|
240
240
|
|
|
241
|
-
#### Example `batchfetch.
|
|
241
|
+
#### Example `batchfetch.yaml` file:
|
|
242
242
|
|
|
243
243
|
In the following example, the `easysession` task clones two Git repositories:
|
|
244
244
|
|
|
@@ -258,7 +258,7 @@ To make `batchfetch` clone only `easysession`, pass its path as an argument:
|
|
|
258
258
|
batchfetch easysession
|
|
259
259
|
```
|
|
260
260
|
|
|
261
|
-
This will execute only the task corresponding to the `easysession` path, skipping all others in the `batchfetch.
|
|
261
|
+
This will execute only the task corresponding to the `easysession` path, skipping all others in the `batchfetch.yaml` file.
|
|
262
262
|
|
|
263
263
|
### How can I configure batchfetch to load a file other than batchfetch.yaml?
|
|
264
264
|
|
|
@@ -46,7 +46,7 @@ tasks:
|
|
|
46
46
|
delete: true
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
Execute the `batchfetch` command from the same directory as `batchfetch.
|
|
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.
|
|
63
|
+
of all target paths defined in the batchfetch.yaml list
|
|
64
64
|
of tasks.
|
|
65
65
|
|
|
66
66
|
options:
|
|
@@ -85,7 +85,7 @@ options:
|
|
|
85
85
|
## Features
|
|
86
86
|
|
|
87
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.
|
|
88
|
-
- Parallel Operations:
|
|
88
|
+
- Parallel Operations: Uses threads to simultaneously Git clone or pull multiple repositories, reducing wait times.
|
|
89
89
|
- User-Friendly Interface: Provides simple and straightforward command-line options that make it easy to get started and effectively manage your repositories.
|
|
90
90
|
- Custom Configuration: Allows the use of a YAML configuration file to specify and manage the repositories you interact with, enabling repeatable setups and consistent environments.
|
|
91
91
|
- Detect files that should not be present in directories managed by batchfetch, known as untracked files.
|
|
@@ -139,20 +139,6 @@ tasks:
|
|
|
139
139
|
revision: b9c6d9b6134b4981760893254f804a371ffbc899
|
|
140
140
|
```
|
|
141
141
|
|
|
142
|
-
### How to configure additional Git remotes?
|
|
143
|
-
|
|
144
|
-
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.
|
|
145
|
-
|
|
146
|
-
```yaml
|
|
147
|
-
---
|
|
148
|
-
tasks:
|
|
149
|
-
- git: https://github.com/jamescherti/easysession.el
|
|
150
|
-
path: emacs/easysession
|
|
151
|
-
remote:
|
|
152
|
-
upstream: https://github.com/name/repo
|
|
153
|
-
upstream2: https://github.com/name/repo2
|
|
154
|
-
```
|
|
155
|
-
|
|
156
142
|
### How to customize Git clone, merge, and update strategies?
|
|
157
143
|
|
|
158
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.
|
|
@@ -185,6 +171,20 @@ tasks:
|
|
|
185
171
|
|
|
186
172
|
```
|
|
187
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
|
+
|
|
188
188
|
### How to execute a command before and after a task?
|
|
189
189
|
|
|
190
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.
|
|
@@ -202,9 +202,9 @@ tasks:
|
|
|
202
202
|
|
|
203
203
|
### How to make batchfetch handle only one path?
|
|
204
204
|
|
|
205
|
-
To configure `batchfetch` to handle a specific path, you can define your tasks in a `batchfetch.
|
|
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.
|
|
206
206
|
|
|
207
|
-
#### Example `batchfetch.
|
|
207
|
+
#### Example `batchfetch.yaml` file:
|
|
208
208
|
|
|
209
209
|
In the following example, the `easysession` task clones two Git repositories:
|
|
210
210
|
|
|
@@ -224,7 +224,7 @@ To make `batchfetch` clone only `easysession`, pass its path as an argument:
|
|
|
224
224
|
batchfetch easysession
|
|
225
225
|
```
|
|
226
226
|
|
|
227
|
-
This will execute only the task corresponding to the `easysession` path, skipping all others in the `batchfetch.
|
|
227
|
+
This will execute only the task corresponding to the `easysession` path, skipping all others in the `batchfetch.yaml` file.
|
|
228
228
|
|
|
229
229
|
### How can I configure batchfetch to load a file other than batchfetch.yaml?
|
|
230
230
|
|
|
@@ -31,7 +31,7 @@ from .helpers import run_indent_str
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
class BatchFetchError(Exception):
|
|
34
|
-
"""Exception raised by
|
|
34
|
+
"""Exception raised by batchfetch functions."""
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
class DataAlreadyInitialized(Exception):
|
|
@@ -39,7 +39,10 @@ class DataAlreadyInitialized(Exception):
|
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
class TaskBase:
|
|
42
|
+
"""Base class representing a general task to be executed."""
|
|
43
|
+
|
|
42
44
|
def __init__(self, data: dict[str, Any], options: dict[str, Any]) -> None:
|
|
45
|
+
"""Initialize the task with specific data and options."""
|
|
43
46
|
self.global_options_schema: dict[Any, Any] = {}
|
|
44
47
|
self.global_options_values: dict[str, Any] = options
|
|
45
48
|
|
|
@@ -54,6 +57,7 @@ class TaskBase:
|
|
|
54
57
|
self._values_initialized = False
|
|
55
58
|
|
|
56
59
|
def _initialize_data(self) -> None:
|
|
60
|
+
"""Initialize the options and data values for the task."""
|
|
57
61
|
if self._values_initialized:
|
|
58
62
|
raise DataAlreadyInitialized
|
|
59
63
|
|
|
@@ -79,9 +83,11 @@ class TaskBase:
|
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
def validate_schema(self) -> None:
|
|
86
|
+
"""Validate the schema of the task data and options."""
|
|
82
87
|
self._initialize_data()
|
|
83
88
|
|
|
84
89
|
def __getitem__(self, key: str) -> Any:
|
|
90
|
+
"""Retrieve an item from the task values or options."""
|
|
85
91
|
self._initialize_data()
|
|
86
92
|
|
|
87
93
|
if key in self.values:
|
|
@@ -90,13 +96,14 @@ class TaskBase:
|
|
|
90
96
|
if key in self.options:
|
|
91
97
|
return self.options[key]
|
|
92
98
|
|
|
93
|
-
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}'")
|
|
94
100
|
|
|
95
101
|
|
|
96
102
|
class TaskBatchFetch(TaskBase):
|
|
97
103
|
"""Plugin downloader base class."""
|
|
98
104
|
|
|
99
105
|
def __init__(self, data: dict[str, Any], options: dict[str, Any]) -> None:
|
|
106
|
+
"""Initialize the batchfetch task plugin."""
|
|
100
107
|
new_options: dict[str, Any] = {"exec_before": [],
|
|
101
108
|
"exec_after": [],
|
|
102
109
|
"ignore_untracked": []}
|
|
@@ -132,6 +139,7 @@ class TaskBatchFetch(TaskBase):
|
|
|
132
139
|
}
|
|
133
140
|
|
|
134
141
|
def _initialize_data(self) -> None:
|
|
142
|
+
"""Safely initialize data for the batchfetch task."""
|
|
135
143
|
try:
|
|
136
144
|
super()._initialize_data()
|
|
137
145
|
except DataAlreadyInitialized:
|
|
@@ -141,6 +149,7 @@ class TaskBatchFetch(TaskBase):
|
|
|
141
149
|
self._values_initialized = True
|
|
142
150
|
|
|
143
151
|
def _local_task_exec(self, *args: Any, **kwargs: Any) -> None:
|
|
152
|
+
"""Execute a local task command and capture its output."""
|
|
144
153
|
stdout = run_indent_str(env=self.env, spaces=self.indent,
|
|
145
154
|
*args, **kwargs)
|
|
146
155
|
if not stdout.endswith("\n"):
|
|
@@ -148,6 +157,7 @@ class TaskBatchFetch(TaskBase):
|
|
|
148
157
|
self.add_output(stdout)
|
|
149
158
|
|
|
150
159
|
def _exec_before(self, cwd: os.PathLike = Path(".")) -> None:
|
|
160
|
+
"""Execute pre-task commands defined in options and task items."""
|
|
151
161
|
self._initialize_data()
|
|
152
162
|
if self["delete"]:
|
|
153
163
|
return
|
|
@@ -165,6 +175,7 @@ class TaskBatchFetch(TaskBase):
|
|
|
165
175
|
self._local_task_exec(cmd, cwd=str(cwd))
|
|
166
176
|
|
|
167
177
|
def _exec_after(self, cwd: os.PathLike = Path(".")) -> None:
|
|
178
|
+
"""Execute post-task commands if the task was changed successfully."""
|
|
168
179
|
self._initialize_data()
|
|
169
180
|
if self["delete"] or self.is_error() or not self.is_changed():
|
|
170
181
|
return
|
|
@@ -182,32 +193,39 @@ class TaskBatchFetch(TaskBase):
|
|
|
182
193
|
self._local_task_exec(cmd, cwd=str(cwd))
|
|
183
194
|
|
|
184
195
|
def is_changed(self) -> bool:
|
|
196
|
+
"""Return True if the task execution resulted in changes."""
|
|
185
197
|
self._initialize_data()
|
|
186
198
|
return bool(self.values["result"]["changed"])
|
|
187
199
|
|
|
188
200
|
def set_changed(self, changed: bool) -> None:
|
|
201
|
+
"""Set the changed status of the task."""
|
|
189
202
|
self._initialize_data()
|
|
190
203
|
self.values["result"]["changed"] = changed
|
|
191
204
|
|
|
192
205
|
def get_changed(self) -> bool:
|
|
206
|
+
"""Return the changed status of the task without initializing data."""
|
|
193
207
|
try:
|
|
194
208
|
return bool(self.values["result"]["changed"])
|
|
195
209
|
except KeyError:
|
|
196
210
|
return False
|
|
197
211
|
|
|
198
212
|
def is_error(self) -> bool:
|
|
213
|
+
"""Return True if the task encountered an error."""
|
|
199
214
|
self._initialize_data()
|
|
200
215
|
return bool(self.values["result"]["error"])
|
|
201
216
|
|
|
202
217
|
def set_error(self, error: bool) -> None:
|
|
218
|
+
"""Set the error status of the task."""
|
|
203
219
|
self._initialize_data()
|
|
204
220
|
self.values["result"]["error"] = error
|
|
205
221
|
|
|
206
222
|
def set_output(self, output: str) -> None:
|
|
223
|
+
"""Set the output string of the task."""
|
|
207
224
|
self._initialize_data()
|
|
208
225
|
self.values["result"]["output"] = output
|
|
209
226
|
|
|
210
227
|
def add_output(self, output: str) -> None:
|
|
228
|
+
"""Append a string to the output of the task."""
|
|
211
229
|
self._initialize_data()
|
|
212
230
|
self.values["result"]["output"] += output
|
|
213
231
|
|
|
@@ -217,5 +235,6 @@ class TaskBatchFetch(TaskBase):
|
|
|
217
235
|
return str(self.values["result"]["output"])
|
|
218
236
|
|
|
219
237
|
def update(self) -> dict[str, Any]:
|
|
238
|
+
"""Update the task data and return the current values."""
|
|
220
239
|
self._initialize_data()
|
|
221
240
|
return self.values
|
|
@@ -47,11 +47,12 @@ class BatchFetchCli:
|
|
|
47
47
|
# pylint: disable=too-many-positional-arguments
|
|
48
48
|
def __init__(self, max_workers: int, verbose: bool, check_untracked: bool,
|
|
49
49
|
targets: list[str], cwd: os.PathLike) -> None:
|
|
50
|
+
"""Initialize the batchfetch command-line interface."""
|
|
50
51
|
os.chdir(cwd)
|
|
51
52
|
self._cwd = cwd
|
|
52
53
|
self._logger = logging.getLogger(self.__class__.__name__)
|
|
53
54
|
self.targets = {Path(item).absolute() for item in targets}
|
|
54
|
-
self.cfg: dict = {}
|
|
55
|
+
self.cfg: dict[str, Any] = {}
|
|
55
56
|
self.check_untracked = check_untracked
|
|
56
57
|
self.tracked_paths: dict[Path, set[str]] = {}
|
|
57
58
|
self.ignore_untracked: set[Path] = set()
|
|
@@ -66,6 +67,7 @@ class BatchFetchCli:
|
|
|
66
67
|
self._plugin_add("git", BatchFetchGit)
|
|
67
68
|
|
|
68
69
|
def _plugin_add(self, keyword: str, batchfetch_class: Any) -> None:
|
|
70
|
+
"""Register a plugin class and its schema."""
|
|
69
71
|
batchfetch_instance = batchfetch_class(data={},
|
|
70
72
|
options={})
|
|
71
73
|
batchfetch_instance.validate_schema()
|
|
@@ -81,6 +83,7 @@ class BatchFetchCli:
|
|
|
81
83
|
}
|
|
82
84
|
|
|
83
85
|
def _plugin_get(self, raw_data: dict[str, Any]) -> str:
|
|
86
|
+
"""Retrieve the correct plugin keyword based on raw data."""
|
|
84
87
|
keyword_found = None
|
|
85
88
|
for keyword in self.batchfetch_classes:
|
|
86
89
|
if keyword in raw_data:
|
|
@@ -99,10 +102,12 @@ class BatchFetchCli:
|
|
|
99
102
|
return keyword_found
|
|
100
103
|
|
|
101
104
|
def load(self, path: Path) -> None:
|
|
105
|
+
"""Load and parse the batchfetch YAML configuration file."""
|
|
102
106
|
# pylint: disable=too-many-try-statements
|
|
103
107
|
try:
|
|
104
108
|
with open(path, "r", encoding="utf-8") as fhandler:
|
|
105
|
-
yaml_dict =
|
|
109
|
+
yaml_dict: Any = \
|
|
110
|
+
yaml.load(fhandler, Loader=yaml.FullLoader)
|
|
106
111
|
if not isinstance(yaml_dict, dict):
|
|
107
112
|
print(f"Error: Invalid format: {yaml_dict}.",
|
|
108
113
|
file=sys.stderr)
|
|
@@ -142,6 +147,7 @@ class BatchFetchCli:
|
|
|
142
147
|
raise BatchFetchError(str(err)) from err
|
|
143
148
|
|
|
144
149
|
def _loads(self, data: dict[str, Any]) -> None:
|
|
150
|
+
"""Validate and load the configuration data dictionary."""
|
|
145
151
|
schema = Schema(self.cfg_schema)
|
|
146
152
|
try:
|
|
147
153
|
schema.validate(data)
|
|
@@ -160,6 +166,7 @@ class BatchFetchCli:
|
|
|
160
166
|
self._loads_tasks(data)
|
|
161
167
|
|
|
162
168
|
def _loads_tasks(self, data: dict[str, Any]) -> None:
|
|
169
|
+
"""Load and validate tasks from the configuration data."""
|
|
163
170
|
if "tasks" not in data:
|
|
164
171
|
return
|
|
165
172
|
|
|
@@ -191,6 +198,7 @@ class BatchFetchCli:
|
|
|
191
198
|
# pylint: disable=too-many-branches
|
|
192
199
|
# pylint: disable=too-many-statements
|
|
193
200
|
def run_tasks(self) -> bool:
|
|
201
|
+
"""Execute all configured tasks in parallel using a thread pool."""
|
|
194
202
|
failed = []
|
|
195
203
|
error = False
|
|
196
204
|
threads = []
|
|
@@ -279,7 +287,7 @@ class BatchFetchCli:
|
|
|
279
287
|
|
|
280
288
|
return False
|
|
281
289
|
else:
|
|
282
|
-
if self.check_untracked:
|
|
290
|
+
if self.check_untracked and not self.targets:
|
|
283
291
|
self._find_untracked_paths()
|
|
284
292
|
|
|
285
293
|
if num_success == 0:
|
|
@@ -358,7 +366,7 @@ def parse_args() -> argparse.Namespace:
|
|
|
358
366
|
"targets", metavar="target", type=str, nargs="*",
|
|
359
367
|
help=("This is a target path that batchfetch is supposed to handle. "
|
|
360
368
|
"When no target is specified, execute the tasks of all target "
|
|
361
|
-
"paths defined in the batchfetch.
|
|
369
|
+
"paths defined in the batchfetch.yaml list of tasks."),
|
|
362
370
|
)
|
|
363
371
|
|
|
364
372
|
parser.add_argument(
|
|
@@ -421,7 +429,7 @@ def parse_args() -> argparse.Namespace:
|
|
|
421
429
|
|
|
422
430
|
|
|
423
431
|
def command_line_interface() -> None:
|
|
424
|
-
"""
|
|
432
|
+
"""Entry point for the command line interface."""
|
|
425
433
|
# pylint: disable=too-many-try-statements
|
|
426
434
|
try:
|
|
427
435
|
errno = 0
|
|
@@ -459,7 +467,7 @@ def command_line_interface() -> None:
|
|
|
459
467
|
except KeyboardInterrupt:
|
|
460
468
|
print("Interrupted.", file=sys.stderr)
|
|
461
469
|
errno = 1
|
|
462
|
-
except (yaml.
|
|
470
|
+
except (yaml.error.MarkedYAMLError, BatchFetchError) as err:
|
|
463
471
|
print(f"Error: {err}.", file=sys.stderr)
|
|
464
472
|
errno = 1
|
|
465
473
|
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
from __future__ import annotations
|
|
22
22
|
|
|
23
|
+
import logging
|
|
23
24
|
import os
|
|
24
25
|
import shutil
|
|
25
26
|
import subprocess
|
|
@@ -45,9 +46,10 @@ class BatchFetchGit(TaskBatchFetch):
|
|
|
45
46
|
"""Clone or update a Git repository."""
|
|
46
47
|
|
|
47
48
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
49
|
+
"""Initialize the Git batchfetch task."""
|
|
48
50
|
self._git_fetch_origin_done = False
|
|
49
51
|
|
|
50
|
-
self.branch_commit_ref = None
|
|
52
|
+
self.branch_commit_ref: str | None = None
|
|
51
53
|
self.is_branch = False
|
|
52
54
|
|
|
53
55
|
super().__init__(*args, **kwargs)
|
|
@@ -90,10 +92,11 @@ class BatchFetchGit(TaskBatchFetch):
|
|
|
90
92
|
})
|
|
91
93
|
|
|
92
94
|
self.git_local_dir = Path(self["path"])
|
|
93
|
-
self.current_branch = None
|
|
94
|
-
self.current_commit_ref = None
|
|
95
|
+
self.current_branch: str | None = None
|
|
96
|
+
self.current_commit_ref: str | None = None
|
|
95
97
|
|
|
96
98
|
def _initialize_data(self) -> None:
|
|
99
|
+
"""Initialize and parse the repository path data."""
|
|
97
100
|
super()._initialize_data()
|
|
98
101
|
|
|
99
102
|
self.values[self.main_key] = \
|
|
@@ -149,8 +152,15 @@ class BatchFetchGit(TaskBatchFetch):
|
|
|
149
152
|
self._exec_before(cwd=self.git_local_dir)
|
|
150
153
|
|
|
151
154
|
if not self["revision"]:
|
|
152
|
-
|
|
153
|
-
|
|
155
|
+
# Attempt to read the default branch from local origin/HEAD
|
|
156
|
+
sym_ref = self._run_get_firstline(
|
|
157
|
+
["git", "symbolic-ref", "--short",
|
|
158
|
+
"refs/remotes/origin/HEAD"]
|
|
159
|
+
).strip()
|
|
160
|
+
if sym_ref.startswith("origin/"):
|
|
161
|
+
self.values["revision"] = \
|
|
162
|
+
sym_ref.split("origin/", 1)[1]
|
|
163
|
+
|
|
154
164
|
if not self["revision"]:
|
|
155
165
|
raise BatchFetchError(
|
|
156
166
|
"Unable to determine the default origin branch"
|
|
@@ -187,6 +197,7 @@ class BatchFetchGit(TaskBatchFetch):
|
|
|
187
197
|
return self.values
|
|
188
198
|
|
|
189
199
|
def _run_get_firstline(self, *args: Any, **kwargs: Any) -> str:
|
|
200
|
+
"""Run a command and return the first line of standard output."""
|
|
190
201
|
stdout, _ = self._run(*args, **kwargs)
|
|
191
202
|
try:
|
|
192
203
|
return stdout[0]
|
|
@@ -194,8 +205,8 @@ class BatchFetchGit(TaskBatchFetch):
|
|
|
194
205
|
return ""
|
|
195
206
|
|
|
196
207
|
def _run(self, cmd: Union[list[str], str],
|
|
197
|
-
cwd: Union[None, os.PathLike] = None,
|
|
198
|
-
env: Union[None, dict] = None,
|
|
208
|
+
cwd: Union[None, os.PathLike, str] = None,
|
|
209
|
+
env: Union[None, dict[str, str]] = None,
|
|
199
210
|
**kwargs: Any) -> tuple[list[str], list[str]]:
|
|
200
211
|
"""Execute a command and return stdout and stderr.
|
|
201
212
|
|
|
@@ -214,6 +225,33 @@ class BatchFetchGit(TaskBatchFetch):
|
|
|
214
225
|
env = self.env
|
|
215
226
|
return run_simple(cmd=cmd, env=env, cwd=cwd, **kwargs)
|
|
216
227
|
|
|
228
|
+
def _get_upstream_branch(self) -> str:
|
|
229
|
+
"""Return the upstream tracking branch for the current branch."""
|
|
230
|
+
if not self.current_branch:
|
|
231
|
+
raise BatchFetchError(
|
|
232
|
+
"Not currently on any branch. Cannot determine upstream."
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
stdout, _ = self._run(
|
|
237
|
+
["git", "rev-parse", "--abbrev-ref",
|
|
238
|
+
f"{self.current_branch}@{{upstream}}"]
|
|
239
|
+
)
|
|
240
|
+
# TODO: Don't repeat strip 2 times
|
|
241
|
+
if stdout and stdout[0].strip():
|
|
242
|
+
return stdout[0].strip()
|
|
243
|
+
except subprocess.CalledProcessError as err:
|
|
244
|
+
raise BatchFetchError(
|
|
245
|
+
"No upstream tracking branch found "
|
|
246
|
+
f"for '{self.current_branch}'. "
|
|
247
|
+
"Please configure an upstream branch before updating."
|
|
248
|
+
) from err
|
|
249
|
+
|
|
250
|
+
raise BatchFetchError(
|
|
251
|
+
"Failed to parse upstream tracking "
|
|
252
|
+
f"branch for '{self.current_branch}'."
|
|
253
|
+
)
|
|
254
|
+
|
|
217
255
|
def _git_ref(self, cwd: Union[None, Path] = None) -> str:
|
|
218
256
|
"""Get the commit revision of HEAD.
|
|
219
257
|
|
|
@@ -228,6 +266,7 @@ class BatchFetchGit(TaskBatchFetch):
|
|
|
228
266
|
return output
|
|
229
267
|
|
|
230
268
|
def _update_current_branch_name(self) -> None:
|
|
269
|
+
"""Update instance variables with the current branch and commit."""
|
|
231
270
|
try:
|
|
232
271
|
# This returns the branch name
|
|
233
272
|
stdout, _ = self._run(["git", "symbolic-ref", "--short", "HEAD"])
|
|
@@ -236,6 +275,7 @@ class BatchFetchGit(TaskBatchFetch):
|
|
|
236
275
|
except (IndexError, subprocess.CalledProcessError):
|
|
237
276
|
# Not a symbolic ref
|
|
238
277
|
self.current_branch = None
|
|
278
|
+
self.is_branch = False
|
|
239
279
|
|
|
240
280
|
if self.current_branch:
|
|
241
281
|
try:
|
|
@@ -250,6 +290,7 @@ class BatchFetchGit(TaskBatchFetch):
|
|
|
250
290
|
pass
|
|
251
291
|
|
|
252
292
|
def _repo_delete(self) -> None:
|
|
293
|
+
"""Delete the local Git repository directory safely."""
|
|
253
294
|
if not self.git_local_dir.exists():
|
|
254
295
|
self.add_output(self.indent_spaces + "[INFO] Already deleted\n")
|
|
255
296
|
elif not self.git_local_dir.joinpath(".git").is_dir():
|
|
@@ -266,6 +307,7 @@ class BatchFetchGit(TaskBatchFetch):
|
|
|
266
307
|
self.set_changed(True)
|
|
267
308
|
|
|
268
309
|
def _repo_clone(self) -> None:
|
|
310
|
+
"""Execute git clone for the task repository."""
|
|
269
311
|
git_clone_args = self["git_clone_args"]
|
|
270
312
|
# git_clone_args += ["--recurse-submodules"]
|
|
271
313
|
|
|
@@ -275,14 +317,16 @@ class BatchFetchGit(TaskBatchFetch):
|
|
|
275
317
|
self.set_changed(True)
|
|
276
318
|
|
|
277
319
|
def _repo_fetch(self) -> bool:
|
|
320
|
+
"""Fetch the remote repository and determine if an update is needed."""
|
|
278
321
|
# Merge
|
|
279
322
|
do_git_fetch = self["git_pull"]
|
|
280
|
-
do_git_fetch = False
|
|
281
323
|
|
|
282
324
|
try:
|
|
283
325
|
# Check if the revision such as
|
|
284
326
|
_, _ = self._run(["git", "cat-file", "-e", self["revision"]])
|
|
285
|
-
except subprocess.CalledProcessError:
|
|
327
|
+
except subprocess.CalledProcessError as err:
|
|
328
|
+
logging.debug("Ignoring subprocess error checking cat-file: %s",
|
|
329
|
+
err)
|
|
286
330
|
do_git_fetch = True
|
|
287
331
|
self.add_output(
|
|
288
332
|
self.indent_spaces
|
|
@@ -303,8 +347,11 @@ class BatchFetchGit(TaskBatchFetch):
|
|
|
303
347
|
+ f"{self['revision']} is a branch, not a tag"
|
|
304
348
|
+ "\n")
|
|
305
349
|
do_git_fetch = True
|
|
306
|
-
except subprocess.CalledProcessError:
|
|
307
|
-
|
|
350
|
+
except subprocess.CalledProcessError as err:
|
|
351
|
+
logging.debug(
|
|
352
|
+
"Ignoring subprocess error checking show-ref: %s",
|
|
353
|
+
err,
|
|
354
|
+
)
|
|
308
355
|
|
|
309
356
|
if not do_git_fetch:
|
|
310
357
|
self.add_output(self.indent_spaces + "[INFO] git fetch ignored\n")
|
|
@@ -323,6 +370,7 @@ class BatchFetchGit(TaskBatchFetch):
|
|
|
323
370
|
return False
|
|
324
371
|
|
|
325
372
|
def _git_apply_update_strategy(self) -> bool:
|
|
373
|
+
"""Merge, rebase, or reset local branches according to the strategy."""
|
|
326
374
|
git_updated = False
|
|
327
375
|
|
|
328
376
|
# Merge, Rebase, or Reset
|
|
@@ -343,34 +391,44 @@ class BatchFetchGit(TaskBatchFetch):
|
|
|
343
391
|
"prevent data loss."
|
|
344
392
|
)
|
|
345
393
|
|
|
394
|
+
# Retrieve upstream branch or raise exception if not found
|
|
395
|
+
upstream_branch = self._get_upstream_branch()
|
|
396
|
+
|
|
346
397
|
self.add_output(
|
|
347
398
|
self.indent_spaces
|
|
348
399
|
+ f"[INFO] Applying update strategy: {strategy} from "
|
|
349
|
-
f"
|
|
400
|
+
f"{upstream_branch}\n"
|
|
350
401
|
)
|
|
351
402
|
|
|
352
403
|
if strategy == "rebase":
|
|
353
|
-
self._run(["git", "rebase"] +
|
|
354
|
-
[f"origin/{self.current_branch}"])
|
|
404
|
+
self._run(["git", "rebase"] + [upstream_branch])
|
|
355
405
|
elif strategy == "reset":
|
|
356
|
-
self._run(["git", "reset", "--hard",
|
|
357
|
-
f"origin/{self.current_branch}"])
|
|
406
|
+
self._run(["git", "reset", "--hard", upstream_branch])
|
|
358
407
|
else:
|
|
359
408
|
self._run(["git", "merge"] + self["git_merge_args"] +
|
|
360
|
-
[
|
|
409
|
+
[upstream_branch])
|
|
361
410
|
|
|
362
411
|
git_ref_after_update = self._git_ref(cwd=self.git_local_dir)
|
|
363
412
|
if commit_ref_head != git_ref_after_update:
|
|
364
413
|
git_updated = True
|
|
365
414
|
self.set_changed(True)
|
|
366
|
-
self._run([
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
415
|
+
stdout, _ = self._run([
|
|
416
|
+
"git", "log",
|
|
417
|
+
"--pretty=format:%h %ad %s [%cn]",
|
|
418
|
+
"--decorate", "--date=short",
|
|
419
|
+
f"{commit_ref_head}..{git_ref_after_update}"
|
|
420
|
+
])
|
|
421
|
+
if stdout:
|
|
422
|
+
self.add_output(self.indent_spaces +
|
|
423
|
+
"[INFO] Updated commits:\n")
|
|
424
|
+
for line in stdout:
|
|
425
|
+
self.add_output(self.indent_spaces +
|
|
426
|
+
" " + line + "\n")
|
|
370
427
|
|
|
371
428
|
return git_updated
|
|
372
429
|
|
|
373
430
|
def _git_get_remote_url(self, remote_name: str = "origin") -> str:
|
|
431
|
+
"""Retrieve the configured url for the given remote name."""
|
|
374
432
|
origin_url = ""
|
|
375
433
|
try:
|
|
376
434
|
stdout, _ = self._run(["git", "config",
|
|
@@ -384,6 +442,7 @@ class BatchFetchGit(TaskBatchFetch):
|
|
|
384
442
|
|
|
385
443
|
def _git_set_remote_url(self, url: str,
|
|
386
444
|
remote_name: str = "origin") -> str:
|
|
445
|
+
"""Reconfigure or add a Git remote with a new target url."""
|
|
387
446
|
origin_url = ""
|
|
388
447
|
try:
|
|
389
448
|
self._run(["git", "remote", "remove", remote_name])
|
|
@@ -418,6 +477,7 @@ class BatchFetchGit(TaskBatchFetch):
|
|
|
418
477
|
return False
|
|
419
478
|
|
|
420
479
|
def _git_rev_parse_verify(self, revision: str) -> list[str]:
|
|
480
|
+
"""Verify and return the parsed reference for a git revision."""
|
|
421
481
|
stdout: list[str] = []
|
|
422
482
|
error = False
|
|
423
483
|
try:
|
|
@@ -434,6 +494,7 @@ class BatchFetchGit(TaskBatchFetch):
|
|
|
434
494
|
return stdout
|
|
435
495
|
|
|
436
496
|
def _repo_fix_branch(self) -> bool:
|
|
497
|
+
"""Check out the correct revision branch if current branch differs."""
|
|
437
498
|
git_ref_after_merge = self._git_ref(cwd=self.git_local_dir)
|
|
438
499
|
branch_changed = False
|
|
439
500
|
if self["revision"]:
|
|
@@ -441,24 +502,37 @@ class BatchFetchGit(TaskBatchFetch):
|
|
|
441
502
|
# returns a different commit revision
|
|
442
503
|
git_tags, _ = self._run(["git", "tag", "--points-at", "HEAD"])
|
|
443
504
|
|
|
505
|
+
is_branch = False
|
|
444
506
|
# Also check the commit revision in case
|
|
445
507
|
# branch is a commit revision instead of a tag
|
|
446
508
|
try:
|
|
447
|
-
# Check if the branch exists
|
|
509
|
+
# Check if the branch exists remotely
|
|
448
510
|
git_ref_branch = self._git_rev_parse_verify("origin/"
|
|
449
511
|
+ self["revision"]
|
|
450
512
|
+ "^{commit}")[0]
|
|
513
|
+
is_branch = True
|
|
451
514
|
except GitRevisionDoesNotExist:
|
|
452
515
|
# Check if the commit ref exists
|
|
453
516
|
try:
|
|
454
517
|
git_ref_branch = \
|
|
455
518
|
self._git_rev_parse_verify(self["revision"])[0]
|
|
519
|
+
if self._git_is_local_branch(self["revision"]):
|
|
520
|
+
is_branch = True
|
|
456
521
|
except GitRevisionDoesNotExist as err:
|
|
457
522
|
raise BatchFetchError(f"The branch '{self['revision']}' "
|
|
458
523
|
"does not exist.") from err
|
|
459
524
|
|
|
460
|
-
|
|
461
|
-
|
|
525
|
+
needs_checkout = False
|
|
526
|
+
if self.current_branch != self["revision"]:
|
|
527
|
+
if is_branch:
|
|
528
|
+
needs_checkout = True
|
|
529
|
+
elif self.current_branch is not None:
|
|
530
|
+
needs_checkout = True
|
|
531
|
+
elif git_ref_after_merge != git_ref_branch and \
|
|
532
|
+
self["revision"] not in git_tags:
|
|
533
|
+
needs_checkout = True
|
|
534
|
+
|
|
535
|
+
if needs_checkout:
|
|
462
536
|
# Update the branch
|
|
463
537
|
self._run(["git", "checkout"] + [self["revision"]])
|
|
464
538
|
self.add_output(self.indent_spaces
|
|
@@ -473,6 +547,7 @@ class BatchFetchGit(TaskBatchFetch):
|
|
|
473
547
|
return branch_changed
|
|
474
548
|
|
|
475
549
|
def _git_fetch_origin(self) -> None:
|
|
550
|
+
"""Execute `git fetch origin` if it has not already been done."""
|
|
476
551
|
# Fetch
|
|
477
552
|
if not self._git_fetch_origin_done:
|
|
478
553
|
cmd = ["git", "fetch", "origin"]
|
|
@@ -480,6 +555,7 @@ class BatchFetchGit(TaskBatchFetch):
|
|
|
480
555
|
self._git_fetch_origin_done = True
|
|
481
556
|
|
|
482
557
|
def _repo_fix_remote_origin(self) -> None:
|
|
558
|
+
"""Ensure remote origin URL matches configuration and upstream sets."""
|
|
483
559
|
correct_origin_url = self[self.main_key]
|
|
484
560
|
update_remote_origin = False
|
|
485
561
|
|
|
@@ -491,6 +567,15 @@ class BatchFetchGit(TaskBatchFetch):
|
|
|
491
567
|
update_remote_origin = True
|
|
492
568
|
|
|
493
569
|
if update_remote_origin:
|
|
570
|
+
# We must fetch the upstream branch BEFORE updating the remote,
|
|
571
|
+
# because removing the remote drops the tracking configuration.
|
|
572
|
+
upstream_branch = None
|
|
573
|
+
if self.current_branch:
|
|
574
|
+
try:
|
|
575
|
+
upstream_branch = self._get_upstream_branch()
|
|
576
|
+
except BatchFetchError:
|
|
577
|
+
pass
|
|
578
|
+
|
|
494
579
|
# Update remote
|
|
495
580
|
try:
|
|
496
581
|
self._git_set_remote_url(correct_origin_url)
|
|
@@ -501,17 +586,20 @@ class BatchFetchGit(TaskBatchFetch):
|
|
|
501
586
|
# Get the current branch
|
|
502
587
|
if self.current_branch:
|
|
503
588
|
try:
|
|
589
|
+
target_upstream = (upstream_branch if upstream_branch
|
|
590
|
+
else f"origin/{self.current_branch}")
|
|
591
|
+
|
|
504
592
|
self.add_output(
|
|
505
593
|
self.indent_spaces
|
|
506
594
|
+ "[INFO] Git fetch origin reason: "
|
|
507
595
|
f"we need to set the upstream "
|
|
508
|
-
f"origin to {
|
|
596
|
+
f"origin to {target_upstream}"
|
|
509
597
|
"\n"
|
|
510
598
|
)
|
|
511
599
|
self._git_fetch_origin()
|
|
512
600
|
|
|
513
601
|
cmd = ["git", "branch",
|
|
514
|
-
f"--set-upstream-to=
|
|
602
|
+
f"--set-upstream-to={target_upstream}"]
|
|
515
603
|
_, _ = self._run(cmd)
|
|
516
604
|
# TODO: handle errors
|
|
517
605
|
except subprocess.CalledProcessError as err:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: batchfetch
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.9
|
|
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.
|
|
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.
|
|
97
|
+
of all target paths defined in the batchfetch.yaml list
|
|
98
98
|
of tasks.
|
|
99
99
|
|
|
100
100
|
options:
|
|
@@ -119,7 +119,7 @@ options:
|
|
|
119
119
|
## Features
|
|
120
120
|
|
|
121
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.
|
|
122
|
-
- Parallel Operations:
|
|
122
|
+
- Parallel Operations: Uses threads to simultaneously Git clone or pull multiple repositories, reducing wait times.
|
|
123
123
|
- User-Friendly Interface: Provides simple and straightforward command-line options that make it easy to get started and effectively manage your repositories.
|
|
124
124
|
- Custom Configuration: Allows the use of a YAML configuration file to specify and manage the repositories you interact with, enabling repeatable setups and consistent environments.
|
|
125
125
|
- Detect files that should not be present in directories managed by batchfetch, known as untracked files.
|
|
@@ -173,20 +173,6 @@ tasks:
|
|
|
173
173
|
revision: b9c6d9b6134b4981760893254f804a371ffbc899
|
|
174
174
|
```
|
|
175
175
|
|
|
176
|
-
### How to configure additional Git remotes?
|
|
177
|
-
|
|
178
|
-
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.
|
|
179
|
-
|
|
180
|
-
```yaml
|
|
181
|
-
---
|
|
182
|
-
tasks:
|
|
183
|
-
- git: https://github.com/jamescherti/easysession.el
|
|
184
|
-
path: emacs/easysession
|
|
185
|
-
remote:
|
|
186
|
-
upstream: https://github.com/name/repo
|
|
187
|
-
upstream2: https://github.com/name/repo2
|
|
188
|
-
```
|
|
189
|
-
|
|
190
176
|
### How to customize Git clone, merge, and update strategies?
|
|
191
177
|
|
|
192
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.
|
|
@@ -219,6 +205,20 @@ tasks:
|
|
|
219
205
|
|
|
220
206
|
```
|
|
221
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
|
+
|
|
222
222
|
### How to execute a command before and after a task?
|
|
223
223
|
|
|
224
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.
|
|
@@ -236,9 +236,9 @@ tasks:
|
|
|
236
236
|
|
|
237
237
|
### How to make batchfetch handle only one path?
|
|
238
238
|
|
|
239
|
-
To configure `batchfetch` to handle a specific path, you can define your tasks in a `batchfetch.
|
|
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.
|
|
240
240
|
|
|
241
|
-
#### Example `batchfetch.
|
|
241
|
+
#### Example `batchfetch.yaml` file:
|
|
242
242
|
|
|
243
243
|
In the following example, the `easysession` task clones two Git repositories:
|
|
244
244
|
|
|
@@ -258,7 +258,7 @@ To make `batchfetch` clone only `easysession`, pass its path as an argument:
|
|
|
258
258
|
batchfetch easysession
|
|
259
259
|
```
|
|
260
260
|
|
|
261
|
-
This will execute only the task corresponding to the `easysession` path, skipping all others in the `batchfetch.
|
|
261
|
+
This will execute only the task corresponding to the `easysession` path, skipping all others in the `batchfetch.yaml` file.
|
|
262
262
|
|
|
263
263
|
### How can I configure batchfetch to load a file other than batchfetch.yaml?
|
|
264
264
|
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
.gitignore
|
|
1
2
|
LICENSE
|
|
2
3
|
README.md
|
|
4
|
+
run_tests.sh
|
|
3
5
|
setup.py
|
|
6
|
+
.github/FUNDING.yml
|
|
4
7
|
batchfetch/__init__.py
|
|
5
8
|
batchfetch/batchfetch_base.py
|
|
6
9
|
batchfetch/batchfetch_cli.py
|
|
@@ -12,4 +15,6 @@ batchfetch.egg-info/dependency_links.txt
|
|
|
12
15
|
batchfetch.egg-info/entry_points.txt
|
|
13
16
|
batchfetch.egg-info/requires.txt
|
|
14
17
|
batchfetch.egg-info/top_level.txt
|
|
15
|
-
tests/test_helpers.py
|
|
18
|
+
tests/test_helpers.py
|
|
19
|
+
tests/data/test-md5sum.txt
|
|
20
|
+
tests/data/test-run_simple.sh
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Copyright (C) 2024-2026 James Cherti
|
|
4
|
+
# URL: https://github.com/jamescherti/batchfetch
|
|
5
|
+
#
|
|
6
|
+
# This program is free software: you can redistribute it and/or modify it under
|
|
7
|
+
# the terms of the GNU General Public License as published by the Free Software
|
|
8
|
+
# Foundation, either version 3 of the License, or (at your option) any later
|
|
9
|
+
# version.
|
|
10
|
+
#
|
|
11
|
+
# This program is distributed in the hope that it will be useful, but WITHOUT
|
|
12
|
+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
13
|
+
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
14
|
+
#
|
|
15
|
+
# You should have received a copy of the GNU General Public License along with
|
|
16
|
+
# this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
#
|
|
18
|
+
cd "$(dirname "${BASH_SOURCE[0]}")"
|
|
19
|
+
export PYTHONPATH="$(pwd)"
|
|
20
|
+
exec pytest -v -v --cov=batchfetch --cov=tests \
|
|
21
|
+
--cov-report=term \
|
|
22
|
+
--cov-report=html:htmlcov \
|
|
23
|
+
tests/test_*py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|