batchfetch 1.0.4__tar.gz → 1.0.6__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.1
2
2
  Name: batchfetch
3
- Version: 1.0.4
3
+ Version: 1.0.6
4
4
  Summary: Efficiently clone and pull multiple Git repositories.
5
5
  Home-page: https://github.com/jamescherti/batchfetch
6
6
  Author: James Cherti
@@ -17,10 +17,6 @@ Classifier: Topic :: Utilities
17
17
  Requires-Python: >=3.6, <4
18
18
  Description-Content-Type: text/markdown
19
19
  License-File: LICENSE
20
- Requires-Dist: colorama
21
- Requires-Dist: schema
22
- Requires-Dist: setproctitle
23
- Requires-Dist: PyYAML
24
20
 
25
21
  # Batchfetch - Efficiently clone or pull multiple Git repositories in parallel
26
22
 
@@ -61,12 +57,12 @@ tasks:
61
57
  # Clone the tag 1.5 of the consult repository to the './consult'
62
58
  # directory
63
59
  - git: https://github.com/minad/consult
64
- branch: "1.5"
60
+ reference: "1.5"
65
61
 
66
62
  # Clone the s.el repository to the './another-name.el' directory
67
63
  - git: https://github.com/magnars/s.el
68
64
  path: another-name.el
69
- branch: dda84d38fffdaf0c9b12837b504b402af910d01d
65
+ reference: dda84d38fffdaf0c9b12837b504b402af910d01d
70
66
 
71
67
  # Delete './impatient-mode'
72
68
  - git: https://github.com/skeeto/impatient-mode
@@ -84,14 +80,15 @@ usage: batchfetch [--option] [args]
84
80
 
85
81
  Command line interface.
86
82
 
83
+ positional arguments:
84
+ N Specify the batchfetch YAML file(s) (default: './batchfetch.yaml').
85
+
87
86
  options:
88
87
  -h, --help show this help message and exit
89
- -j JOBS, --jobs JOBS
90
- Run up to N Number of parallel git processes (Default: 5).
88
+ -j JOBS, --jobs JOBS Run up to N Number of parallel processes (Default: 5).
91
89
  -v, --verbose Enable verbose mode.
92
- -f BATCHFETCH_FILE, --batchfetch-file BATCHFETCH_FILE
93
- Specify the batchfetch YAML file (default: './batchfetch.yaml').
94
90
  ```
91
+
95
92
  ## License
96
93
 
97
94
  Copyright (c) [James Cherti](https://www.jamescherti.com)
@@ -37,12 +37,12 @@ tasks:
37
37
  # Clone the tag 1.5 of the consult repository to the './consult'
38
38
  # directory
39
39
  - git: https://github.com/minad/consult
40
- branch: "1.5"
40
+ reference: "1.5"
41
41
 
42
42
  # Clone the s.el repository to the './another-name.el' directory
43
43
  - git: https://github.com/magnars/s.el
44
44
  path: another-name.el
45
- branch: dda84d38fffdaf0c9b12837b504b402af910d01d
45
+ reference: dda84d38fffdaf0c9b12837b504b402af910d01d
46
46
 
47
47
  # Delete './impatient-mode'
48
48
  - git: https://github.com/skeeto/impatient-mode
@@ -60,14 +60,15 @@ usage: batchfetch [--option] [args]
60
60
 
61
61
  Command line interface.
62
62
 
63
+ positional arguments:
64
+ N Specify the batchfetch YAML file(s) (default: './batchfetch.yaml').
65
+
63
66
  options:
64
67
  -h, --help show this help message and exit
65
- -j JOBS, --jobs JOBS
66
- Run up to N Number of parallel git processes (Default: 5).
68
+ -j JOBS, --jobs JOBS Run up to N Number of parallel processes (Default: 5).
67
69
  -v, --verbose Enable verbose mode.
68
- -f BATCHFETCH_FILE, --batchfetch-file BATCHFETCH_FILE
69
- Specify the batchfetch YAML file (default: './batchfetch.yaml').
70
70
  ```
71
+
71
72
  ## License
72
73
 
73
74
  Copyright (c) [James Cherti](https://www.jamescherti.com)
@@ -16,8 +16,3 @@
16
16
  # this program. If not, see <https://www.gnu.org/licenses/>.
17
17
  #
18
18
  """The entry-point of batchfetch."""
19
-
20
- from .batchfetch_cli import command_line_interface
21
-
22
- if __name__ == "__main__":
23
- command_line_interface()
@@ -10,7 +10,8 @@
10
10
  #
11
11
  # This program is distributed in the hope that it will be useful, but WITHOUT
12
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.
13
+ # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14
+ # details.
14
15
  #
15
16
  # You should have received a copy of the GNU General Public License along with
16
17
  # this program. If not, see <https://www.gnu.org/licenses/>.
@@ -31,19 +32,13 @@ class BatchFetchError(Exception):
31
32
  """Exception raised by Downloader()."""
32
33
 
33
34
 
34
- class GitBranchDoesNotExist(Exception):
35
- """Exception raised by Name()."""
36
-
37
-
38
35
  class BatchFetchBase:
39
36
  """Plugin downloader base class."""
40
37
 
41
- env = os.environ.copy()
42
- env["GIT_TERMINAL_PROMPT"] = "0"
43
-
44
- indent = 4
45
-
46
38
  def __init__(self, data: Dict[str, Any], options: Dict[str, Any]):
39
+ self.indent = 4
40
+ self.env = os.environ.copy()
41
+
47
42
  # Default
48
43
  self.global_options_schema: Dict[Any, Any] = {
49
44
  Optional("pre_exec"): Or([str], str),
@@ -10,7 +10,8 @@
10
10
  #
11
11
  # This program is distributed in the hope that it will be useful, but WITHOUT
12
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.
13
+ # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14
+ # details.
14
15
  #
15
16
  # You should have received a copy of the GNU General Public License along with
16
17
  # this program. If not, see <https://www.gnu.org/licenses/>.
@@ -24,7 +25,7 @@ import subprocess
24
25
  import sys
25
26
  from concurrent.futures import ThreadPoolExecutor, as_completed
26
27
  from pathlib import Path
27
- from typing import Set
28
+ from typing import Any, Dict, Set
28
29
 
29
30
  import colorama
30
31
  import yaml # type: ignore
@@ -32,31 +33,13 @@ from colorama import Fore
32
33
  from schema import Optional, Or, Schema, SchemaError
33
34
  from setproctitle import setproctitle
34
35
 
35
- from .batchfetch_base import BatchFetchError
36
+ from .batchfetch_base import BatchFetchBase, BatchFetchError
36
37
  from .batchfetch_git import BatchFetchGit
37
38
 
38
39
 
39
40
  class BatchFetchCli:
40
41
  """Command-line-interface that downloads."""
41
42
 
42
- main_key = "git"
43
-
44
- # TODO: Make this automatic
45
- empty_downloader_git = BatchFetchGit(
46
- data={main_key: "http://www.domain.com"},
47
- options={},
48
- )
49
- empty_downloader_git.validate_schema() # Gen structure
50
-
51
- cfg_schema = {
52
- Optional("options"):
53
- empty_downloader_git.global_options_schema, # type: ignore
54
- # TODO: Make this added automatically
55
- Optional("tasks"): [
56
- Or(str, empty_downloader_git.item_schema)
57
- ]
58
- }
59
-
60
43
  def __init__(self, max_workers: int, verbose: bool = False):
61
44
  self.cfg: dict = {}
62
45
  self.folder = Path(".")
@@ -66,16 +49,55 @@ class BatchFetchCli:
66
49
  self._logger = logging.getLogger(self.__class__.__name__)
67
50
  self.dirs_relative_to_batchfetch: Set[str] = set()
68
51
 
52
+ # Plugin
53
+ self.batchfetch_schemas: Dict[Any, Any] = {}
54
+ self.batchfetch_classes: Dict[str, BatchFetchBase] = {}
55
+ self.cfg_schema: Dict[Any, Any] = {}
56
+ self._plugin_add("git", BatchFetchGit)
57
+
58
+ def _plugin_add(self, keyword: str, batchfetch_class: Any):
59
+ batchfetch_instance = batchfetch_class(data={},
60
+ options={})
61
+ batchfetch_instance.validate_schema()
62
+
63
+ self.batchfetch_schemas[keyword] = batchfetch_instance.item_schema
64
+ self.batchfetch_classes[keyword] = batchfetch_class
65
+ self.cfg_schema = {
66
+ Optional("options"):
67
+ batchfetch_instance.global_options_schema, # type: ignore
68
+ Optional("tasks"): [
69
+ Or(*list(self.batchfetch_schemas.values()))
70
+ ]
71
+ }
72
+
73
+ def _plugin_get(self, raw_data: dict) -> str:
74
+ keyword_found = None
75
+ for keyword in self.batchfetch_classes:
76
+ if keyword in raw_data:
77
+ if keyword_found is not None:
78
+ err_str = (f"The keywords {keyword} and {keyword_found} "
79
+ f"are mutually exclusive. Error in: {raw_data}")
80
+ raise BatchFetchError(err_str)
81
+ keyword_found = keyword
82
+
83
+ if not keyword_found:
84
+ err_str = "None of the keywords " + \
85
+ ", ".join(self.batchfetch_classes.keys()) + \
86
+ f" have been found in: {raw_data}"
87
+ raise BatchFetchError(err_str)
88
+
89
+ return keyword_found
90
+
69
91
  def load(self, path: Path):
70
92
  try:
71
93
  with open(path, "r", encoding="utf-8") as fhandler:
72
94
  yaml_dict = yaml.load(fhandler, Loader=yaml.FullLoader)
73
- self.loads(dict(yaml_dict))
95
+ self._loads(dict(yaml_dict))
74
96
  except OSError as err:
75
97
  raise BatchFetchError(str(err)) from err
76
98
 
77
- def loads(self, data: dict):
78
- schema = Schema(BatchFetchCli.cfg_schema)
99
+ def _loads(self, data: dict):
100
+ schema = Schema(self.cfg_schema)
79
101
  try:
80
102
  schema.validate(data)
81
103
  except SchemaError as err:
@@ -84,47 +106,45 @@ class BatchFetchCli:
84
106
 
85
107
  self.cfg = {
86
108
  "options": {"clone_args": []},
87
- # TODO: Make this added automatically
88
109
  "tasks": [],
89
110
  }
90
111
 
91
112
  if "options" in data:
92
113
  self.cfg["options"].update(data["options"])
93
114
 
94
- # TODO: Make this automatic
95
- self._loads_git(data)
115
+ self._loads_tasks(data)
96
116
 
97
- def _loads_git(self, data: dict):
117
+ def _loads_tasks(self, data: dict):
98
118
  if "tasks" not in data:
99
119
  return
100
120
 
101
121
  dict_local_dir = {} # type: ignore
102
- for git_repo_raw in data["tasks"]:
103
- if isinstance(git_repo_raw, str):
104
- git_repo_raw = {BatchFetchCli.main_key: git_repo_raw}
122
+ for task in data["tasks"]:
123
+ keyword = self._plugin_get(task)
124
+ batchfetch_class = self.batchfetch_classes[keyword]
105
125
 
106
126
  try:
107
- plugin_downloader_git = BatchFetchGit(
108
- data=git_repo_raw,
127
+ batchfetch_instance = batchfetch_class( # type: ignore
128
+ data=task,
109
129
  options=self.cfg["options"],
110
130
  )
111
- self.cfg["tasks"].append(plugin_downloader_git)
131
+ self.cfg["tasks"].append(batchfetch_instance)
112
132
  except SchemaError as err:
113
133
  print(f"Schema error: {err}.", file=sys.stderr)
114
134
  sys.exit(1)
115
135
 
116
- if plugin_downloader_git["path"] in dict_local_dir:
117
- err_str = ("more than one repository will be cloned " +
118
- "to the directory '" +
119
- str(plugin_downloader_git["path"]) + "' (" +
120
- str(git_repo_raw[BatchFetchCli.main_key]) + " and " +
121
- str(dict_local_dir[plugin_downloader_git["path"]]) +
136
+ dest_path = Path(batchfetch_instance["path"]).resolve()
137
+ if str(dest_path) in dict_local_dir:
138
+ err_str = ("More than one task have the " +
139
+ f"destination path '{dest_path}' (" +
140
+ str(task[keyword]) + " and " +
141
+ str(dict_local_dir[(str(dest_path))]) +
122
142
  ")")
123
143
  raise BatchFetchError(err_str)
124
- dict_local_dir[plugin_downloader_git["path"]] = \
125
- plugin_downloader_git[BatchFetchCli.main_key]
126
144
 
127
- def download(self) -> bool:
145
+ dict_local_dir[str(dest_path)] = batchfetch_instance[keyword]
146
+
147
+ def run_tasks(self) -> bool:
128
148
  failed = []
129
149
  error = False
130
150
  threads = []
@@ -136,13 +156,12 @@ class BatchFetchCli:
136
156
  try:
137
157
  self.dirs_relative_to_batchfetch = set()
138
158
 
139
- # TODO: Make adding to all downloads automatic
140
- all_downloads = self.cfg["tasks"]
141
- for download_item in all_downloads:
142
- self.dirs_relative_to_batchfetch.add(str(download_item["path"]))
143
- if not download_item["delete"]:
144
- self.managed_filenames.add(download_item["path"])
145
- threads.append(executor_update.submit(download_item.update))
159
+ all_tasks = self.cfg["tasks"]
160
+ for task in all_tasks:
161
+ self.dirs_relative_to_batchfetch.add(str(task["path"]))
162
+ if not task["delete"]:
163
+ self.managed_filenames.add(task["path"])
164
+ threads.append(executor_update.submit(task.update))
146
165
 
147
166
  for future in as_completed(threads):
148
167
  data = future.result()
@@ -174,29 +193,20 @@ class BatchFetchCli:
174
193
  if error:
175
194
  if failed:
176
195
  print("Failed:")
177
- for git_update_result in failed:
178
- print(" - url:", git_update_result[BatchFetchCli.main_key])
179
- print(" dir:", git_update_result["path"])
196
+ for failed_result in failed:
197
+ print(" -", failed_result["path"])
180
198
  else:
181
199
  print("Failed.")
182
200
 
183
201
  return False
184
202
  else:
185
203
  if num_success == 0:
186
- print("Already up to date.")
204
+ print("Nothing to do.")
187
205
  elif not self.verbose:
188
206
  print("Success.")
189
207
 
190
208
  return True
191
209
 
192
- # def check_managed_directories(self):
193
- # # managed_downloads = {Path(item).resolve()
194
- # # for item in self.managed_filenames}
195
- # # managed_directories = {item.parent
196
- # # for item in managed_downloads}
197
- # # TODO Implement checker
198
- # pass
199
-
200
210
 
201
211
  def parse_args():
202
212
  """Parse the command line arguments."""
@@ -204,9 +214,13 @@ def parse_args():
204
214
  usage = "%(prog)s [--option] [args]"
205
215
  parser = argparse.ArgumentParser(description=desc, usage=usage)
206
216
 
217
+ parser.add_argument("batchfetch_files", metavar="N", type=str, nargs="*",
218
+ help=("Specify the batchfetch YAML file(s) "
219
+ "(default: './batchfetch.yaml')."))
220
+
207
221
  parser.add_argument(
208
222
  "-j", "--jobs", default="5", required=False,
209
- help="Run up to N Number of parallel git processes (Default: 5).",
223
+ help="Run up to N Number of parallel processes (Default: 5).",
210
224
  )
211
225
 
212
226
  parser.add_argument(
@@ -214,43 +228,26 @@ def parse_args():
214
228
  help="Enable verbose mode.",
215
229
  )
216
230
 
217
- parser.add_argument(
218
- "-f",
219
- "--batchfetch-file",
220
- default="./batchfetch.yaml",
221
- required=False,
222
- help="Specify the batchfetch YAML file (default: './batchfetch.yaml').",
223
- )
224
-
225
231
  args = parser.parse_args()
226
232
 
227
- if not Path(args.batchfetch_file).is_file():
228
- print(f"Error: File not found: {args.batchfetch_file}",
229
- file=sys.stderr)
230
- sys.exit(1)
233
+ for batchfetch_file in args.batchfetch_files:
234
+ if not Path(batchfetch_file).is_file():
235
+ print(f"Error: File not found: {batchfetch_file}",
236
+ file=sys.stderr)
237
+ sys.exit(1)
231
238
 
232
239
  return args
233
240
 
234
241
 
235
- def command_line_interface():
236
- """Command line interface."""
242
+ def run_batchfetch_procedure(batchfetch_file: Path, args) -> int:
237
243
  errno = 0
238
- logging.basicConfig(level=logging.INFO, stream=sys.stdout,
239
- format="%(asctime)s %(name)s: %(message)s")
240
-
241
- colorama.init()
242
- setproctitle(subprocess.list2cmdline([Path(sys.argv[0]).name] +
243
- sys.argv[1:]))
244
-
245
- args = parse_args()
246
- downloader_cli = BatchFetchCli(verbose=args.verbose,
244
+ batchfetch_cli = BatchFetchCli(verbose=args.verbose,
247
245
  max_workers=int(args.jobs))
248
-
249
- downloader_cli.load(args.batchfetch_file)
250
- os.chdir(os.path.dirname(args.batchfetch_file))
246
+ batchfetch_cli.load(batchfetch_file)
247
+ os.chdir(batchfetch_file.parent)
251
248
 
252
249
  try:
253
- if not downloader_cli.download():
250
+ if not batchfetch_cli.run_tasks():
254
251
  errno = 1
255
252
  except KeyboardInterrupt:
256
253
  print("Interrupted.", file=sys.stderr)
@@ -258,7 +255,32 @@ def command_line_interface():
258
255
  except BatchFetchError as err:
259
256
  print(f"Error: {err}.", file=sys.stderr)
260
257
  errno = 1
261
- # else:
262
- # downloader_cli.check_managed_directories()
263
258
 
264
- sys.exit(errno)
259
+ return errno
260
+
261
+
262
+ def command_line_interface():
263
+ """Command line interface."""
264
+ try:
265
+ errno = 0
266
+ logging.basicConfig(level=logging.INFO, stream=sys.stdout,
267
+ format="%(asctime)s %(name)s: %(message)s")
268
+
269
+ colorama.init()
270
+ setproctitle(subprocess.list2cmdline([Path(sys.argv[0]).name] +
271
+ sys.argv[1:]))
272
+
273
+ args = parse_args()
274
+ done = []
275
+ for batchfetch_file in args.batchfetch_files:
276
+ batchfetch_file = Path(batchfetch_file)
277
+ batchfetch_file_resolved = batchfetch_file.resolve()
278
+ if batchfetch_file_resolved in done:
279
+ continue
280
+
281
+ done.append(batchfetch_file_resolved)
282
+ errno |= run_batchfetch_procedure(batchfetch_file, args)
283
+
284
+ sys.exit(errno)
285
+ except BrokenPipeError:
286
+ pass
@@ -10,7 +10,8 @@
10
10
  #
11
11
  # This program is distributed in the hope that it will be useful, but WITHOUT
12
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.
13
+ # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14
+ # details.
14
15
  #
15
16
  # You should have received a copy of the GNU General Public License along with
16
17
  # this program. If not, see <https://www.gnu.org/licenses/>.
@@ -27,25 +28,28 @@ from typing import List, Union
27
28
 
28
29
  from schema import Optional
29
30
 
30
- from .batchfetch_base import (BatchFetchBase, BatchFetchError,
31
- GitBranchDoesNotExist)
31
+ from .batchfetch_base import BatchFetchBase, BatchFetchError
32
32
  from .helpers import run_simple
33
33
 
34
34
 
35
+ class GitReferenceDoesNotExist(Exception):
36
+ """Exception raised by Name()."""
37
+
38
+
35
39
  class BatchFetchGit(BatchFetchBase):
36
40
  """Clone or update a Git repository."""
37
41
 
38
- indent_spaces = " " * BatchFetchBase.indent
39
- main_key = "git"
40
-
41
42
  def __init__(self, *args, **kwargs):
42
43
  super().__init__(*args, **kwargs)
44
+ self.env["GIT_TERMINAL_PROMPT"] = "0"
45
+ self.indent_spaces = " " * self.indent
46
+ self.main_key = "git"
43
47
 
44
48
  # Schema
45
49
  self.item_schema.update({
46
50
  # Local options
47
- BatchFetchGit.main_key: str,
48
- Optional("branch"): str,
51
+ self.main_key: str,
52
+ Optional("reference"): str,
49
53
 
50
54
  # Same as global options
51
55
  Optional("clone_args"): [str],
@@ -63,8 +67,8 @@ class BatchFetchGit(BatchFetchBase):
63
67
  "git_pull": True})
64
68
 
65
69
  self.item_default_values.update({
66
- BatchFetchGit.main_key: "",
67
- "branch": "",
70
+ self.main_key: "",
71
+ "reference": "",
68
72
  "delete": False,
69
73
  })
70
74
 
@@ -75,16 +79,19 @@ class BatchFetchGit(BatchFetchBase):
75
79
  def _initialize_data(self):
76
80
  super()._initialize_data()
77
81
 
78
- self.values[BatchFetchGit.main_key] = \
79
- self.values[BatchFetchGit.main_key].rstrip("/")
82
+ self.values[self.main_key] = \
83
+ self.values[self.main_key].rstrip("/")
80
84
 
81
85
  if "path" not in self.values:
82
86
  self.values["path"] = \
83
87
  posixpath.basename(
84
- self.values[BatchFetchGit.main_key]) # type: ignore
88
+ self.values[self.main_key]) # type: ignore
85
89
 
86
90
  def _git_ref(self, cwd: Union[None, Path] = None) -> str:
87
- """Get the commit reference of HEAD."""
91
+ """Get the commit reference of HEAD.
92
+
93
+ The command will fail if the branch is detached.
94
+ """
88
95
  cmd = "git show-ref --head --verify HEAD"
89
96
  try:
90
97
  stdout, _ = run_simple(cmd, cwd=cwd, env=self.env)
@@ -112,8 +119,8 @@ class BatchFetchGit(BatchFetchBase):
112
119
 
113
120
  self.add_output(
114
121
  f"[GIT {update_type}] {self[self.main_key]}" +
115
- (f" (branch: {self['branch']})"
116
- if self["branch"] else "") + "\n"
122
+ (f" (Reference: {self['reference']})"
123
+ if self["reference"] else "") + "\n"
117
124
  )
118
125
 
119
126
  try:
@@ -152,7 +159,7 @@ class BatchFetchGit(BatchFetchBase):
152
159
  textwrap.indent(err.stderr, " " * self.indent))
153
160
 
154
161
  if not self.is_error() and not self.is_changed():
155
- self.add_output(self.indent_spaces + "# Already up to date\n")
162
+ self.add_output(self.indent_spaces + "[INFO] Nothing to do.\n")
156
163
 
157
164
  return self.values
158
165
 
@@ -169,7 +176,7 @@ class BatchFetchGit(BatchFetchBase):
169
176
 
170
177
  def _repo_delete(self):
171
178
  if not self.git_local_dir.exists():
172
- self.add_output(self.indent_spaces + "# Already deleted\n")
179
+ self.add_output(self.indent_spaces + "[INFO] Already deleted\n")
173
180
  elif not self.git_local_dir.joinpath(".git").is_dir():
174
181
  self.add_output(
175
182
  self.indent_spaces +
@@ -180,7 +187,7 @@ class BatchFetchGit(BatchFetchBase):
180
187
  elif self.git_local_dir.is_dir():
181
188
  shutil.rmtree(str(self.git_local_dir))
182
189
  self.add_output(self.indent_spaces +
183
- f"# Deleted: '{self.git_local_dir}'")
190
+ f"[INFO] Deleted: '{self.git_local_dir}'")
184
191
  self.set_changed(True)
185
192
 
186
193
  def _repo_clone(self):
@@ -188,7 +195,7 @@ class BatchFetchGit(BatchFetchBase):
188
195
  git_clone_args += ["--recurse-submodules"]
189
196
 
190
197
  cmd = ["git", "clone"] + git_clone_args + \
191
- [self[BatchFetchGit.main_key], str(self.git_local_dir)]
198
+ [self[self.main_key], str(self.git_local_dir)]
192
199
  self._run(cmd, env=self.env)
193
200
  self.set_changed(True)
194
201
 
@@ -204,28 +211,37 @@ class BatchFetchGit(BatchFetchBase):
204
211
  ignore_git_pull = False
205
212
  if not self["git_pull"]:
206
213
  ignore_git_pull = True
207
- elif self["branch"]:
214
+
215
+ if self["reference"]:
216
+ ignore_git_pull = False
208
217
  # Check if the new branch exists
209
218
  try:
210
- self._git_tags(self["branch"])
211
- except GitBranchDoesNotExist:
219
+ self._git_tags(self["reference"])
220
+ except GitReferenceDoesNotExist:
212
221
  pass
213
222
  else:
214
- # The branch exists
215
-
216
- # Ignore Git pull when the git reference is the same
217
- commit_ref = self._git_ref(cwd=self.git_local_dir)
218
- if (self.current_branch and
219
- (commit_ref == self["branch"] or
220
- self.current_branch == self["branch"])):
221
- ignore_git_pull = True
222
- # Ignore Git pull if it is not a local branch
223
- elif not self._git_is_local_branch(self["branch"]):
223
+ # The branch exists:
224
+ # 1. Ignore Git pull when the git reference is the same
225
+ # as the "branch:" key
226
+ try:
227
+ commit_ref = self._git_ref(cwd=self.git_local_dir)
228
+ except subprocess.CalledProcessError:
229
+ # Ignore git pull because the head is detached
230
+ pass
231
+ else:
232
+ if (self.current_branch and
233
+ (commit_ref == self["reference"] or
234
+ self.current_branch == self["reference"])):
235
+ ignore_git_pull = True
236
+
237
+ # 2. Ignore Git pull if it is not a local branch
238
+ if (not ignore_git_pull and
239
+ not self._git_is_local_branch(self["reference"])):
224
240
  ignore_git_pull = True
225
241
 
226
242
  if ignore_git_pull:
227
243
  self.add_output(self.indent_spaces +
228
- "# git pull ignored\n")
244
+ "[INFO] git pull ignored\n")
229
245
  else:
230
246
  cmd = ["git", "fetch", "origin"]
231
247
  self._run(cmd, cwd=str(self.git_local_dir), env=self.env)
@@ -272,15 +288,15 @@ class BatchFetchGit(BatchFetchBase):
272
288
  env=self.env,
273
289
  cwd=self.git_local_dir)
274
290
  except subprocess.CalledProcessError as err:
275
- raise GitBranchDoesNotExist(
276
- f"The branch '{branch}' does not exist.") from err
291
+ raise GitReferenceDoesNotExist(
292
+ f"The reference '{branch}' does not exist.") from err
277
293
 
278
294
  return stdout
279
295
 
280
296
  def _repo_fix_branch(self) -> bool:
281
297
  git_ref_after_merge = self._git_ref(cwd=self.git_local_dir)
282
298
  branch_changed = False
283
- if self["branch"]:
299
+ if self["reference"]:
284
300
  # We also need tags because sometimes, a branch
285
301
  # returns a different commit reference
286
302
  git_tags, _ = run_simple(
@@ -292,18 +308,19 @@ class BatchFetchGit(BatchFetchBase):
292
308
  # Also check the commit reference in case
293
309
  # branch is a commit reference instead of a tag
294
310
  try:
295
- git_ref_branch = self._git_tags(self["branch"])[0]
296
- except GitBranchDoesNotExist as err:
311
+ git_ref_branch = self._git_tags(self["reference"])[0]
312
+ except GitReferenceDoesNotExist as err:
297
313
  raise BatchFetchError(f"The branch '{self['branch']}' "
298
314
  "does not exist.") from err
299
315
 
300
316
  if git_ref_after_merge != git_ref_branch and \
301
- self["branch"] not in git_tags:
317
+ self["reference"] not in git_tags:
302
318
  # Update the branch
303
- self._run(["git", "checkout"] + [self["branch"]],
319
+ self._run(["git", "checkout"] + [self["reference"]],
304
320
  cwd=str(self.git_local_dir), env=self.env)
305
- self.add_output(self.indent_spaces + "# Branch changed to " +
306
- self["branch"] + "\n")
321
+ self.add_output(self.indent_spaces +
322
+ "[INFO] Branch changed to " +
323
+ self["reference"] + "\n")
307
324
  self.set_changed(True)
308
325
  branch_changed = True
309
326
 
@@ -339,7 +356,7 @@ class BatchFetchGit(BatchFetchBase):
339
356
  except IndexError:
340
357
  origin_url = ""
341
358
 
342
- if origin_url != self[BatchFetchGit.main_key]:
359
+ if origin_url != self[self.main_key]:
343
360
  self.set_error(True)
344
361
  self.add_output(
345
362
  self.indent_spaces +
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env python
1
2
  #
2
3
  # Copyright (c) James Cherti
3
4
  # URL: https://github.com/jamescherti/batchfetch
@@ -9,7 +10,8 @@
9
10
  #
10
11
  # This program is distributed in the hope that it will be useful, but WITHOUT
11
12
  # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12
- # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
13
+ # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14
+ # details.
13
15
  #
14
16
  # You should have received a copy of the GNU General Public License along with
15
17
  # this program. If not, see <https://www.gnu.org/licenses/>.
@@ -28,7 +30,7 @@ def md5sum(filename: os.PathLike):
28
30
  Calculate and return the MD5 checksum of a file.
29
31
 
30
32
  Args:
31
- filename: The path to the file for which the MD5 checksum is calculated.
33
+ filename: Path to the file for which the MD5 checksum is calculated.
32
34
 
33
35
  Returns:
34
36
  str: The MD5 checksum of the file.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: batchfetch
3
- Version: 1.0.4
3
+ Version: 1.0.6
4
4
  Summary: Efficiently clone and pull multiple Git repositories.
5
5
  Home-page: https://github.com/jamescherti/batchfetch
6
6
  Author: James Cherti
@@ -17,10 +17,6 @@ Classifier: Topic :: Utilities
17
17
  Requires-Python: >=3.6, <4
18
18
  Description-Content-Type: text/markdown
19
19
  License-File: LICENSE
20
- Requires-Dist: colorama
21
- Requires-Dist: schema
22
- Requires-Dist: setproctitle
23
- Requires-Dist: PyYAML
24
20
 
25
21
  # Batchfetch - Efficiently clone or pull multiple Git repositories in parallel
26
22
 
@@ -61,12 +57,12 @@ tasks:
61
57
  # Clone the tag 1.5 of the consult repository to the './consult'
62
58
  # directory
63
59
  - git: https://github.com/minad/consult
64
- branch: "1.5"
60
+ reference: "1.5"
65
61
 
66
62
  # Clone the s.el repository to the './another-name.el' directory
67
63
  - git: https://github.com/magnars/s.el
68
64
  path: another-name.el
69
- branch: dda84d38fffdaf0c9b12837b504b402af910d01d
65
+ reference: dda84d38fffdaf0c9b12837b504b402af910d01d
70
66
 
71
67
  # Delete './impatient-mode'
72
68
  - git: https://github.com/skeeto/impatient-mode
@@ -84,14 +80,15 @@ usage: batchfetch [--option] [args]
84
80
 
85
81
  Command line interface.
86
82
 
83
+ positional arguments:
84
+ N Specify the batchfetch YAML file(s) (default: './batchfetch.yaml').
85
+
87
86
  options:
88
87
  -h, --help show this help message and exit
89
- -j JOBS, --jobs JOBS
90
- Run up to N Number of parallel git processes (Default: 5).
88
+ -j JOBS, --jobs JOBS Run up to N Number of parallel processes (Default: 5).
91
89
  -v, --verbose Enable verbose mode.
92
- -f BATCHFETCH_FILE, --batchfetch-file BATCHFETCH_FILE
93
- Specify the batchfetch YAML file (default: './batchfetch.yaml').
94
90
  ```
91
+
95
92
  ## License
96
93
 
97
94
  Copyright (c) [James Cherti](https://www.jamescherti.com)
@@ -1,7 +1,5 @@
1
- .gitignore
2
1
  LICENSE
3
2
  README.md
4
- run_tests.sh
5
3
  setup.py
6
4
  batchfetch/__init__.py
7
5
  batchfetch/batchfetch_base.py
@@ -13,7 +11,4 @@ batchfetch.egg-info/SOURCES.txt
13
11
  batchfetch.egg-info/dependency_links.txt
14
12
  batchfetch.egg-info/entry_points.txt
15
13
  batchfetch.egg-info/requires.txt
16
- batchfetch.egg-info/top_level.txt
17
- tests/test_helpers.py
18
- tests/data/test-md5sum.txt
19
- tests/data/test-run_simple.sh
14
+ batchfetch.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ batchfetch = batchfetch.batchfetch_cli:command_line_interface
@@ -22,7 +22,7 @@ from setuptools import find_packages, setup
22
22
 
23
23
  setup(
24
24
  name="batchfetch",
25
- version="1.0.4",
25
+ version="1.0.6",
26
26
  packages=find_packages(),
27
27
  description="Efficiently clone and pull multiple Git repositories.",
28
28
  license="GPLv3",
@@ -51,7 +51,7 @@ setup(
51
51
  ],
52
52
  entry_points={
53
53
  "console_scripts": [
54
- "batchfetch=batchfetch.__init__:command_line_interface",
54
+ "batchfetch=batchfetch.batchfetch_cli:command_line_interface",
55
55
  ],
56
56
  },
57
57
  )
@@ -1,138 +0,0 @@
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,2 +0,0 @@
1
- [console_scripts]
2
- batchfetch = batchfetch.__init__:command_line_interface
@@ -1,23 +0,0 @@
1
- #!/usr/bin/env bash
2
- #
3
- # Copyright (c) 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
@@ -1,3 +0,0 @@
1
- Test 1.
2
- Test 2.
3
- Test 3.
@@ -1,5 +0,0 @@
1
- #!/usr/bin/env bash
2
-
3
- cd "$(dirname "${BASH_SOURCE[0]}")"
4
- cat ./test-md5sum.txt
5
- cat ./test-md5sum.txt | tail -n 1 >&2
@@ -1,56 +0,0 @@
1
- #!/usr/bin/env python
2
- #
3
- # Copyright (c) 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
- """Unit tests."""
19
-
20
- from pathlib import Path
21
-
22
- from batchfetch import helpers
23
-
24
- DATA_PATH = Path(".").joinpath("tests", "data").absolute()
25
- SCRIPT_RUN_SIMPLE = DATA_PATH / "test-run_simple.sh"
26
- TEST_MD5SUM_FILE = DATA_PATH / "test-md5sum.txt"
27
-
28
-
29
- def test_md5sum():
30
- md5sum = helpers.md5sum(TEST_MD5SUM_FILE)
31
- assert md5sum == "f31e127edc87a6aa2eb01b7d94d2ec58"
32
-
33
-
34
- def test_run_simple():
35
- # Stdout
36
- stdout_lines, stderr_lines = helpers.run_simple(str(SCRIPT_RUN_SIMPLE))
37
- assert stdout_lines == ['Test 1.',
38
- 'Test 2.',
39
- 'Test 3.']
40
- assert stderr_lines == ['Test 3.']
41
-
42
-
43
- def test_indent_raw_output():
44
- list_str = \
45
- helpers.indent_raw_output(["Test 1.", "Test 2."])
46
-
47
- assert list_str == [" Test 1.", " Test 2."]
48
-
49
-
50
- def test_run_indent():
51
- stdout_lines, stderr_lines = helpers.run_indent(str(SCRIPT_RUN_SIMPLE))
52
- assert stdout_lines == [f' [RUN] {str(SCRIPT_RUN_SIMPLE)}',
53
- ' Test 1.',
54
- ' Test 2.',
55
- ' Test 3.']
56
- assert stderr_lines == [' Test 3.']
File without changes
@@ -1,4 +1,4 @@
1
+ PyYAML
1
2
  colorama
2
3
  schema
3
4
  setproctitle
4
- PyYAML
File without changes