git-recap 0.1.4__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 +150 -16
- git_recap/providers/base_fetcher.py +96 -0
- git_recap/providers/github_fetcher.py +309 -10
- git_recap/providers/gitlab_fetcher.py +131 -4
- git_recap/providers/local_fetcher.py +390 -0
- git_recap/providers/url_fetcher.py +147 -14
- {git_recap-0.1.4.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.4.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.4.dist-info/RECORD +0 -14
- {git_recap-0.1.4.dist-info → git_recap-0.1.6.dist-info}/licenses/LICENSE +0 -0
- {git_recap-0.1.4.dist-info → git_recap-0.1.6.dist-info}/top_level.txt +0 -0
|
@@ -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
|
|
@@ -2,7 +2,6 @@ import os
|
|
|
2
2
|
import re
|
|
3
3
|
import shutil
|
|
4
4
|
import subprocess
|
|
5
|
-
from pathlib import Path
|
|
6
5
|
import tempfile
|
|
7
6
|
from typing import List, Dict, Any, Optional
|
|
8
7
|
from datetime import datetime
|
|
@@ -36,7 +35,7 @@ class URLFetcher(BaseFetcher):
|
|
|
36
35
|
)
|
|
37
36
|
self.url = self._normalize_url(url)
|
|
38
37
|
self.temp_dir = None
|
|
39
|
-
|
|
38
|
+
self.repo_path = None
|
|
40
39
|
self._clone_repo()
|
|
41
40
|
|
|
42
41
|
def _normalize_url(self, url: str) -> str:
|
|
@@ -59,7 +58,7 @@ class URLFetcher(BaseFetcher):
|
|
|
59
58
|
capture_output=True,
|
|
60
59
|
text=True,
|
|
61
60
|
check=True,
|
|
62
|
-
timeout=10
|
|
61
|
+
timeout=10
|
|
63
62
|
)
|
|
64
63
|
if not result.stdout.strip():
|
|
65
64
|
raise ValueError(f"URL {self.url} points to an empty repository")
|
|
@@ -71,8 +70,8 @@ class URLFetcher(BaseFetcher):
|
|
|
71
70
|
def _clone_repo(self) -> None:
|
|
72
71
|
"""Clone the repository to a temporary directory with all branches."""
|
|
73
72
|
self.temp_dir = tempfile.mkdtemp(prefix="gitrecap_")
|
|
73
|
+
self.repo_path = self.temp_dir
|
|
74
74
|
try:
|
|
75
|
-
# First clone with --no-checkout to save bandwidth
|
|
76
75
|
subprocess.run(
|
|
77
76
|
["git", "clone", "--no-checkout", self.url, self.temp_dir],
|
|
78
77
|
check=True,
|
|
@@ -81,7 +80,6 @@ class URLFetcher(BaseFetcher):
|
|
|
81
80
|
timeout=300
|
|
82
81
|
)
|
|
83
82
|
|
|
84
|
-
# Fetch all branches
|
|
85
83
|
subprocess.run(
|
|
86
84
|
["git", "-C", self.temp_dir, "fetch", "--all"],
|
|
87
85
|
check=True,
|
|
@@ -90,7 +88,6 @@ class URLFetcher(BaseFetcher):
|
|
|
90
88
|
timeout=300
|
|
91
89
|
)
|
|
92
90
|
|
|
93
|
-
# Verify the cloned repository has at least one commit
|
|
94
91
|
verify_result = subprocess.run(
|
|
95
92
|
["git", "-C", self.temp_dir, "rev-list", "--count", "--all"],
|
|
96
93
|
capture_output=True,
|
|
@@ -138,7 +135,6 @@ class URLFetcher(BaseFetcher):
|
|
|
138
135
|
check=True
|
|
139
136
|
)
|
|
140
137
|
branches = [b.strip() for b in result.stdout.splitlines() if b.strip()]
|
|
141
|
-
# Filter out HEAD reference if present
|
|
142
138
|
return [b for b in branches if not b.endswith('/HEAD')]
|
|
143
139
|
except subprocess.CalledProcessError:
|
|
144
140
|
return []
|
|
@@ -154,7 +150,7 @@ class URLFetcher(BaseFetcher):
|
|
|
154
150
|
"log",
|
|
155
151
|
"--pretty=format:%H|%an|%ad|%s",
|
|
156
152
|
"--date=iso",
|
|
157
|
-
"--all"
|
|
153
|
+
"--all"
|
|
158
154
|
]
|
|
159
155
|
|
|
160
156
|
if self.start_date:
|
|
@@ -173,7 +169,7 @@ class URLFetcher(BaseFetcher):
|
|
|
173
169
|
capture_output=True,
|
|
174
170
|
text=True,
|
|
175
171
|
check=True,
|
|
176
|
-
timeout=120
|
|
172
|
+
timeout=120
|
|
177
173
|
)
|
|
178
174
|
return self._parse_git_log(result.stdout)
|
|
179
175
|
except subprocess.TimeoutExpired:
|
|
@@ -206,7 +202,7 @@ class URLFetcher(BaseFetcher):
|
|
|
206
202
|
"timestamp": timestamp
|
|
207
203
|
})
|
|
208
204
|
except ValueError:
|
|
209
|
-
continue
|
|
205
|
+
continue
|
|
210
206
|
|
|
211
207
|
return entries
|
|
212
208
|
|
|
@@ -229,16 +225,153 @@ class URLFetcher(BaseFetcher):
|
|
|
229
225
|
Raises:
|
|
230
226
|
NotImplementedError: Always, since release fetching is not supported for URLFetcher.
|
|
231
227
|
"""
|
|
232
|
-
# If in the future, support for fetching releases from generic git repos is added,
|
|
233
|
-
# implement logic here (e.g., parse tags and annotate with metadata).
|
|
234
228
|
raise NotImplementedError("Release fetching is not supported for generic Git URLs (URLFetcher).")
|
|
235
229
|
|
|
230
|
+
def get_branches(self) -> List[str]:
|
|
231
|
+
"""
|
|
232
|
+
Get all branches in the repository.
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
List[str]: List of branch names.
|
|
236
|
+
|
|
237
|
+
Raises:
|
|
238
|
+
NotImplementedError: Always, since branch listing is not yet implemented for URLFetcher.
|
|
239
|
+
"""
|
|
240
|
+
raise NotImplementedError("Branch listing is not yet implemented for generic Git URLs (URLFetcher).")
|
|
241
|
+
|
|
242
|
+
def get_valid_target_branches(self, source_branch: str) -> List[str]:
|
|
243
|
+
"""
|
|
244
|
+
Get branches that can receive a pull request from the source branch.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
source_branch (str): The source branch name.
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
List[str]: List of valid target branch names.
|
|
251
|
+
|
|
252
|
+
Raises:
|
|
253
|
+
NotImplementedError: Always, since PR target validation is not supported for URLFetcher.
|
|
254
|
+
"""
|
|
255
|
+
raise NotImplementedError("Pull request target branch validation is not supported for generic Git URLs (URLFetcher).")
|
|
256
|
+
|
|
257
|
+
def create_pull_request(
|
|
258
|
+
self,
|
|
259
|
+
head_branch: str,
|
|
260
|
+
base_branch: str,
|
|
261
|
+
title: str,
|
|
262
|
+
body: str,
|
|
263
|
+
draft: bool = False,
|
|
264
|
+
reviewers: Optional[List[str]] = None,
|
|
265
|
+
assignees: Optional[List[str]] = None,
|
|
266
|
+
labels: Optional[List[str]] = None
|
|
267
|
+
) -> Dict[str, Any]:
|
|
268
|
+
"""
|
|
269
|
+
Create a pull request between two branches.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
head_branch: Source branch for the PR.
|
|
273
|
+
base_branch: Target branch for the PR.
|
|
274
|
+
title: PR title.
|
|
275
|
+
body: PR description.
|
|
276
|
+
draft: Whether to create as draft PR (default: False).
|
|
277
|
+
reviewers: List of reviewer usernames (optional).
|
|
278
|
+
assignees: List of assignee usernames (optional).
|
|
279
|
+
labels: List of label names (optional).
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
Dict[str, Any]: Dictionary containing PR metadata or error information.
|
|
283
|
+
|
|
284
|
+
Raises:
|
|
285
|
+
NotImplementedError: Always, since PR creation is not supported for URLFetcher.
|
|
286
|
+
"""
|
|
287
|
+
raise NotImplementedError("Pull request creation is not supported for generic Git URLs (URLFetcher).")
|
|
288
|
+
|
|
289
|
+
def get_authors(self, repo_names: List[str]) -> List[Dict[str, str]]:
|
|
290
|
+
"""
|
|
291
|
+
Retrieve unique authors from cloned repository using git log.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
repo_names: Not used for URL fetcher (single repo only).
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
List of unique author dictionaries with name and email.
|
|
298
|
+
"""
|
|
299
|
+
authors_set = set()
|
|
300
|
+
|
|
301
|
+
try:
|
|
302
|
+
if not hasattr(self, 'repo_path') or not os.path.exists(self.repo_path):
|
|
303
|
+
print("Repository not cloned yet")
|
|
304
|
+
return []
|
|
305
|
+
|
|
306
|
+
cmd = [
|
|
307
|
+
'git', '-C', self.repo_path, 'log',
|
|
308
|
+
'--all',
|
|
309
|
+
'--format=%an|%ae'
|
|
310
|
+
]
|
|
311
|
+
|
|
312
|
+
result = subprocess.run(
|
|
313
|
+
cmd,
|
|
314
|
+
capture_output=True,
|
|
315
|
+
text=True,
|
|
316
|
+
check=True
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
for line in result.stdout.strip().split('\n'):
|
|
320
|
+
if '|' in line:
|
|
321
|
+
name, email = line.split('|', 1)
|
|
322
|
+
authors_set.add((name.strip(), email.strip()))
|
|
323
|
+
|
|
324
|
+
cmd_committer = [
|
|
325
|
+
'git', '-C', self.repo_path, 'log',
|
|
326
|
+
'--all',
|
|
327
|
+
'--format=%cn|%ce'
|
|
328
|
+
]
|
|
329
|
+
|
|
330
|
+
result_committer = subprocess.run(
|
|
331
|
+
cmd_committer,
|
|
332
|
+
capture_output=True,
|
|
333
|
+
text=True,
|
|
334
|
+
check=True
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
for line in result_committer.stdout.strip().split('\n'):
|
|
338
|
+
if '|' in line:
|
|
339
|
+
name, email = line.split('|', 1)
|
|
340
|
+
authors_set.add((name.strip(), email.strip()))
|
|
341
|
+
|
|
342
|
+
authors_list = [
|
|
343
|
+
{"name": name, "email": email}
|
|
344
|
+
for name, email in sorted(authors_set)
|
|
345
|
+
]
|
|
346
|
+
|
|
347
|
+
return authors_list
|
|
348
|
+
|
|
349
|
+
except subprocess.CalledProcessError as e:
|
|
350
|
+
print(f"Git command failed: {e}")
|
|
351
|
+
return []
|
|
352
|
+
except Exception as e:
|
|
353
|
+
print(f"Error in get_authors: {e}")
|
|
354
|
+
return []
|
|
355
|
+
|
|
356
|
+
def get_current_author(self) -> Optional[Dict[str, str]]:
|
|
357
|
+
"""
|
|
358
|
+
Retrieve the current authenticated user's information.
|
|
359
|
+
|
|
360
|
+
For URL-based cloning, there is no authenticated user context,
|
|
361
|
+
so this method always returns None.
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
None: URL fetcher has no default author.
|
|
365
|
+
"""
|
|
366
|
+
return None
|
|
367
|
+
|
|
236
368
|
def clear(self) -> None:
|
|
237
369
|
"""Clean up temporary directory."""
|
|
238
370
|
if self.temp_dir and os.path.exists(self.temp_dir):
|
|
239
371
|
try:
|
|
240
372
|
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
|
241
373
|
except Exception:
|
|
242
|
-
pass
|
|
374
|
+
pass
|
|
243
375
|
finally:
|
|
244
|
-
self.temp_dir = None
|
|
376
|
+
self.temp_dir = None
|
|
377
|
+
self.repo_path = None
|
|
@@ -1,25 +1,30 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: git-recap
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
4
4
|
Summary: A modular Python tool that aggregates and formats user-authored messages from repositories.
|
|
5
|
-
Author: Bruno V.
|
|
6
|
-
|
|
5
|
+
Author-email: "Bruno V." <bruno.vitorino@tecnico.ulisboa.pt>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/BrunoV21/GitRecap
|
|
8
|
+
Project-URL: Documentation, https://github.com/BrunoV21/GitRecap#readme
|
|
9
|
+
Project-URL: Repository, https://github.com/BrunoV21/GitRecap.git
|
|
10
|
+
Project-URL: Issues, https://github.com/BrunoV21/GitRecap/issues
|
|
11
|
+
Keywords: git,github,gitlab,azure-devops,version-control,repository
|
|
7
12
|
Classifier: Programming Language :: Python :: 3
|
|
8
13
|
Classifier: License :: OSI Approved :: MIT License
|
|
9
14
|
Classifier: Operating System :: OS Independent
|
|
15
|
+
Requires-Python: >=3.10
|
|
10
16
|
Description-Content-Type: text/markdown
|
|
11
17
|
License-File: LICENSE
|
|
12
18
|
Requires-Dist: PyGithub==2.6.1
|
|
13
19
|
Requires-Dist: azure-devops==7.1.0b4
|
|
14
20
|
Requires-Dist: python-gitlab==5.6.0
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
23
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
24
|
+
Requires-Dist: black>=22.0.0; extra == "dev"
|
|
25
|
+
Requires-Dist: flake8>=5.0.0; extra == "dev"
|
|
26
|
+
Requires-Dist: mypy>=0.990; extra == "dev"
|
|
20
27
|
Dynamic: license-file
|
|
21
|
-
Dynamic: requires-dist
|
|
22
|
-
Dynamic: summary
|
|
23
28
|
|
|
24
29
|
<a href="https://www.uneed.best/tool/gitrecap">
|
|
25
30
|
<img src="https://www.uneed.best/POTD2A.png" style="width: 250px;" alt="Uneed POTD2 Badge" />
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
git_recap/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
git_recap/cli.py,sha256=oWSHWiIW1I6XyxyzDRVyMukCKs467KaIgDJzg40IsAg,8530
|
|
3
|
+
git_recap/fetcher.py,sha256=7_EZ3Af5jP_dldPbGrgpT-Gzmc1gg1NDxY2TfRuAnyc,3552
|
|
4
|
+
git_recap/utils.py,sha256=NAG0cvm1sYKEQD5E9PueduB3CZQRTahrioxZNgwwmi4,5481
|
|
5
|
+
git_recap/providers/__init__.py,sha256=pGAdkLeklLMD8feL7gZvEeHvsSvZlGpezbPOPKsqH3o,409
|
|
6
|
+
git_recap/providers/azure_fetcher.py,sha256=_hAgt1Za-uGkRuve7-FKq_bQ9-nAlLexD0GsgTBl_1k,14675
|
|
7
|
+
git_recap/providers/base_fetcher.py,sha256=ZANa-vdSpI_wDQ2U0QillSx6V4ljqxuFBn9bjSqq9bg,8633
|
|
8
|
+
git_recap/providers/github_fetcher.py,sha256=n87yYlYmsUyi3O0usqXyYP4ZJ-x3FFE-o9e_tLjDGv0,21638
|
|
9
|
+
git_recap/providers/gitlab_fetcher.py,sha256=IHC4CE3E_0ri2AhS7a5DPesOrSl5YNcP_hYBHzbIZGo,12376
|
|
10
|
+
git_recap/providers/local_fetcher.py,sha256=oCeddwzE7cJzMLCPWCOFRkNqvH45EM2KxxsOg3W5LME,12781
|
|
11
|
+
git_recap/providers/url_fetcher.py,sha256=pSd2PSeYpPsHdws6LsQ3ruZdDd9rz5LN6U7dxF_eHLU,12939
|
|
12
|
+
git_recap-0.1.6.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
13
|
+
git_recap-0.1.6.dist-info/METADATA,sha256=jXU579yPa9QJHb0s0dII9P35FxYI7tkgmUetuGkAZF8,5404
|
|
14
|
+
git_recap-0.1.6.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
15
|
+
git_recap-0.1.6.dist-info/entry_points.txt,sha256=28MZTffgA2q41JRbRHe1TQ-u1XtsyJK021ThKbp0bSU,49
|
|
16
|
+
git_recap-0.1.6.dist-info/top_level.txt,sha256=1JUKd3WPB8c3LcD1deIW-1UTmYzA0zJqwugAz72YZ_o,10
|
|
17
|
+
git_recap-0.1.6.dist-info/RECORD,,
|