Coverage for app/services/git_service.py: 90.77%
65 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-25 16:05 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-25 16:05 +0000
1"""Git service for repository management with HTTPS token authentication."""
3import logging
4import re
5import shutil
6from pathlib import Path
8import git
10from ..config import settings
12logger = logging.getLogger(__name__)
15class GitService:
16 """Service for Git operations with HTTPS token authentication."""
18 def __init__(self) -> None:
19 """Initialize Git service with settings."""
20 self.base_path = Path(settings.TEMP_REPO_BASE_PATH)
21 self.token = settings.GIT_ACCESS_TOKEN
23 def _parse_git_url(self, git_url: str) -> dict | None:
24 """Parse Git URL and return components."""
25 # SSH format: git@github.com:owner/repo.git
26 ssh_pattern = r"^git@([^:]+):([^/]+)/(.+?)(?:\.git)?$"
27 # HTTPS format: https://github.com/owner/repo.git
28 https_pattern = r"^https?://([^/]+)/([^/]+)/(.+?)(?:\.git)?$"
30 ssh_match = re.match(ssh_pattern, git_url)
31 if ssh_match:
32 host, owner, repo = ssh_match.groups()
33 return {"host": host, "owner": owner, "repo": repo}
35 https_match = re.match(https_pattern, git_url)
36 if https_match:
37 host, owner, repo = https_match.groups()
38 return {"host": host, "owner": owner, "repo": repo}
40 return None
42 def _get_authenticated_url(self, git_url: str) -> str:
43 """Convert Git URL to HTTPS format with token authentication."""
44 url = git_url
45 if url.startswith("git@"):
46 # Convert SSH to HTTPS: git@github.com:owner/repo.git -> https://token@github.com/owner/repo.git
47 url = url.replace(":", "/").replace("git@", f"https://{self.token}@")
48 elif url.startswith("https://"):
49 # Add token to HTTPS URL
50 parsed = self._parse_git_url(url)
51 if parsed:
52 url = f"https://{self.token}@{parsed['host']}/{parsed['owner']}/{parsed['repo']}.git"
54 return url
56 def clone_release(self, git_url: str, tag: str, deployment_id: str) -> str:
57 """
58 Clone a specific release/tag using shallow clone with HTTPS token authentication.
60 Args:
61 git_url: URL of the Git repository
62 tag: Tag/Release name
63 deployment_id: Unique ID for the target folder
65 Returns:
66 Path to the cloned repository
68 Raises:
69 Exception with detailed error message
70 """
71 repo_path = self.base_path / f"deploy_{deployment_id}"
73 if repo_path.exists():
74 logger.info(f"Removing existing repo at {repo_path}")
75 shutil.rmtree(repo_path)
77 try:
78 # Convert to authenticated HTTPS URL
79 auth_url = self._get_authenticated_url(git_url)
81 logger.info(f"Cloning repository {git_url}")
82 logger.info(f"Target: {repo_path}")
83 logger.info(f"Branch/Tag: {tag}")
84 logger.info("Mode: Shallow clone (depth=1, single branch)")
86 # Clone with --branch <tag> --depth 1 (no history, only the tag)
87 repo = git.Repo.clone_from(auth_url, str(repo_path), branch=tag, depth=1, single_branch=True)
89 logger.info(f"✓ Repository cloned successfully to {repo_path}")
90 logger.info(f"Current HEAD: {repo.head.commit.hexsha}")
92 return str(repo_path)
94 except git.exc.GitCommandError as e:
95 error_msg = f"Git command failed: {e.stderr if hasattr(e, 'stderr') else str(e)}"
96 logger.error(error_msg)
97 if repo_path.exists():
98 shutil.rmtree(repo_path)
99 raise Exception(error_msg)
100 except Exception as e:
101 error_msg = f"Failed to clone release {tag} from {git_url}: {str(e)}"
102 logger.error(error_msg)
103 if repo_path.exists():
104 shutil.rmtree(repo_path)
105 raise Exception(error_msg)
107 def cleanup_repository(self, repo_path: str) -> None:
108 """Delete the cloned repository."""
109 path = Path(repo_path)
110 if path.exists():
111 logger.info(f"Cleaning up {repo_path}")
112 shutil.rmtree(path)
115# Singleton
116git_service = GitService()