repopy 0.3.0__tar.gz → 0.3.1__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.
- {repopy-0.3.0/src/repopy.egg-info → repopy-0.3.1}/PKG-INFO +12 -4
- {repopy-0.3.0 → repopy-0.3.1}/README.md +11 -3
- {repopy-0.3.0 → repopy-0.3.1}/pyproject.toml +10 -1
- repopy-0.3.1/src/repopy/__main__.py +50 -0
- repopy-0.3.1/src/repopy/cli.py +115 -0
- {repopy-0.3.0 → repopy-0.3.1}/src/repopy/dependencies.py +5 -4
- repopy-0.3.1/src/repopy/file_system.py +200 -0
- repopy-0.3.1/src/repopy/git_engine.py +91 -0
- repopy-0.3.1/src/repopy/orchestrators.py +166 -0
- {repopy-0.3.0 → repopy-0.3.1}/src/repopy/os_detector.py +14 -10
- repopy-0.3.1/src/repopy/prompts.py +146 -0
- repopy-0.3.1/src/repopy/py.typed +0 -0
- repopy-0.3.1/src/repopy/templates.py +60 -0
- repopy-0.3.1/src/repopy/validators.py +48 -0
- {repopy-0.3.0 → repopy-0.3.1/src/repopy.egg-info}/PKG-INFO +12 -4
- {repopy-0.3.0 → repopy-0.3.1}/src/repopy.egg-info/SOURCES.txt +5 -0
- {repopy-0.3.0 → repopy-0.3.1}/tests/test_cli.py +26 -8
- repopy-0.3.1/tests/test_dependencies.py +27 -0
- repopy-0.3.1/tests/test_file_system.py +356 -0
- repopy-0.3.1/tests/test_git_engine.py +173 -0
- repopy-0.3.1/tests/test_main.py +156 -0
- repopy-0.3.1/tests/test_orchestrators.py +413 -0
- repopy-0.3.1/tests/test_os_detector.py +43 -0
- repopy-0.3.1/tests/test_prompts.py +207 -0
- repopy-0.3.1/tests/test_templates.py +40 -0
- repopy-0.3.1/tests/test_validators.py +62 -0
- repopy-0.3.0/src/repopy/__main__.py +0 -47
- repopy-0.3.0/src/repopy/cli.py +0 -115
- repopy-0.3.0/src/repopy/file_system.py +0 -186
- repopy-0.3.0/src/repopy/git_engine.py +0 -70
- repopy-0.3.0/src/repopy/orchestrators.py +0 -149
- repopy-0.3.0/src/repopy/prompts.py +0 -131
- repopy-0.3.0/src/repopy/templates.py +0 -47
- repopy-0.3.0/src/repopy/validators.py +0 -53
- repopy-0.3.0/tests/test_file_system.py +0 -27
- repopy-0.3.0/tests/test_git_engine.py +0 -106
- repopy-0.3.0/tests/test_orchestrators.py +0 -179
- repopy-0.3.0/tests/test_prompts.py +0 -106
- repopy-0.3.0/tests/test_validators.py +0 -41
- {repopy-0.3.0 → repopy-0.3.1}/LICENSE +0 -0
- {repopy-0.3.0 → repopy-0.3.1}/setup.cfg +0 -0
- {repopy-0.3.0 → repopy-0.3.1}/src/repopy/__init__.py +0 -0
- {repopy-0.3.0 → repopy-0.3.1}/src/repopy.egg-info/dependency_links.txt +0 -0
- {repopy-0.3.0 → repopy-0.3.1}/src/repopy.egg-info/entry_points.txt +0 -0
- {repopy-0.3.0 → repopy-0.3.1}/src/repopy.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: repopy
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: A modular CLI workspace initializer and cloning automation tool.
|
|
5
5
|
Author-email: Janitha Manatunga <janithamanatunga@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -28,9 +28,13 @@ Dynamic: license-file
|
|
|
28
28
|
A modular, cross-platform Python CLI workspace manager and developer tooling suite designed to automate reproducible local scaffolding, virtual environment creation, and remote repository synchronization.
|
|
29
29
|
|
|
30
30
|

|
|
31
|
+
[](https://opensource.org/license/mit)
|
|
32
|
+
[](https://pypi.org)
|
|
33
|
+
|
|
31
34
|
[](https://github.com/manatunga/repopy/actions/workflows/tests.yml)
|
|
32
|
-
[](https://codecov.io/github/manatunga/repopy)
|
|
36
|
+
[](https://mypy-lang.org)
|
|
37
|
+
[](#)
|
|
34
38
|
|
|
35
39
|
---
|
|
36
40
|
|
|
@@ -167,16 +171,20 @@ repopy/
|
|
|
167
171
|
│ ├── templates.py # Static asset definitions & manifests
|
|
168
172
|
│ └── validators.py # Input sanitation & regex filters
|
|
169
173
|
├── tests/
|
|
174
|
+
| ├── __init__.py
|
|
170
175
|
│ ├── test_cli.py # CLI argument parser & flag exclusivity tests
|
|
176
|
+
│ ├── test_dependencies.py # Binary detection & absence verification tests
|
|
171
177
|
│ ├── test_file_system.py # Workspace scaffolding & requirements parsing tests
|
|
172
178
|
│ ├── test_git_engine.py # Subprocess Git mocking & exit code tests
|
|
179
|
+
│ ├── test_main.py # CLI dispatching & interrupt lifecycle tests
|
|
173
180
|
│ ├── test_orchestrators.py # Pipeline logic, prompts, and rollback tests
|
|
181
|
+
│ ├── test_os_detector.py # Cross-platform path resolution tests
|
|
174
182
|
│ ├── test_prompts.py # Input loop & default manifest tests
|
|
183
|
+
│ ├── test_templates.py # PEP 621 generator & asset template tests
|
|
175
184
|
│ └── test_validators.py # Path and schema validation tests
|
|
176
185
|
├── pyproject.toml # PEP 621 packaging configuration
|
|
177
186
|
├── LICENSE # MIT License
|
|
178
187
|
└── README.md
|
|
179
|
-
|
|
180
188
|
```
|
|
181
189
|
|
|
182
190
|
---
|
|
@@ -3,9 +3,13 @@
|
|
|
3
3
|
A modular, cross-platform Python CLI workspace manager and developer tooling suite designed to automate reproducible local scaffolding, virtual environment creation, and remote repository synchronization.
|
|
4
4
|
|
|
5
5
|

|
|
6
|
+
[](https://opensource.org/license/mit)
|
|
7
|
+
[](https://pypi.org)
|
|
8
|
+
|
|
6
9
|
[](https://github.com/manatunga/repopy/actions/workflows/tests.yml)
|
|
7
|
-
[](https://codecov.io/github/manatunga/repopy)
|
|
11
|
+
[](https://mypy-lang.org)
|
|
12
|
+
[](#)
|
|
9
13
|
|
|
10
14
|
---
|
|
11
15
|
|
|
@@ -142,16 +146,20 @@ repopy/
|
|
|
142
146
|
│ ├── templates.py # Static asset definitions & manifests
|
|
143
147
|
│ └── validators.py # Input sanitation & regex filters
|
|
144
148
|
├── tests/
|
|
149
|
+
| ├── __init__.py
|
|
145
150
|
│ ├── test_cli.py # CLI argument parser & flag exclusivity tests
|
|
151
|
+
│ ├── test_dependencies.py # Binary detection & absence verification tests
|
|
146
152
|
│ ├── test_file_system.py # Workspace scaffolding & requirements parsing tests
|
|
147
153
|
│ ├── test_git_engine.py # Subprocess Git mocking & exit code tests
|
|
154
|
+
│ ├── test_main.py # CLI dispatching & interrupt lifecycle tests
|
|
148
155
|
│ ├── test_orchestrators.py # Pipeline logic, prompts, and rollback tests
|
|
156
|
+
│ ├── test_os_detector.py # Cross-platform path resolution tests
|
|
149
157
|
│ ├── test_prompts.py # Input loop & default manifest tests
|
|
158
|
+
│ ├── test_templates.py # PEP 621 generator & asset template tests
|
|
150
159
|
│ └── test_validators.py # Path and schema validation tests
|
|
151
160
|
├── pyproject.toml # PEP 621 packaging configuration
|
|
152
161
|
├── LICENSE # MIT License
|
|
153
162
|
└── README.md
|
|
154
|
-
|
|
155
163
|
```
|
|
156
164
|
|
|
157
165
|
---
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "repopy"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.1"
|
|
8
8
|
description = "A modular CLI workspace initializer and cloning automation tool."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
@@ -35,3 +35,12 @@ Repository = "https://github.com/manatunga/repopy"
|
|
|
35
35
|
|
|
36
36
|
[project.scripts]
|
|
37
37
|
repopy = "repopy.__main__:main"
|
|
38
|
+
|
|
39
|
+
[tool.coverage.report]
|
|
40
|
+
exclude_lines = [
|
|
41
|
+
"pragma: no cover",
|
|
42
|
+
"if __name__ == .__main__.:",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
[tool.setuptools.package-data]
|
|
46
|
+
repopy = ["py.typed"]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main entrypoint of repopy. Brings together the argument parser and
|
|
3
|
+
the orchestrators together under a main function.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from repopy.cli import parse_arguments
|
|
9
|
+
from repopy.orchestrators import CloneInitializer, LinkInitializer, LocalInitializer
|
|
10
|
+
from repopy.prompts import capture_project_manifests
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main() -> None:
|
|
14
|
+
try:
|
|
15
|
+
args = parse_arguments()
|
|
16
|
+
|
|
17
|
+
if args.command == "init":
|
|
18
|
+
print("⌛ Working on it...\n")
|
|
19
|
+
manifest = capture_project_manifests(args)
|
|
20
|
+
local_initializer = LocalInitializer(manifest)
|
|
21
|
+
success = local_initializer.run()
|
|
22
|
+
|
|
23
|
+
if success and args.link:
|
|
24
|
+
print("⌛ Initiating automated repository link shortcut...")
|
|
25
|
+
link_initializer = LinkInitializer(args.link, args.message)
|
|
26
|
+
link_initializer.run()
|
|
27
|
+
|
|
28
|
+
elif args.command == "clone":
|
|
29
|
+
print("⌛ Working on it...\n")
|
|
30
|
+
clone_initializer = CloneInitializer(
|
|
31
|
+
args.repo_url, args.name, args.install, args.no_install
|
|
32
|
+
)
|
|
33
|
+
clone_initializer.run()
|
|
34
|
+
|
|
35
|
+
elif args.command == "link":
|
|
36
|
+
print("⌛ Working on it...\n")
|
|
37
|
+
link_initializer = LinkInitializer(args.repo_url, args.message)
|
|
38
|
+
link_initializer.run()
|
|
39
|
+
|
|
40
|
+
else:
|
|
41
|
+
print("Usage: repopy [init | clone | link] --help")
|
|
42
|
+
print("⚠️ Error: Please specify a subcommand")
|
|
43
|
+
|
|
44
|
+
except KeyboardInterrupt:
|
|
45
|
+
print("\n ✖️ Workspace operation cancelled by user.")
|
|
46
|
+
sys.exit(1)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
if __name__ == "__main__":
|
|
50
|
+
main()
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Parser layer. Imports Python's built-in argparse module
|
|
3
|
+
to identify user intent and any extra information passed.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def parse_arguments() -> argparse.Namespace:
|
|
10
|
+
parser = argparse.ArgumentParser(
|
|
11
|
+
description="repopy: A modular project scaffolder and workflow manager for Python."
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
15
|
+
|
|
16
|
+
# ---------------------------------------------------------------------------------
|
|
17
|
+
# `repopy init` subcommand
|
|
18
|
+
# ---------------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
init_parser = subparsers.add_parser(
|
|
21
|
+
"init",
|
|
22
|
+
help="Initializes a standardized Python development workspace in a new directory",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# Optional argument to add a project_name
|
|
26
|
+
init_parser.add_argument(
|
|
27
|
+
"project_name", nargs="?", help="Name of the target project folder to create"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Project theme shortcut flag (-t / --theme)
|
|
31
|
+
init_parser.add_argument(
|
|
32
|
+
"-t",
|
|
33
|
+
"--theme",
|
|
34
|
+
choices=["minimal", "web_api", "cli_package", "data_science"],
|
|
35
|
+
help="Pre-select an architectural theme layout",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# Skip flag (-s / --skip) to bypass input prompts completely
|
|
39
|
+
init_parser.add_argument(
|
|
40
|
+
"-s",
|
|
41
|
+
"--skip",
|
|
42
|
+
action="store_true",
|
|
43
|
+
help="Skip interactive loops and generate workspace using default values",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Link flag (-l / --link) to pass an existing remote URL during creation
|
|
47
|
+
init_parser.add_argument(
|
|
48
|
+
"-l",
|
|
49
|
+
"--link",
|
|
50
|
+
metavar="GIT_URL",
|
|
51
|
+
help="Automatically link and push the new local repo to this remote Git URL",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Custom commit message shortcut allowed directly during init orchestration
|
|
55
|
+
init_parser.add_argument(
|
|
56
|
+
"-m",
|
|
57
|
+
"--message",
|
|
58
|
+
help="Custom commit message to use for the initial repository push",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# ---------------------------------------------------------------------------------
|
|
62
|
+
# `repopy clone` subcommand
|
|
63
|
+
# ---------------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
clone_parser = subparsers.add_parser(
|
|
66
|
+
"clone", help="Clones a remote Git repository to the local machine"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Positional argument to add remote Git URL
|
|
70
|
+
clone_parser.add_argument(
|
|
71
|
+
"repo_url", help="Repository URL from Github to clone locally"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Name flag (-n / --name) to pass an optional name for the to-be-cloned repository
|
|
75
|
+
clone_parser.add_argument("-n", "--name", help="Name of the project folder")
|
|
76
|
+
|
|
77
|
+
# Mutually exclusive commands regarding installing dependencies:
|
|
78
|
+
# install flag (-i / --install) for installing dependencies, bypassing prompt
|
|
79
|
+
# download-only flag (-d / --download-only) for only fetching dependencies without installing, bypassing prompt
|
|
80
|
+
deps_group = clone_parser.add_mutually_exclusive_group()
|
|
81
|
+
deps_group.add_argument(
|
|
82
|
+
"-i",
|
|
83
|
+
"--install",
|
|
84
|
+
action="store_true",
|
|
85
|
+
help="Automatically install dependencies without confirmation",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
deps_group.add_argument(
|
|
89
|
+
"--no-install",
|
|
90
|
+
action="store_true",
|
|
91
|
+
help="Skip dependency installation completely (fetch-only safe mode)",
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# ---------------------------------------------------------------------------------
|
|
95
|
+
# `repopy link` subcommand
|
|
96
|
+
# ---------------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
link_parser = subparsers.add_parser(
|
|
99
|
+
"link",
|
|
100
|
+
help="Stage, commit and link current local repository to a remote Git URL",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Positional argument to add a dedicated existing remote Git URL for the local repository
|
|
104
|
+
link_parser.add_argument(
|
|
105
|
+
"repo_url",
|
|
106
|
+
help="Target remote Git repository URL (e.g., git@github.com:user/repo.git)",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
link_parser.add_argument(
|
|
110
|
+
"-m",
|
|
111
|
+
"--message",
|
|
112
|
+
help="Custom commit message to use instead of the default repopy message",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
return parser.parse_args()
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
"""
|
|
2
2
|
Layer for Dependency-checking. Contains functions to check whether
|
|
3
3
|
expected dependencies (such as python and git) are installed.
|
|
4
|
-
|
|
4
|
+
"""
|
|
5
5
|
|
|
6
6
|
import shutil
|
|
7
7
|
|
|
8
|
+
|
|
8
9
|
def has_python() -> bool:
|
|
9
|
-
return bool(shutil.which(
|
|
10
|
+
return bool(shutil.which("python") or shutil.which("python3"))
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
def has_git() -> bool:
|
|
13
|
-
return bool(shutil.which(
|
|
14
|
+
return bool(shutil.which("git"))
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core Engine for the `repopy local` command. Houses the FileSystemEngine class
|
|
3
|
+
that holds the methods required to manipulate directories, create virtural
|
|
4
|
+
environments and create .gitignore and README.md templates.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import shutil
|
|
11
|
+
import subprocess
|
|
12
|
+
import venv
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from repopy.os_detector import is_windows
|
|
16
|
+
from repopy.templates import (
|
|
17
|
+
GITIGNORE_TEMPLATE,
|
|
18
|
+
generate_pyproject_toml,
|
|
19
|
+
generate_readme,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class FileSystemEngine:
|
|
26
|
+
def __init__(self, project_path: Path):
|
|
27
|
+
self.project_path = project_path
|
|
28
|
+
|
|
29
|
+
def create_root_directory(self) -> bool:
|
|
30
|
+
"""Creates new project directory with user-specified project name"""
|
|
31
|
+
try:
|
|
32
|
+
self.project_path.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
return True
|
|
34
|
+
|
|
35
|
+
except OSError:
|
|
36
|
+
logger.error(f"❌ Failed to create directory at {self.project_path}")
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
def create_theme_directories(self, theme: str, project_name: str):
|
|
40
|
+
"""Maps and structures the child directory tree based on the project theme"""
|
|
41
|
+
if theme == "web_api":
|
|
42
|
+
sub_dirs = ["src/app", "src/app/routes", "src/app/models"]
|
|
43
|
+
|
|
44
|
+
elif theme == "cli_package":
|
|
45
|
+
sub_dirs = [f"src/{project_name}", "tests"]
|
|
46
|
+
|
|
47
|
+
elif theme == "data_science":
|
|
48
|
+
sub_dirs = ["data/raw", "data/processed", "notebooks", "src/pipeline"]
|
|
49
|
+
|
|
50
|
+
else:
|
|
51
|
+
return True
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
for folder in sub_dirs:
|
|
55
|
+
target_folder_path = (self.project_path / folder).resolve()
|
|
56
|
+
target_folder_path.mkdir(parents=True, exist_ok=True)
|
|
57
|
+
|
|
58
|
+
return True
|
|
59
|
+
|
|
60
|
+
except OSError:
|
|
61
|
+
logger.error(f"❌ Failed to build nested structures for theme {theme}")
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
def write_theme_configurations(self, theme: str, manifest: dict) -> bool:
|
|
65
|
+
"""Deploys standard configuration templates (gitignore, requirements.txt or toml)"""
|
|
66
|
+
try:
|
|
67
|
+
gitignore_path = self.project_path / ".gitignore"
|
|
68
|
+
with open(gitignore_path, "w") as f:
|
|
69
|
+
f.write(GITIGNORE_TEMPLATE)
|
|
70
|
+
|
|
71
|
+
readme_content = generate_readme(manifest)
|
|
72
|
+
readme_path = self.project_path / "README.md"
|
|
73
|
+
with open(readme_path, "w") as f:
|
|
74
|
+
f.write(readme_content)
|
|
75
|
+
|
|
76
|
+
if theme == "minimal":
|
|
77
|
+
req_path = self.project_path / "requirements.txt"
|
|
78
|
+
with open(req_path, "w") as f:
|
|
79
|
+
f.write("")
|
|
80
|
+
|
|
81
|
+
else:
|
|
82
|
+
toml_content = generate_pyproject_toml(manifest)
|
|
83
|
+
toml_path = self.project_path / "pyproject.toml"
|
|
84
|
+
with open(toml_path, "w") as f:
|
|
85
|
+
f.write(toml_content)
|
|
86
|
+
|
|
87
|
+
return True
|
|
88
|
+
|
|
89
|
+
except OSError:
|
|
90
|
+
logger.error("❌ Failed to deploy structural file sheets")
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
def create_virtual_environment(self) -> bool:
|
|
94
|
+
"""Initializes a python virtual environment"""
|
|
95
|
+
venv_dir = self.project_path / ".venv"
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
venv.create(venv_dir, with_pip=True)
|
|
99
|
+
return True
|
|
100
|
+
|
|
101
|
+
except Exception:
|
|
102
|
+
logger.exception(
|
|
103
|
+
f"❌ Failed to create virtual environment at {self.project_path}"
|
|
104
|
+
)
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
def initialize_git(self) -> bool:
|
|
108
|
+
"""Initializes git within the newly created project directory"""
|
|
109
|
+
try:
|
|
110
|
+
result = subprocess.run(
|
|
111
|
+
["git", "init"],
|
|
112
|
+
cwd=self.project_path,
|
|
113
|
+
capture_output=True,
|
|
114
|
+
text=True,
|
|
115
|
+
check=False,
|
|
116
|
+
)
|
|
117
|
+
return result.returncode == 0
|
|
118
|
+
except OSError:
|
|
119
|
+
logger.error(f"❌ Unable to initialize git at {self.project_path}")
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
def read_requirements(self) -> list[str]:
|
|
123
|
+
"""Read repository dependencies if requirements.txt exists and displays them"""
|
|
124
|
+
req_path = self.project_path / "requirements.txt"
|
|
125
|
+
if not req_path.is_file():
|
|
126
|
+
return []
|
|
127
|
+
|
|
128
|
+
with open(req_path, "r") as f:
|
|
129
|
+
lines = [
|
|
130
|
+
cleaned_line
|
|
131
|
+
for line in f
|
|
132
|
+
if (cleaned_line := line.strip()) and not cleaned_line.startswith("#")
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
return lines
|
|
136
|
+
|
|
137
|
+
def install_dependencies(self) -> bool:
|
|
138
|
+
"""Install repository dependencies if requirements.txt is present"""
|
|
139
|
+
req_file = self.project_path / "requirements.txt"
|
|
140
|
+
|
|
141
|
+
if not req_file.exists():
|
|
142
|
+
return True
|
|
143
|
+
|
|
144
|
+
if is_windows():
|
|
145
|
+
pip_path = self.project_path / ".venv" / "Scripts" / "pip.exe"
|
|
146
|
+
else:
|
|
147
|
+
pip_path = self.project_path / ".venv" / "bin" / "pip"
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
result = subprocess.run(
|
|
151
|
+
[str(pip_path), "install", "-r", str(req_file)],
|
|
152
|
+
capture_output=True,
|
|
153
|
+
text=True,
|
|
154
|
+
check=False,
|
|
155
|
+
)
|
|
156
|
+
return result.returncode == 0
|
|
157
|
+
|
|
158
|
+
except OSError:
|
|
159
|
+
logger.error(f"❌ Failed to install dependencies at {self.project_path}")
|
|
160
|
+
return False
|
|
161
|
+
|
|
162
|
+
def get_activation_guide(self) -> str:
|
|
163
|
+
if is_windows():
|
|
164
|
+
venv_activation = """
|
|
165
|
+
.\\.venv\\Scripts\\Activate.ps1 (if on Powershell)
|
|
166
|
+
.\\.venv\\Scripts\\activate.bat (if on cmd)\n
|
|
167
|
+
"""
|
|
168
|
+
else:
|
|
169
|
+
venv_activation = """
|
|
170
|
+
source .venv/bin/activate\n
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
return f"""
|
|
174
|
+
To activate the virtual environment and get started, enter the following:
|
|
175
|
+
|
|
176
|
+
cd {self.project_path}
|
|
177
|
+
{venv_activation}
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
def cleanup(self) -> None:
|
|
181
|
+
"""Removes any partially created project directories if a failure occurs"""
|
|
182
|
+
if self.project_path.exists():
|
|
183
|
+
try:
|
|
184
|
+
shutil.rmtree(self.project_path)
|
|
185
|
+
logger.info(
|
|
186
|
+
f"☑️ Successfully cleaned up half-baked workspace at {self.project_path}"
|
|
187
|
+
)
|
|
188
|
+
except OSError:
|
|
189
|
+
logger.error(f"❌ Failed to clean up directory at {self.project_path}")
|
|
190
|
+
|
|
191
|
+
def build_workspace(self, theme: str, manifest: dict) -> bool:
|
|
192
|
+
"""Orchestrate entire file-system creation sequence"""
|
|
193
|
+
|
|
194
|
+
return bool(
|
|
195
|
+
self.create_root_directory()
|
|
196
|
+
and self.initialize_git()
|
|
197
|
+
and self.create_theme_directories(theme, manifest["project_name"])
|
|
198
|
+
and self.write_theme_configurations(theme, manifest)
|
|
199
|
+
and self.create_virtual_environment()
|
|
200
|
+
)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core Engine for the `repopy clone` command. Houses the GitEngine class that
|
|
3
|
+
holds the methods required to cone a GitHub repository and install
|
|
4
|
+
dependencies.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import subprocess
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class GitEngine:
|
|
17
|
+
def __init__(self, repo_url: str, dir_name: str | None):
|
|
18
|
+
self.repo_url = repo_url
|
|
19
|
+
|
|
20
|
+
cleaned_url = repo_url.removesuffix(".git")
|
|
21
|
+
final_name = dir_name if dir_name else Path(cleaned_url).name
|
|
22
|
+
|
|
23
|
+
self.project_path = Path(final_name)
|
|
24
|
+
|
|
25
|
+
def clone_repository(self) -> bool:
|
|
26
|
+
"""Clone GitHub repository using the given url to the given directory name"""
|
|
27
|
+
try:
|
|
28
|
+
result = subprocess.run(
|
|
29
|
+
["git", "clone", self.repo_url, str(self.project_path)],
|
|
30
|
+
text=True,
|
|
31
|
+
check=False,
|
|
32
|
+
)
|
|
33
|
+
return result.returncode == 0
|
|
34
|
+
|
|
35
|
+
except OSError:
|
|
36
|
+
logger.error(f"❌ Failed to clone repository at {self.project_path}")
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class GitLinkEngine:
|
|
41
|
+
def __init__(self, repo_url: str, message: str):
|
|
42
|
+
self.repo_url = repo_url
|
|
43
|
+
self.message = message
|
|
44
|
+
|
|
45
|
+
def link_and_push(self) -> bool:
|
|
46
|
+
"""Runs the sequence of Git terminal execution blocks to link the repo"""
|
|
47
|
+
try:
|
|
48
|
+
stage_result = subprocess.run(
|
|
49
|
+
["git", "add", "."], capture_output=True, text=True, check=False
|
|
50
|
+
)
|
|
51
|
+
if stage_result.returncode != 0:
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
commit_result = subprocess.run(
|
|
55
|
+
["git", "commit", "-m", self.message],
|
|
56
|
+
capture_output=True,
|
|
57
|
+
text=True,
|
|
58
|
+
check=False,
|
|
59
|
+
)
|
|
60
|
+
if (
|
|
61
|
+
commit_result.returncode != 0
|
|
62
|
+
and "nothing to commit" not in commit_result.stdout
|
|
63
|
+
):
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
branch_result = subprocess.run(
|
|
67
|
+
["git", "branch", "-M", "main"],
|
|
68
|
+
capture_output=True,
|
|
69
|
+
text=True,
|
|
70
|
+
check=False,
|
|
71
|
+
)
|
|
72
|
+
if branch_result.returncode != 0:
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
remote_result = subprocess.run(
|
|
76
|
+
["git", "remote", "add", "origin", self.repo_url],
|
|
77
|
+
capture_output=True,
|
|
78
|
+
text=True,
|
|
79
|
+
check=False,
|
|
80
|
+
)
|
|
81
|
+
if remote_result.returncode != 0:
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
push_result = subprocess.run(
|
|
85
|
+
["git", "push", "-u", "origin", "main"], text=True, check=False
|
|
86
|
+
)
|
|
87
|
+
return push_result.returncode == 0
|
|
88
|
+
|
|
89
|
+
except OSError:
|
|
90
|
+
logger.error("❌ Failed to execute Git linking operations")
|
|
91
|
+
return False
|