cloninator 0.1.0__py3-none-any.whl

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.
cloninator/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
cloninator/__main__.py ADDED
@@ -0,0 +1,24 @@
1
+ from argparse import ArgumentParser
2
+
3
+ from cloninator.clone import clone
4
+ from cloninator.generate import generate
5
+
6
+
7
+ def get_parser() -> ArgumentParser:
8
+ parser = ArgumentParser()
9
+ usage = parser.add_mutually_exclusive_group(required=True)
10
+ usage.add_argument("--clone", action="store_true")
11
+ usage.add_argument("--generate", action="store_true")
12
+
13
+ return parser
14
+
15
+
16
+ def main() -> None:
17
+ args = get_parser().parse_args()
18
+ if args.clone:
19
+ clone()
20
+ elif args.generate:
21
+ generate()
22
+ else:
23
+ msg = "Invalid usage"
24
+ raise ValueError(msg)
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
cloninator/clone.py ADDED
@@ -0,0 +1,42 @@
1
+ from subprocess import run
2
+
3
+ from cloninator.utils import Repo, get_config
4
+
5
+
6
+ def add_repo(repo: Repo) -> None:
7
+ path = repo.path
8
+ path.mkdir(parents=True, exist_ok=True)
9
+ origin = repo.remotes[0]
10
+ print(f"🟢 Cloning {origin.url} at {path}...")
11
+ run(
12
+ ["git", "clone", origin.url, path, "--origin", origin.name], # noqa: S603, S607
13
+ check=True,
14
+ )
15
+ for remote in repo.remotes[1:]:
16
+ print(f"🟢 Adding remote {remote.name} at {remote.url} for {path}...")
17
+ run(
18
+ [ # noqa: S603, S607
19
+ "git",
20
+ "-C",
21
+ path,
22
+ "remote",
23
+ "add",
24
+ remote.name,
25
+ remote.url,
26
+ ],
27
+ check=True,
28
+ )
29
+ post_checkout = repo.post_checkout
30
+ print(f"🟢 Running post-checkout commands {list(post_checkout)} for {path}...")
31
+ for command in post_checkout:
32
+ run(command, cwd=path, shell=True, check=True) # noqa: S602
33
+
34
+
35
+ def clone() -> None:
36
+ config = get_config()
37
+ for repo in config.repos:
38
+ path = repo.path
39
+ if path.exists() and any(path.iterdir()):
40
+ print(f"🔵 Repo {path} already exists, skipping...")
41
+ else:
42
+ add_repo(repo)
cloninator/generate.py ADDED
@@ -0,0 +1,31 @@
1
+ from pathlib import Path
2
+ from typing import Any
3
+
4
+ from yaml import safe_dump
5
+
6
+ from cloninator.utils import get_config, get_repos
7
+
8
+
9
+ def generate() -> None:
10
+ config = get_config(soft_info=False)
11
+ root = config.root
12
+ repos = get_repos(root=root)
13
+ missing_repos = repos.repos - config.repos
14
+ repos_dict: dict[str, Any] = {}
15
+ for missing_repo in missing_repos:
16
+ current_dict = repos_dict
17
+ path = missing_repo.path.relative_to(root)
18
+ for directory in reversed(path.parents[:-1]):
19
+ current_dict.setdefault(directory.name, {})
20
+ current_dict = current_dict[directory.name]
21
+ current_dict[path.name] = {
22
+ "/remotes": [
23
+ {
24
+ "name": remote.name,
25
+ "url": remote.url,
26
+ }
27
+ for remote in missing_repo.remotes
28
+ ]
29
+ }
30
+ with Path("repos.yaml").open("w") as file:
31
+ safe_dump(repos_dict, file)
cloninator/utils.py ADDED
@@ -0,0 +1,125 @@
1
+ from collections.abc import Iterator
2
+ from dataclasses import dataclass
3
+ from pathlib import Path
4
+ from subprocess import run
5
+ from typing import Any
6
+
7
+ from dj_settings import ConfigParser
8
+
9
+ CONF_DIR = Path.home().joinpath(".config", "cloninator")
10
+ CONF = CONF_DIR.joinpath("config.yaml")
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class Remote:
15
+ name: str
16
+ url: str
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class Repo:
21
+ path: Path
22
+ remotes: tuple[Remote, ...]
23
+ post_checkout: tuple[str, ...]
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class Config:
28
+ root: Path
29
+ repos: frozenset[Repo]
30
+
31
+
32
+ def validate_repo(path: Path, data: dict[str, Any]) -> str:
33
+ if not all(key.startswith("/") for key in data):
34
+ return f"❌ Repo info for {path} has keys that don't start with /, skipping..."
35
+ if "/remotes" not in data:
36
+ return f"❌ Repo info for {path} is missing remotes info, skipping..."
37
+ if not data["/remotes"]:
38
+ return f"❌ Repo info for {path} has no remotes, skipping..."
39
+ for remote in data["/remotes"]:
40
+ if "name" not in remote:
41
+ return f"❌ Repo info for {path} has a remote without name, skipping..."
42
+ if "url" not in remote:
43
+ return f"❌ Repo info for {path} has a remote without a url, skipping..."
44
+ return ""
45
+
46
+
47
+ def _get_config(
48
+ path: Path, data: dict[str, Any]
49
+ ) -> Iterator[tuple[Path, dict[str, Any]]]:
50
+ for key, value in data.items():
51
+ if isinstance(value, dict):
52
+ if any(key.startswith("/") for key in value):
53
+ yield path.joinpath(key), value
54
+ else:
55
+ yield from _get_config(path.joinpath(key), value)
56
+
57
+
58
+ def get_config(*, soft_info: bool = True) -> Config:
59
+ data = ConfigParser([CONF]).data
60
+ try:
61
+ root = Path(data["/root"])
62
+ except KeyError as exc:
63
+ msg = "Root key is missing from the config file."
64
+ raise ValueError(msg) from exc
65
+
66
+ repos = set()
67
+ for path, path_data in _get_config(root, data):
68
+ if error_message := validate_repo(path, path_data):
69
+ print(error_message)
70
+ else:
71
+ if soft_info:
72
+ post_checkout = tuple(path_data.get("/post_checkout", []))
73
+ else:
74
+ post_checkout = ()
75
+ repos.add(
76
+ Repo(
77
+ path=path,
78
+ remotes=tuple(Remote(**remote) for remote in path_data["/remotes"]),
79
+ post_checkout=post_checkout,
80
+ )
81
+ )
82
+
83
+ return Config(root=root, repos=frozenset(repos))
84
+
85
+
86
+ def get_repos(root: Path | None = None) -> Config:
87
+ data = ConfigParser([CONF]).data
88
+ if root is None:
89
+ try:
90
+ root = Path(data["/root"])
91
+ except KeyError as exc:
92
+ msg = "Root key is missing from the config file."
93
+ raise ValueError(msg) from exc
94
+
95
+ repos = set()
96
+ for git_dir in root.rglob(".git/"):
97
+ path = git_dir.parent
98
+ response = run(
99
+ [ # noqa: S603, S607
100
+ "git",
101
+ "-C",
102
+ path,
103
+ "config",
104
+ "--get-regex",
105
+ r"remote\..*\.url",
106
+ ],
107
+ capture_output=True,
108
+ check=False,
109
+ )
110
+ if response.stderr:
111
+ error = response.stderr.decode()
112
+ raise ValueError(error)
113
+ output = response.stdout.decode()
114
+ if not output:
115
+ print(f"🔵 Repo {path} is local, skipping...")
116
+ continue
117
+ remotes_info = output.splitlines()
118
+ remotes = []
119
+ for remote in remotes_info:
120
+ full_name, url = remote.split()
121
+ _, name, _ = full_name.split(".")
122
+ remotes.append(Remote(name=name, url=url))
123
+ repos.add(Repo(path=path, remotes=tuple(remotes), post_checkout=()))
124
+
125
+ return Config(root=root, repos=frozenset(repos))
@@ -0,0 +1,57 @@
1
+ Metadata-Version: 2.1
2
+ Name: cloninator
3
+ Version: 0.1.0
4
+ Summary: A cli tool to clone your repos
5
+ Home-page: https://cloninator.readthedocs.io/en/stable/
6
+ License: LGPL-3.0+
7
+ Keywords: git
8
+ Author: Stephanos Kuma
9
+ Author-email: stephanos@kuma.ai
10
+ Requires-Python: >=3.11,<4.0
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Requires-Dist: PyYAML (>=6.0,<7.0)
18
+ Requires-Dist: dj_settings (>=5.0,<6.0)
19
+ Project-URL: Documentation, https://cloninator.readthedocs.io/en/stable/
20
+ Project-URL: Repository, https://github.com/spapanik/cloninator
21
+ Description-Content-Type: text/markdown
22
+
23
+ # cloninator: A cli tool to clone your repos
24
+
25
+ [![tests][test_badge]][test_url]
26
+ [![license][licence_badge]][licence_url]
27
+ [![pypi][pypi_badge]][pypi_url]
28
+ [![downloads][pepy_badge]][pepy_url]
29
+ [![code style: black][black_badge]][black_url]
30
+ [![build automation: yam][yam_badge]][yam_url]
31
+ [![Lint: ruff][ruff_badge]][ruff_url]
32
+
33
+ Long project description and tldr goes here
34
+
35
+ ## Links
36
+
37
+ - [Documentation]
38
+ - [Changelog]
39
+
40
+ [test_badge]: https://github.com/spapanik/cloninator/actions/workflows/tests.yml/badge.svg
41
+ [test_url]: https://github.com/spapanik/cloninator/actions/workflows/tests.yml
42
+ [licence_badge]: https://img.shields.io/badge/License-LGPL_v3-blue.svg
43
+ [licence_url]: https://github.com/spapanik/cloninator/blob/main/docs/LICENSE.md
44
+ [pypi_badge]: https://img.shields.io/pypi/v/cloninator
45
+ [pypi_url]: https://pypi.org/project/cloninator
46
+ [pepy_badge]: https://pepy.tech/badge/cloninator
47
+ [pepy_url]: https://pepy.tech/project/cloninator
48
+ [black_badge]: https://img.shields.io/badge/code%20style-black-000000.svg
49
+ [black_url]: https://github.com/psf/black
50
+ [yam_badge]: https://img.shields.io/badge/build%20automation-yamk-success
51
+ [yam_url]: https://github.com/spapanik/yamk
52
+ [ruff_badge]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v1.json
53
+ [ruff_url]: https://github.com/charliermarsh/ruff
54
+ [Documentation]: https://cloninator.readthedocs.io/en/stable/
55
+ [Changelog]: https://github.com/spapanik/cloninator/blob/main/docs/CHANGELOG.md
56
+
57
+
@@ -0,0 +1,10 @@
1
+ cloninator/__init__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
2
+ cloninator/__main__.py,sha256=jIQ9Ay0ESp0LJ3WeFllxn0GL8Cht3s-W9427W7naskA,583
3
+ cloninator/__version__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
4
+ cloninator/clone.py,sha256=icvxTiG92VJ550I0zmQNfjqyG5WJNy26bCeqV9GbTEE,1257
5
+ cloninator/generate.py,sha256=XJZY05HtEDYyt3WK4v33uryPb4B4sE-rdogvbmy8GFE,960
6
+ cloninator/utils.py,sha256=Ze_uqC2AL1Re99Q6GoDBN_wgyDUsYSRjsKxog-AV9yk,3831
7
+ cloninator-0.1.0.dist-info/METADATA,sha256=8kqF6d7iTzxmQeIFJ5xMERiXO2sxEersen2e46uZeUs,2345
8
+ cloninator-0.1.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
9
+ cloninator-0.1.0.dist-info/entry_points.txt,sha256=ehyxigwsuZNEb_9s7tIHl69a0bLFQMlFDbvXSC8x0eM,55
10
+ cloninator-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 1.9.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ cloninator=cloninator.__main__:main
3
+