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.
Files changed (45) hide show
  1. {repopy-0.3.0/src/repopy.egg-info → repopy-0.3.1}/PKG-INFO +12 -4
  2. {repopy-0.3.0 → repopy-0.3.1}/README.md +11 -3
  3. {repopy-0.3.0 → repopy-0.3.1}/pyproject.toml +10 -1
  4. repopy-0.3.1/src/repopy/__main__.py +50 -0
  5. repopy-0.3.1/src/repopy/cli.py +115 -0
  6. {repopy-0.3.0 → repopy-0.3.1}/src/repopy/dependencies.py +5 -4
  7. repopy-0.3.1/src/repopy/file_system.py +200 -0
  8. repopy-0.3.1/src/repopy/git_engine.py +91 -0
  9. repopy-0.3.1/src/repopy/orchestrators.py +166 -0
  10. {repopy-0.3.0 → repopy-0.3.1}/src/repopy/os_detector.py +14 -10
  11. repopy-0.3.1/src/repopy/prompts.py +146 -0
  12. repopy-0.3.1/src/repopy/py.typed +0 -0
  13. repopy-0.3.1/src/repopy/templates.py +60 -0
  14. repopy-0.3.1/src/repopy/validators.py +48 -0
  15. {repopy-0.3.0 → repopy-0.3.1/src/repopy.egg-info}/PKG-INFO +12 -4
  16. {repopy-0.3.0 → repopy-0.3.1}/src/repopy.egg-info/SOURCES.txt +5 -0
  17. {repopy-0.3.0 → repopy-0.3.1}/tests/test_cli.py +26 -8
  18. repopy-0.3.1/tests/test_dependencies.py +27 -0
  19. repopy-0.3.1/tests/test_file_system.py +356 -0
  20. repopy-0.3.1/tests/test_git_engine.py +173 -0
  21. repopy-0.3.1/tests/test_main.py +156 -0
  22. repopy-0.3.1/tests/test_orchestrators.py +413 -0
  23. repopy-0.3.1/tests/test_os_detector.py +43 -0
  24. repopy-0.3.1/tests/test_prompts.py +207 -0
  25. repopy-0.3.1/tests/test_templates.py +40 -0
  26. repopy-0.3.1/tests/test_validators.py +62 -0
  27. repopy-0.3.0/src/repopy/__main__.py +0 -47
  28. repopy-0.3.0/src/repopy/cli.py +0 -115
  29. repopy-0.3.0/src/repopy/file_system.py +0 -186
  30. repopy-0.3.0/src/repopy/git_engine.py +0 -70
  31. repopy-0.3.0/src/repopy/orchestrators.py +0 -149
  32. repopy-0.3.0/src/repopy/prompts.py +0 -131
  33. repopy-0.3.0/src/repopy/templates.py +0 -47
  34. repopy-0.3.0/src/repopy/validators.py +0 -53
  35. repopy-0.3.0/tests/test_file_system.py +0 -27
  36. repopy-0.3.0/tests/test_git_engine.py +0 -106
  37. repopy-0.3.0/tests/test_orchestrators.py +0 -179
  38. repopy-0.3.0/tests/test_prompts.py +0 -106
  39. repopy-0.3.0/tests/test_validators.py +0 -41
  40. {repopy-0.3.0 → repopy-0.3.1}/LICENSE +0 -0
  41. {repopy-0.3.0 → repopy-0.3.1}/setup.cfg +0 -0
  42. {repopy-0.3.0 → repopy-0.3.1}/src/repopy/__init__.py +0 -0
  43. {repopy-0.3.0 → repopy-0.3.1}/src/repopy.egg-info/dependency_links.txt +0 -0
  44. {repopy-0.3.0 → repopy-0.3.1}/src/repopy.egg-info/entry_points.txt +0 -0
  45. {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.0
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
  ![PyPI - Version](https://img.shields.io/pypi/v/repopy?color=orange)&nbsp;&nbsp;&nbsp;
31
+ [![License: MIT](https://img.shields.io/pypi/l/repopy.svg)](https://opensource.org/license/mit)&nbsp;&nbsp;&nbsp;
32
+ [![Python Versions](https://img.shields.io/pypi/pyversions/repopy.svg)](https://pypi.org)
33
+
31
34
  [![CI](https://github.com/manatunga/repopy/actions/workflows/tests.yml/badge.svg)](https://github.com/manatunga/repopy/actions/workflows/tests.yml)&nbsp;&nbsp;&nbsp;
32
- [![Python Versions](https://img.shields.io/pypi/pyversions/repopy.svg)](https://pypi.org)&nbsp;&nbsp;&nbsp;
33
- [![License: MIT](https://img.shields.io/pypi/l/repopy.svg)](https://opensource.org/license/mit)
35
+ [![codecov](https://codecov.io/github/manatunga/repopy/graph/badge.svg?token=2JRECANC1G)](https://codecov.io/github/manatunga/repopy)&nbsp;&nbsp;&nbsp;
36
+ [![type checked - mypy](https://img.shields.io/badge/type--checked-mypy-blue?logo=python)](https://mypy-lang.org)&nbsp;&nbsp;&nbsp;
37
+ [![Ruff](https://custom-icon-badges.demolab.com/badge/Ruff-261230.svg?logo=ruff-logo)](#)
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
  ![PyPI - Version](https://img.shields.io/pypi/v/repopy?color=orange)&nbsp;&nbsp;&nbsp;
6
+ [![License: MIT](https://img.shields.io/pypi/l/repopy.svg)](https://opensource.org/license/mit)&nbsp;&nbsp;&nbsp;
7
+ [![Python Versions](https://img.shields.io/pypi/pyversions/repopy.svg)](https://pypi.org)
8
+
6
9
  [![CI](https://github.com/manatunga/repopy/actions/workflows/tests.yml/badge.svg)](https://github.com/manatunga/repopy/actions/workflows/tests.yml)&nbsp;&nbsp;&nbsp;
7
- [![Python Versions](https://img.shields.io/pypi/pyversions/repopy.svg)](https://pypi.org)&nbsp;&nbsp;&nbsp;
8
- [![License: MIT](https://img.shields.io/pypi/l/repopy.svg)](https://opensource.org/license/mit)
10
+ [![codecov](https://codecov.io/github/manatunga/repopy/graph/badge.svg?token=2JRECANC1G)](https://codecov.io/github/manatunga/repopy)&nbsp;&nbsp;&nbsp;
11
+ [![type checked - mypy](https://img.shields.io/badge/type--checked-mypy-blue?logo=python)](https://mypy-lang.org)&nbsp;&nbsp;&nbsp;
12
+ [![Ruff](https://custom-icon-badges.demolab.com/badge/Ruff-261230.svg?logo=ruff-logo)](#)
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.0"
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('python') or shutil.which('python3'))
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('git'))
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