git-recap 0.1.5__py3-none-any.whl → 0.1.6__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.
- git_recap/cli.py +270 -0
- git_recap/fetcher.py +27 -3
- git_recap/providers/__init__.py +3 -1
- git_recap/providers/azure_fetcher.py +86 -29
- git_recap/providers/base_fetcher.py +30 -0
- git_recap/providers/github_fetcher.py +79 -11
- git_recap/providers/gitlab_fetcher.py +67 -2
- git_recap/providers/local_fetcher.py +390 -0
- git_recap/providers/url_fetcher.py +88 -12
- {git_recap-0.1.5.dist-info → git_recap-0.1.6.dist-info}/METADATA +15 -10
- git_recap-0.1.6.dist-info/RECORD +17 -0
- {git_recap-0.1.5.dist-info → git_recap-0.1.6.dist-info}/WHEEL +1 -1
- git_recap-0.1.6.dist-info/entry_points.txt +2 -0
- git_recap-0.1.5.dist-info/RECORD +0 -14
- {git_recap-0.1.5.dist-info → git_recap-0.1.6.dist-info}/licenses/LICENSE +0 -0
- {git_recap-0.1.5.dist-info → git_recap-0.1.6.dist-info}/top_level.txt +0 -0
|
@@ -12,7 +12,7 @@ class GitHubFetcher(BaseFetcher):
|
|
|
12
12
|
"""
|
|
13
13
|
Fetcher implementation for GitHub repositories.
|
|
14
14
|
|
|
15
|
-
Supports fetching commits, pull requests, issues, and
|
|
15
|
+
Supports fetching commits, pull requests, issues, releases, and authors.
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
18
|
def __init__(self, pat: str, start_date=None, end_date=None, repo_filter=None, authors=None):
|
|
@@ -92,9 +92,7 @@ class GitHubFetcher(BaseFetcher):
|
|
|
92
92
|
|
|
93
93
|
def fetch_pull_requests(self) -> List[Dict[str, Any]]:
|
|
94
94
|
entries = []
|
|
95
|
-
# Maintain a local set to skip duplicate commits already captured in a PR.
|
|
96
95
|
processed_pr_commits = set()
|
|
97
|
-
# Retrieve repos where you're owner, a collaborator, or an organization member.
|
|
98
96
|
for repo in self.repos:
|
|
99
97
|
if self.repo_filter and repo.name not in self.repo_filter:
|
|
100
98
|
continue
|
|
@@ -102,11 +100,10 @@ class GitHubFetcher(BaseFetcher):
|
|
|
102
100
|
for i, pr in enumerate(pulls, start=1):
|
|
103
101
|
if pr.user.login not in self.authors:
|
|
104
102
|
continue
|
|
105
|
-
pr_date = pr.updated_at
|
|
103
|
+
pr_date = pr.updated_at
|
|
106
104
|
if not self._filter_by_date(pr_date):
|
|
107
105
|
continue
|
|
108
106
|
|
|
109
|
-
# Add the pull request itself.
|
|
110
107
|
pr_entry = {
|
|
111
108
|
"type": "pull_request",
|
|
112
109
|
"repo": repo.name,
|
|
@@ -116,7 +113,6 @@ class GitHubFetcher(BaseFetcher):
|
|
|
116
113
|
}
|
|
117
114
|
entries.append(pr_entry)
|
|
118
115
|
|
|
119
|
-
# Now, add commits associated with this pull request.
|
|
120
116
|
pr_commits = pr.get_commits()
|
|
121
117
|
for pr_commit in pr_commits:
|
|
122
118
|
commit_date = pr_commit.commit.author.date
|
|
@@ -178,7 +174,6 @@ class GitHubFetcher(BaseFetcher):
|
|
|
178
174
|
continue
|
|
179
175
|
try:
|
|
180
176
|
for rel in repo.get_releases():
|
|
181
|
-
# Compose asset list
|
|
182
177
|
assets = []
|
|
183
178
|
for asset in rel.get_assets():
|
|
184
179
|
assets.append({
|
|
@@ -203,7 +198,6 @@ class GitHubFetcher(BaseFetcher):
|
|
|
203
198
|
}
|
|
204
199
|
releases.append(release_entry)
|
|
205
200
|
except Exception:
|
|
206
|
-
# If fetching releases fails for a repo, skip it (could be permissions or no releases)
|
|
207
201
|
continue
|
|
208
202
|
return releases
|
|
209
203
|
|
|
@@ -267,7 +261,6 @@ class GitHubFetcher(BaseFetcher):
|
|
|
267
261
|
continue
|
|
268
262
|
logger.debug(f"Processing repository: {repo.name}")
|
|
269
263
|
repo_branches = [branch.name for branch in repo.get_branches()]
|
|
270
|
-
# Get existing open PRs from source branch
|
|
271
264
|
try:
|
|
272
265
|
open_prs = repo.get_pulls(state='open', head=source_branch)
|
|
273
266
|
except GithubException as e:
|
|
@@ -284,7 +277,6 @@ class GitHubFetcher(BaseFetcher):
|
|
|
284
277
|
if branch_name in existing_pr_targets:
|
|
285
278
|
logger.debug(f"Excluding branch with existing PR: {branch_name}")
|
|
286
279
|
continue
|
|
287
|
-
# Optionally check if source is ahead of target (performance cost)
|
|
288
280
|
valid_targets.append(branch_name)
|
|
289
281
|
logger.debug(f"Valid target branch: {branch_name}")
|
|
290
282
|
logger.debug(f"Found {len(valid_targets)} valid target branches")
|
|
@@ -405,4 +397,80 @@ class GitHubFetcher(BaseFetcher):
|
|
|
405
397
|
raise
|
|
406
398
|
except Exception as e:
|
|
407
399
|
logger.error(f"Unexpected error while creating pull request: {str(e)}")
|
|
408
|
-
raise Exception(f"Failed to create pull request: {str(e)}")
|
|
400
|
+
raise Exception(f"Failed to create pull request: {str(e)}")
|
|
401
|
+
|
|
402
|
+
def get_authors(self, repo_names: List[str]) -> List[Dict[str, str]]:
|
|
403
|
+
"""
|
|
404
|
+
Retrieve unique authors from specified GitHub repositories.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
repo_names: List of repository names (format: "owner/repo").
|
|
408
|
+
Empty list fetches from all accessible repositories.
|
|
409
|
+
|
|
410
|
+
Returns:
|
|
411
|
+
List of unique author dictionaries with name and email.
|
|
412
|
+
"""
|
|
413
|
+
authors_set = set()
|
|
414
|
+
|
|
415
|
+
try:
|
|
416
|
+
if not repo_names:
|
|
417
|
+
repos = self.github.get_user().get_repos()
|
|
418
|
+
repo_names = [repo.full_name for repo in repos]
|
|
419
|
+
|
|
420
|
+
for repo_name in repo_names:
|
|
421
|
+
try:
|
|
422
|
+
repo = self.github.get_repo(repo_name)
|
|
423
|
+
|
|
424
|
+
commits = repo.get_commits()
|
|
425
|
+
|
|
426
|
+
for commit in commits:
|
|
427
|
+
if commit.commit.author:
|
|
428
|
+
author_name = commit.commit.author.name or "Unknown"
|
|
429
|
+
author_email = commit.commit.author.email or "unknown@example.com"
|
|
430
|
+
authors_set.add((author_name, author_email))
|
|
431
|
+
|
|
432
|
+
if commit.commit.committer:
|
|
433
|
+
committer_name = commit.commit.committer.name or "Unknown"
|
|
434
|
+
committer_email = commit.commit.committer.email or "unknown@example.com"
|
|
435
|
+
authors_set.add((committer_name, committer_email))
|
|
436
|
+
|
|
437
|
+
except GithubException as e:
|
|
438
|
+
print(f"Error fetching authors from {repo_name}: {e}")
|
|
439
|
+
continue
|
|
440
|
+
|
|
441
|
+
authors_list = [
|
|
442
|
+
{"name": name, "email": email}
|
|
443
|
+
for name, email in sorted(authors_set)
|
|
444
|
+
]
|
|
445
|
+
|
|
446
|
+
return authors_list
|
|
447
|
+
|
|
448
|
+
except Exception as e:
|
|
449
|
+
print(f"Error in get_authors: {e}")
|
|
450
|
+
return []
|
|
451
|
+
|
|
452
|
+
def get_current_author(self) -> Optional[Dict[str, str]]:
|
|
453
|
+
"""
|
|
454
|
+
Retrieve the current authenticated user's information.
|
|
455
|
+
|
|
456
|
+
Returns the authenticated GitHub user's name and email if available.
|
|
457
|
+
|
|
458
|
+
Returns:
|
|
459
|
+
Optional[Dict[str, str]]: Dictionary with 'name' and 'email' keys,
|
|
460
|
+
or None if user information is unavailable.
|
|
461
|
+
"""
|
|
462
|
+
try:
|
|
463
|
+
if not self.user:
|
|
464
|
+
logger.warning("No authenticated user available")
|
|
465
|
+
return None
|
|
466
|
+
|
|
467
|
+
user_name = self.user.name or self.user.login or "Unknown"
|
|
468
|
+
user_email = self.user.email or f"{self.user.login}@users.noreply.github.com"
|
|
469
|
+
|
|
470
|
+
return {
|
|
471
|
+
"name": user_name,
|
|
472
|
+
"email": user_email
|
|
473
|
+
}
|
|
474
|
+
except Exception as e:
|
|
475
|
+
logger.error(f"Error retrieving current author: {str(e)}")
|
|
476
|
+
return None
|
|
@@ -7,7 +7,7 @@ class GitLabFetcher(BaseFetcher):
|
|
|
7
7
|
"""
|
|
8
8
|
Fetcher implementation for GitLab repositories.
|
|
9
9
|
|
|
10
|
-
Supports fetching commits, merge requests (pull requests), and
|
|
10
|
+
Supports fetching commits, merge requests (pull requests), issues, and authors.
|
|
11
11
|
Release fetching is not supported and will raise NotImplementedError.
|
|
12
12
|
"""
|
|
13
13
|
|
|
@@ -246,4 +246,69 @@ class GitLabFetcher(BaseFetcher):
|
|
|
246
246
|
Raises:
|
|
247
247
|
NotImplementedError: Always, since PR creation is not yet implemented for GitLabFetcher.
|
|
248
248
|
"""
|
|
249
|
-
raise NotImplementedError("Pull request (merge request) creation is not yet implemented for GitLab (GitLabFetcher).")
|
|
249
|
+
raise NotImplementedError("Pull request (merge request) creation is not yet implemented for GitLab (GitLabFetcher).")
|
|
250
|
+
|
|
251
|
+
def get_authors(self, repo_names: List[str]) -> List[Dict[str, str]]:
|
|
252
|
+
"""
|
|
253
|
+
Retrieve unique authors from specified GitLab projects.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
repo_names: List of project names/IDs.
|
|
257
|
+
Empty list fetches from all accessible projects.
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
List of unique author dictionaries with name and email.
|
|
261
|
+
"""
|
|
262
|
+
authors_set = set()
|
|
263
|
+
|
|
264
|
+
try:
|
|
265
|
+
# If no specific projects provided, get all accessible projects
|
|
266
|
+
if not repo_names:
|
|
267
|
+
projects = self.gl.projects.list(membership=True, all=True)
|
|
268
|
+
repo_names = [project.path_with_namespace for project in projects]
|
|
269
|
+
|
|
270
|
+
for repo_name in repo_names:
|
|
271
|
+
try:
|
|
272
|
+
project = self.gl.projects.get(repo_name)
|
|
273
|
+
|
|
274
|
+
# Fetch commits
|
|
275
|
+
commits = project.commits.list(all=True)
|
|
276
|
+
|
|
277
|
+
for commit in commits:
|
|
278
|
+
author_name = commit.author_name or "Unknown"
|
|
279
|
+
author_email = commit.author_email or "unknown@example.com"
|
|
280
|
+
authors_set.add((author_name, author_email))
|
|
281
|
+
|
|
282
|
+
# Also add committer if available
|
|
283
|
+
if hasattr(commit, 'committer_name') and commit.committer_name:
|
|
284
|
+
committer_name = commit.committer_name
|
|
285
|
+
committer_email = commit.committer_email or "unknown@example.com"
|
|
286
|
+
authors_set.add((committer_name, committer_email))
|
|
287
|
+
|
|
288
|
+
except gitlab.exceptions.GitlabGetError as e:
|
|
289
|
+
print(f"Error fetching authors from {repo_name}: {e}")
|
|
290
|
+
continue
|
|
291
|
+
|
|
292
|
+
# Convert set to list of dictionaries
|
|
293
|
+
authors_list = [
|
|
294
|
+
{"name": name, "email": email}
|
|
295
|
+
for name, email in sorted(authors_set)
|
|
296
|
+
]
|
|
297
|
+
|
|
298
|
+
return authors_list
|
|
299
|
+
|
|
300
|
+
except Exception as e:
|
|
301
|
+
print(f"Error in get_authors: {e}")
|
|
302
|
+
return []
|
|
303
|
+
|
|
304
|
+
def get_current_author(self) -> Optional[Dict[str, str]]:
|
|
305
|
+
"""
|
|
306
|
+
Retrieve the current authenticated user's information.
|
|
307
|
+
|
|
308
|
+
For GitLab, default author functionality is not currently implemented,
|
|
309
|
+
so this method returns None.
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
None: GitLab fetcher does not support default author retrieval.
|
|
313
|
+
"""
|
|
314
|
+
return None
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
from typing import List, Dict, Any, Optional
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from git_recap.providers.base_fetcher import BaseFetcher
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LocalFetcher(BaseFetcher):
|
|
9
|
+
"""
|
|
10
|
+
Fetcher implementation for local Git repositories.
|
|
11
|
+
|
|
12
|
+
Works directly on a local git repository path without cloning.
|
|
13
|
+
Supports fetching commits, authors, and branch information.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
repo_path: str,
|
|
19
|
+
authors: Optional[List[str]] = None,
|
|
20
|
+
start_date: Optional[datetime] = None,
|
|
21
|
+
end_date: Optional[datetime] = None,
|
|
22
|
+
repo_filter: Optional[List[str]] = None,
|
|
23
|
+
validate_repo: bool = True
|
|
24
|
+
):
|
|
25
|
+
"""
|
|
26
|
+
Initialize the LocalFetcher.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
repo_path: Path to the local git repository
|
|
30
|
+
authors: List of author names to filter by (optional)
|
|
31
|
+
start_date: Start date for filtering commits (optional)
|
|
32
|
+
end_date: End date for filtering commits (optional)
|
|
33
|
+
repo_filter: List of repository names to filter (optional)
|
|
34
|
+
validate_repo: Whether to validate the repository path (default: True)
|
|
35
|
+
"""
|
|
36
|
+
super().__init__(pat=None, start_date=start_date, end_date=end_date, repo_filter=repo_filter, authors=authors)
|
|
37
|
+
self.repo_path = repo_path
|
|
38
|
+
if validate_repo:
|
|
39
|
+
self._validate_repo()
|
|
40
|
+
|
|
41
|
+
def _validate_repo(self) -> None:
|
|
42
|
+
"""
|
|
43
|
+
Validate that the provided path is a valid git repository.
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
ValueError: If the path is not a valid git repository.
|
|
47
|
+
"""
|
|
48
|
+
if not os.path.exists(self.repo_path):
|
|
49
|
+
raise ValueError(f"Path does not exist: {self.repo_path}")
|
|
50
|
+
|
|
51
|
+
if not os.path.isdir(self.repo_path):
|
|
52
|
+
raise ValueError(f"Path is not a directory: {self.repo_path}")
|
|
53
|
+
|
|
54
|
+
git_dir = os.path.join(self.repo_path, '.git')
|
|
55
|
+
if not os.path.exists(git_dir):
|
|
56
|
+
raise ValueError(f"Not a git repository: {self.repo_path}")
|
|
57
|
+
|
|
58
|
+
# Verify it's a valid git repo by running a simple command
|
|
59
|
+
try:
|
|
60
|
+
_result = subprocess.run(
|
|
61
|
+
["git", "-C", self.repo_path, "rev-parse", "--git-dir"],
|
|
62
|
+
capture_output=True,
|
|
63
|
+
text=True,
|
|
64
|
+
check=True,
|
|
65
|
+
timeout=10
|
|
66
|
+
)
|
|
67
|
+
except subprocess.TimeoutExpired:
|
|
68
|
+
raise ValueError(f"Timeout while validating git repository: {self.repo_path}")
|
|
69
|
+
except subprocess.CalledProcessError as e:
|
|
70
|
+
raise ValueError(f"Invalid git repository: {self.repo_path}. Error: {e.stderr}")
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def repos_names(self) -> List[str]:
|
|
74
|
+
"""
|
|
75
|
+
Return the repository name.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
List[str]: List containing the repository name (single item).
|
|
79
|
+
"""
|
|
80
|
+
repo_name = os.path.basename(self.repo_path)
|
|
81
|
+
return [repo_name]
|
|
82
|
+
|
|
83
|
+
def _run_git_log(self, extra_args: List[str] = None) -> List[Dict[str, Any]]:
|
|
84
|
+
"""
|
|
85
|
+
Run git log command with common arguments and parse output.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
extra_args (List[str], optional): Additional git log arguments.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
List[Dict[str, Any]]: Parsed commit entries.
|
|
92
|
+
"""
|
|
93
|
+
args = [
|
|
94
|
+
"git",
|
|
95
|
+
"-C", self.repo_path,
|
|
96
|
+
"log",
|
|
97
|
+
"--pretty=format:%H|%an|%ad|%s",
|
|
98
|
+
"--date=iso",
|
|
99
|
+
"--all"
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
if self.start_date:
|
|
103
|
+
args.extend(["--since", self.start_date.isoformat()])
|
|
104
|
+
if self.end_date:
|
|
105
|
+
args.extend(["--until", self.end_date.isoformat()])
|
|
106
|
+
if self.authors:
|
|
107
|
+
authors_filter = "|".join(self.authors)
|
|
108
|
+
args.extend(["--author", authors_filter])
|
|
109
|
+
if extra_args:
|
|
110
|
+
args.extend(extra_args)
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
result = subprocess.run(
|
|
114
|
+
args,
|
|
115
|
+
capture_output=True,
|
|
116
|
+
text=True,
|
|
117
|
+
check=True,
|
|
118
|
+
timeout=120
|
|
119
|
+
)
|
|
120
|
+
return self._parse_git_log(result.stdout)
|
|
121
|
+
except subprocess.TimeoutExpired:
|
|
122
|
+
return []
|
|
123
|
+
except subprocess.CalledProcessError:
|
|
124
|
+
return []
|
|
125
|
+
|
|
126
|
+
def _parse_git_log(self, log_output: str) -> List[Dict[str, Any]]:
|
|
127
|
+
"""
|
|
128
|
+
Parse git log output into structured data.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
log_output (str): Raw git log output.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
List[Dict[str, Any]]: Parsed commit entries.
|
|
135
|
+
"""
|
|
136
|
+
entries = []
|
|
137
|
+
for line in log_output.splitlines():
|
|
138
|
+
if not line.strip():
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
sha, author, date_str, message = line.split("|", 3)
|
|
143
|
+
timestamp = datetime.fromisoformat(date_str)
|
|
144
|
+
|
|
145
|
+
if self.start_date and timestamp < self.start_date:
|
|
146
|
+
continue
|
|
147
|
+
if self.end_date and timestamp > self.end_date:
|
|
148
|
+
continue
|
|
149
|
+
|
|
150
|
+
entries.append({
|
|
151
|
+
"type": "commit",
|
|
152
|
+
"repo": self.repos_names[0],
|
|
153
|
+
"message": message,
|
|
154
|
+
"sha": sha,
|
|
155
|
+
"author": author,
|
|
156
|
+
"timestamp": timestamp
|
|
157
|
+
})
|
|
158
|
+
except ValueError:
|
|
159
|
+
continue
|
|
160
|
+
|
|
161
|
+
return entries
|
|
162
|
+
|
|
163
|
+
def fetch_commits(self) -> List[Dict[str, Any]]:
|
|
164
|
+
"""
|
|
165
|
+
Fetch commits from the local repository.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
List[Dict[str, Any]]: List of commit entries.
|
|
169
|
+
"""
|
|
170
|
+
return self._run_git_log()
|
|
171
|
+
|
|
172
|
+
def fetch_pull_requests(self) -> List[Dict[str, Any]]:
|
|
173
|
+
"""
|
|
174
|
+
Fetch pull requests (not applicable for local repositories).
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
List[Dict[str, Any]]: Empty list (PRs are platform-specific).
|
|
178
|
+
"""
|
|
179
|
+
return []
|
|
180
|
+
|
|
181
|
+
def fetch_issues(self) -> List[Dict[str, Any]]:
|
|
182
|
+
"""
|
|
183
|
+
Fetch issues (not applicable for local repositories).
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
List[Dict[str, Any]]: Empty list (issues are platform-specific).
|
|
187
|
+
"""
|
|
188
|
+
return []
|
|
189
|
+
|
|
190
|
+
def fetch_releases(self) -> List[Dict[str, Any]]:
|
|
191
|
+
"""
|
|
192
|
+
Fetch releases for the repository.
|
|
193
|
+
Not applicable for local repositories.
|
|
194
|
+
|
|
195
|
+
Raises:
|
|
196
|
+
NotImplementedError: Always, since release fetching is not supported for LocalFetcher.
|
|
197
|
+
"""
|
|
198
|
+
raise NotImplementedError(
|
|
199
|
+
"Release fetching is not supported for local repositories (LocalFetcher)."
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
def get_branches(self) -> List[str]:
|
|
203
|
+
"""
|
|
204
|
+
Get all branches in the local repository.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
List[str]: List of branch names (both local and remote).
|
|
208
|
+
"""
|
|
209
|
+
try:
|
|
210
|
+
# Get local branches
|
|
211
|
+
result = subprocess.run(
|
|
212
|
+
["git", "-C", self.repo_path, "branch", "--format=%(refname:short)"],
|
|
213
|
+
capture_output=True,
|
|
214
|
+
text=True,
|
|
215
|
+
check=True
|
|
216
|
+
)
|
|
217
|
+
branches = [b.strip() for b in result.stdout.splitlines() if b.strip()]
|
|
218
|
+
|
|
219
|
+
# Get remote branches
|
|
220
|
+
result_remote = subprocess.run(
|
|
221
|
+
["git", "-C", self.repo_path, "branch", "-r", "--format=%(refname:short)"],
|
|
222
|
+
capture_output=True,
|
|
223
|
+
text=True,
|
|
224
|
+
check=True
|
|
225
|
+
)
|
|
226
|
+
remote_branches = [
|
|
227
|
+
b.strip() for b in result_remote.stdout.splitlines()
|
|
228
|
+
if b.strip() and not b.endswith('/HEAD')
|
|
229
|
+
]
|
|
230
|
+
|
|
231
|
+
return branches + remote_branches
|
|
232
|
+
except subprocess.CalledProcessError:
|
|
233
|
+
return []
|
|
234
|
+
|
|
235
|
+
def get_valid_target_branches(self, source_branch: str) -> List[str]:
|
|
236
|
+
"""
|
|
237
|
+
Get branches that can receive a pull request from the source branch.
|
|
238
|
+
Not applicable for local repositories.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
source_branch (str): The source branch name.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
List[str]: Empty list (PRs are platform-specific).
|
|
245
|
+
|
|
246
|
+
Raises:
|
|
247
|
+
NotImplementedError: Always, since PR validation is not supported for LocalFetcher.
|
|
248
|
+
"""
|
|
249
|
+
raise NotImplementedError(
|
|
250
|
+
"Pull request target branch validation is not supported for local repositories (LocalFetcher)."
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
def create_pull_request(
|
|
254
|
+
self,
|
|
255
|
+
head_branch: str,
|
|
256
|
+
base_branch: str,
|
|
257
|
+
title: str,
|
|
258
|
+
body: str,
|
|
259
|
+
draft: bool = False,
|
|
260
|
+
reviewers: Optional[List[str]] = None,
|
|
261
|
+
assignees: Optional[List[str]] = None,
|
|
262
|
+
labels: Optional[List[str]] = None
|
|
263
|
+
) -> Dict[str, Any]:
|
|
264
|
+
"""
|
|
265
|
+
Create a pull request between two branches.
|
|
266
|
+
Not applicable for local repositories.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
head_branch: Source branch for the PR.
|
|
270
|
+
base_branch: Target branch for the PR.
|
|
271
|
+
title: PR title.
|
|
272
|
+
body: PR description.
|
|
273
|
+
draft: Whether to create as draft PR (default: False).
|
|
274
|
+
reviewers: List of reviewer usernames (optional).
|
|
275
|
+
assignees: List of assignee usernames (optional).
|
|
276
|
+
labels: List of label names (optional).
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
Dict[str, Any]: Empty dict (PRs are platform-specific).
|
|
280
|
+
|
|
281
|
+
Raises:
|
|
282
|
+
NotImplementedError: Always, since PR creation is not supported for LocalFetcher.
|
|
283
|
+
"""
|
|
284
|
+
raise NotImplementedError(
|
|
285
|
+
"Pull request creation is not supported for local repositories (LocalFetcher)."
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
def get_authors(self, repo_names: List[str]) -> List[Dict[str, str]]:
|
|
289
|
+
"""
|
|
290
|
+
Retrieve unique authors from the local repository using git log.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
repo_names: Not used for local fetcher (single repo only).
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
List[Dict[str, str]]: List of unique author dictionaries with name and email.
|
|
297
|
+
"""
|
|
298
|
+
authors_set = set()
|
|
299
|
+
|
|
300
|
+
try:
|
|
301
|
+
# Get authors from commit history
|
|
302
|
+
cmd = [
|
|
303
|
+
'git', '-C', self.repo_path, 'log',
|
|
304
|
+
'--all',
|
|
305
|
+
'--format=%an|%ae'
|
|
306
|
+
]
|
|
307
|
+
|
|
308
|
+
result = subprocess.run(
|
|
309
|
+
cmd,
|
|
310
|
+
capture_output=True,
|
|
311
|
+
text=True,
|
|
312
|
+
check=True
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
for line in result.stdout.strip().split('\n'):
|
|
316
|
+
if '|' in line:
|
|
317
|
+
name, email = line.split('|', 1)
|
|
318
|
+
authors_set.add((name.strip(), email.strip()))
|
|
319
|
+
|
|
320
|
+
# Also get committers
|
|
321
|
+
cmd_committer = [
|
|
322
|
+
'git', '-C', self.repo_path, 'log',
|
|
323
|
+
'--all',
|
|
324
|
+
'--format=%cn|%ce'
|
|
325
|
+
]
|
|
326
|
+
|
|
327
|
+
result_committer = subprocess.run(
|
|
328
|
+
cmd_committer,
|
|
329
|
+
capture_output=True,
|
|
330
|
+
text=True,
|
|
331
|
+
check=True
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
for line in result_committer.stdout.strip().split('\n'):
|
|
335
|
+
if '|' in line:
|
|
336
|
+
name, email = line.split('|', 1)
|
|
337
|
+
authors_set.add((name.strip(), email.strip()))
|
|
338
|
+
|
|
339
|
+
authors_list = [
|
|
340
|
+
{"name": name, "email": email}
|
|
341
|
+
for name, email in sorted(authors_set)
|
|
342
|
+
]
|
|
343
|
+
|
|
344
|
+
return authors_list
|
|
345
|
+
|
|
346
|
+
except subprocess.CalledProcessError as e:
|
|
347
|
+
print(f"Git command failed: {e}")
|
|
348
|
+
return []
|
|
349
|
+
except Exception as e:
|
|
350
|
+
print(f"Error in get_authors: {e}")
|
|
351
|
+
return []
|
|
352
|
+
|
|
353
|
+
def get_current_author(self) -> Optional[Dict[str, str]]:
|
|
354
|
+
"""
|
|
355
|
+
Retrieve the current git user's information from local configuration.
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
Optional[Dict[str, str]]: Dictionary with 'name' and 'email' keys,
|
|
359
|
+
or None if not configured.
|
|
360
|
+
"""
|
|
361
|
+
try:
|
|
362
|
+
# Get user name
|
|
363
|
+
result_name = subprocess.run(
|
|
364
|
+
["git", "-C", self.repo_path, "config", "user.name"],
|
|
365
|
+
capture_output=True,
|
|
366
|
+
text=True,
|
|
367
|
+
check=True
|
|
368
|
+
)
|
|
369
|
+
user_name = result_name.stdout.strip()
|
|
370
|
+
|
|
371
|
+
# Get user email
|
|
372
|
+
result_email = subprocess.run(
|
|
373
|
+
["git", "-C", self.repo_path, "config", "user.email"],
|
|
374
|
+
capture_output=True,
|
|
375
|
+
text=True,
|
|
376
|
+
check=True
|
|
377
|
+
)
|
|
378
|
+
user_email = result_email.stdout.strip()
|
|
379
|
+
|
|
380
|
+
if user_name and user_email:
|
|
381
|
+
return {
|
|
382
|
+
"name": user_name,
|
|
383
|
+
"email": user_email
|
|
384
|
+
}
|
|
385
|
+
return None
|
|
386
|
+
except subprocess.CalledProcessError:
|
|
387
|
+
return None
|
|
388
|
+
except Exception as e:
|
|
389
|
+
print(f"Error retrieving current author: {e}")
|
|
390
|
+
return None
|