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 +1 -0
- cloninator/__main__.py +24 -0
- cloninator/__version__.py +1 -0
- cloninator/clone.py +42 -0
- cloninator/generate.py +31 -0
- cloninator/utils.py +125 -0
- cloninator-0.1.0.dist-info/METADATA +57 -0
- cloninator-0.1.0.dist-info/RECORD +10 -0
- cloninator-0.1.0.dist-info/WHEEL +4 -0
- cloninator-0.1.0.dist-info/entry_points.txt +3 -0
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,,
|