git-recap 0.1.2__py3-none-any.whl → 0.1.4__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/providers/azure_fetcher.py +80 -4
- git_recap/providers/base_fetcher.py +64 -8
- git_recap/providers/github_fetcher.py +61 -4
- git_recap/providers/gitlab_fetcher.py +63 -5
- git_recap/providers/url_fetcher.py +28 -16
- git_recap/utils.py +91 -37
- {git_recap-0.1.2.dist-info → git_recap-0.1.4.dist-info}/METADATA +9 -1
- git_recap-0.1.4.dist-info/RECORD +14 -0
- {git_recap-0.1.2.dist-info → git_recap-0.1.4.dist-info}/WHEEL +1 -1
- git_recap-0.1.2.dist-info/RECORD +0 -14
- {git_recap-0.1.2.dist-info → git_recap-0.1.4.dist-info}/licenses/LICENSE +0 -0
- {git_recap-0.1.2.dist-info → git_recap-0.1.4.dist-info}/top_level.txt +0 -0
|
@@ -5,8 +5,25 @@ from typing import List, Dict, Any
|
|
|
5
5
|
from git_recap.providers.base_fetcher import BaseFetcher
|
|
6
6
|
|
|
7
7
|
class AzureFetcher(BaseFetcher):
|
|
8
|
+
"""
|
|
9
|
+
Fetcher implementation for Azure DevOps repositories.
|
|
10
|
+
|
|
11
|
+
Supports fetching commits, pull requests, and issues.
|
|
12
|
+
Release fetching is not supported and will raise NotImplementedError.
|
|
13
|
+
"""
|
|
14
|
+
|
|
8
15
|
def __init__(self, pat: str, organization_url: str, start_date=None, end_date=None, repo_filter=None, authors=None):
|
|
9
|
-
|
|
16
|
+
"""
|
|
17
|
+
Initialize the AzureFetcher.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
pat (str): Personal Access Token for Azure DevOps.
|
|
21
|
+
organization_url (str): The Azure DevOps organization URL.
|
|
22
|
+
start_date (datetime, optional): Start date for filtering entries.
|
|
23
|
+
end_date (datetime, optional): End date for filtering entries.
|
|
24
|
+
repo_filter (List[str], optional): List of repository names to filter.
|
|
25
|
+
authors (List[str], optional): List of author identifiers (e.g., email or unique id).
|
|
26
|
+
"""
|
|
10
27
|
super().__init__(pat, start_date, end_date, repo_filter, authors)
|
|
11
28
|
self.organization_url = organization_url
|
|
12
29
|
credentials = BasicAuthentication('', self.pat)
|
|
@@ -20,17 +37,37 @@ class AzureFetcher(BaseFetcher):
|
|
|
20
37
|
self.authors = []
|
|
21
38
|
|
|
22
39
|
def get_repos(self):
|
|
40
|
+
"""
|
|
41
|
+
Retrieve all repositories in all projects for the organization.
|
|
42
|
+
Returns:
|
|
43
|
+
List of repository objects.
|
|
44
|
+
"""
|
|
23
45
|
projects = self.core_client.get_projects().value
|
|
24
46
|
# Get all repositories in each project
|
|
25
47
|
repos = [self.git_client.get_repositories(project.id) for project in projects]
|
|
26
48
|
return repos
|
|
27
49
|
|
|
28
50
|
@property
|
|
29
|
-
def repos_names(self)->List[str]:
|
|
30
|
-
"
|
|
51
|
+
def repos_names(self) -> List[str]:
|
|
52
|
+
"""
|
|
53
|
+
Return the list of repository names.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
List[str]: List of repository names.
|
|
57
|
+
"""
|
|
58
|
+
# To be implemented if needed for UI or listing.
|
|
31
59
|
...
|
|
32
60
|
|
|
33
61
|
def _filter_by_date(self, date_obj: datetime) -> bool:
|
|
62
|
+
"""
|
|
63
|
+
Check if a datetime object is within the configured date range.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
date_obj (datetime): The datetime to check.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
bool: True if within range, False otherwise.
|
|
70
|
+
"""
|
|
34
71
|
if self.start_date and date_obj < self.start_date:
|
|
35
72
|
return False
|
|
36
73
|
if self.end_date and date_obj > self.end_date:
|
|
@@ -38,11 +75,26 @@ class AzureFetcher(BaseFetcher):
|
|
|
38
75
|
return True
|
|
39
76
|
|
|
40
77
|
def _stop_fetching(self, date_obj: datetime) -> bool:
|
|
78
|
+
"""
|
|
79
|
+
Determine if fetching should stop based on the date.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
date_obj (datetime): The datetime to check.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
bool: True if should stop, False otherwise.
|
|
86
|
+
"""
|
|
41
87
|
if self.start_date and date_obj < self.start_date:
|
|
42
88
|
return True
|
|
43
89
|
return False
|
|
44
90
|
|
|
45
91
|
def fetch_commits(self) -> List[Dict[str, Any]]:
|
|
92
|
+
"""
|
|
93
|
+
Fetch commits for all repositories and authors.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
List[Dict[str, Any]]: List of commit entries.
|
|
97
|
+
"""
|
|
46
98
|
entries = []
|
|
47
99
|
processed_commits = set()
|
|
48
100
|
for repo in self.repos:
|
|
@@ -77,6 +129,12 @@ class AzureFetcher(BaseFetcher):
|
|
|
77
129
|
return entries
|
|
78
130
|
|
|
79
131
|
def fetch_pull_requests(self) -> List[Dict[str, Any]]:
|
|
132
|
+
"""
|
|
133
|
+
Fetch pull requests and their associated commits for all repositories and authors.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
List[Dict[str, Any]]: List of pull request and commit_from_pr entries.
|
|
137
|
+
"""
|
|
80
138
|
entries = []
|
|
81
139
|
processed_pr_commits = set()
|
|
82
140
|
projects = self.core_client.get_projects().value
|
|
@@ -138,6 +196,12 @@ class AzureFetcher(BaseFetcher):
|
|
|
138
196
|
return entries
|
|
139
197
|
|
|
140
198
|
def fetch_issues(self) -> List[Dict[str, Any]]:
|
|
199
|
+
"""
|
|
200
|
+
Fetch issues (work items) assigned to the configured authors.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
List[Dict[str, Any]]: List of issue entries.
|
|
204
|
+
"""
|
|
141
205
|
entries = []
|
|
142
206
|
wit_client = self.connection.clients.get_work_item_tracking_client()
|
|
143
207
|
# Query work items for each author using a simplified WIQL query.
|
|
@@ -160,4 +224,16 @@ class AzureFetcher(BaseFetcher):
|
|
|
160
224
|
entries.append(entry)
|
|
161
225
|
if self._stop_fetching(created_date):
|
|
162
226
|
break
|
|
163
|
-
return entries
|
|
227
|
+
return entries
|
|
228
|
+
|
|
229
|
+
def fetch_releases(self) -> List[Dict[str, Any]]:
|
|
230
|
+
"""
|
|
231
|
+
Fetch releases for Azure DevOps repositories.
|
|
232
|
+
|
|
233
|
+
Not implemented for Azure DevOps.
|
|
234
|
+
|
|
235
|
+
Raises:
|
|
236
|
+
NotImplementedError: Always, since release fetching is not supported for AzureFetcher.
|
|
237
|
+
"""
|
|
238
|
+
# If Azure DevOps release fetching is supported in the future, implement logic here.
|
|
239
|
+
raise NotImplementedError("Release fetching is not supported for Azure DevOps (AzureFetcher).")
|
|
@@ -9,7 +9,7 @@ class BaseFetcher(ABC):
|
|
|
9
9
|
start_date: Optional[datetime] = None,
|
|
10
10
|
end_date: Optional[datetime] = None,
|
|
11
11
|
repo_filter: Optional[List[str]] = None,
|
|
12
|
-
authors: Optional[List[str]]=None
|
|
12
|
+
authors: Optional[List[str]] = None
|
|
13
13
|
):
|
|
14
14
|
self.pat = pat
|
|
15
15
|
if start_date is not None:
|
|
@@ -28,32 +28,82 @@ class BaseFetcher(ABC):
|
|
|
28
28
|
|
|
29
29
|
@property
|
|
30
30
|
@abstractmethod
|
|
31
|
-
def repos_names(self)->List[str]:
|
|
31
|
+
def repos_names(self) -> List[str]:
|
|
32
|
+
"""
|
|
33
|
+
Return the list of repository names accessible to this fetcher.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
List[str]: List of repository names.
|
|
37
|
+
"""
|
|
32
38
|
pass
|
|
33
|
-
|
|
39
|
+
|
|
34
40
|
@abstractmethod
|
|
35
|
-
def fetch_commits(self) -> List[str]:
|
|
41
|
+
def fetch_commits(self) -> List[Dict[str, Any]]:
|
|
42
|
+
"""
|
|
43
|
+
Fetch commit entries for the configured repositories and authors.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
List[Dict[str, Any]]: List of commit entries.
|
|
47
|
+
"""
|
|
36
48
|
pass
|
|
37
49
|
|
|
38
50
|
@abstractmethod
|
|
39
|
-
def fetch_pull_requests(self) -> List[str]:
|
|
51
|
+
def fetch_pull_requests(self) -> List[Dict[str, Any]]:
|
|
52
|
+
"""
|
|
53
|
+
Fetch pull request entries for the configured repositories and authors.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
List[Dict[str, Any]]: List of pull request entries.
|
|
57
|
+
"""
|
|
40
58
|
pass
|
|
41
59
|
|
|
42
60
|
@abstractmethod
|
|
43
|
-
def fetch_issues(self) -> List[str]:
|
|
61
|
+
def fetch_issues(self) -> List[Dict[str, Any]]:
|
|
62
|
+
"""
|
|
63
|
+
Fetch issue entries for the configured repositories and authors.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
List[Dict[str, Any]]: List of issue entries.
|
|
67
|
+
"""
|
|
44
68
|
pass
|
|
45
69
|
|
|
70
|
+
@abstractmethod
|
|
71
|
+
def fetch_releases(self) -> List[Dict[str, Any]]:
|
|
72
|
+
"""
|
|
73
|
+
Fetch releases for all repositories accessible to this fetcher.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
List[Dict[str, Any]]: List of releases, each as a structured dictionary.
|
|
77
|
+
The dictionary should include at least:
|
|
78
|
+
- tag_name: str
|
|
79
|
+
- name: str
|
|
80
|
+
- repo: str
|
|
81
|
+
- author: str
|
|
82
|
+
- published_at: datetime
|
|
83
|
+
- created_at: datetime
|
|
84
|
+
- draft: bool
|
|
85
|
+
- prerelease: bool
|
|
86
|
+
- body: str
|
|
87
|
+
- assets: List[Dict[str, Any]] (if available)
|
|
88
|
+
Raises:
|
|
89
|
+
NotImplementedError: If the provider does not support release fetching.
|
|
90
|
+
"""
|
|
91
|
+
raise NotImplementedError("Release fetching is not implemented for this provider.")
|
|
92
|
+
|
|
46
93
|
def get_authored_messages(self) -> List[Dict[str, Any]]:
|
|
47
94
|
"""
|
|
48
95
|
Aggregates all commit, pull request, and issue entries into a single list,
|
|
49
96
|
ensuring no duplicate commits (based on SHA) are present, and then sorts
|
|
50
97
|
them in chronological order based on their timestamp.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
List[Dict[str, Any]]: Aggregated and sorted list of entries.
|
|
51
101
|
"""
|
|
52
102
|
commit_entries = self.fetch_commits()
|
|
53
103
|
pr_entries = self.fetch_pull_requests()
|
|
54
104
|
try:
|
|
55
105
|
issue_entries = self.fetch_issues()
|
|
56
|
-
except Exception
|
|
106
|
+
except Exception:
|
|
57
107
|
issue_entries = []
|
|
58
108
|
|
|
59
109
|
all_entries = pr_entries + commit_entries + issue_entries
|
|
@@ -75,11 +125,17 @@ class BaseFetcher(ABC):
|
|
|
75
125
|
# Sort all entries by their timestamp.
|
|
76
126
|
final_entries.sort(key=lambda x: x["timestamp"])
|
|
77
127
|
return self.convert_timestamps_to_str(final_entries)
|
|
78
|
-
|
|
128
|
+
|
|
79
129
|
@staticmethod
|
|
80
130
|
def convert_timestamps_to_str(entries: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
81
131
|
"""
|
|
82
132
|
Converts the timestamp field from datetime to string format for each entry in the list.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
entries (List[Dict[str, Any]]): List of entries with possible datetime timestamps.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
List[Dict[str, Any]]: Entries with timestamps as ISO-formatted strings.
|
|
83
139
|
"""
|
|
84
140
|
for entry in entries:
|
|
85
141
|
if isinstance(entry.get("timestamp"), datetime):
|
|
@@ -4,15 +4,21 @@ from typing import List, Dict, Any
|
|
|
4
4
|
from git_recap.providers.base_fetcher import BaseFetcher
|
|
5
5
|
|
|
6
6
|
class GitHubFetcher(BaseFetcher):
|
|
7
|
+
"""
|
|
8
|
+
Fetcher implementation for GitHub repositories.
|
|
9
|
+
|
|
10
|
+
Supports fetching commits, pull requests, issues, and releases.
|
|
11
|
+
"""
|
|
12
|
+
|
|
7
13
|
def __init__(self, pat: str, start_date=None, end_date=None, repo_filter=None, authors=None):
|
|
8
14
|
super().__init__(pat, start_date, end_date, repo_filter, authors)
|
|
9
15
|
self.github = Github(self.pat)
|
|
10
|
-
self.user = self.github.get_user()
|
|
16
|
+
self.user = self.github.get_user()
|
|
11
17
|
self.repos = self.user.get_repos(affiliation="owner,collaborator,organization_member")
|
|
12
18
|
self.authors.append(self.user.login)
|
|
13
19
|
|
|
14
20
|
@property
|
|
15
|
-
def repos_names(self)->List[str]:
|
|
21
|
+
def repos_names(self) -> List[str]:
|
|
16
22
|
return [repo.name for repo in self.repos]
|
|
17
23
|
|
|
18
24
|
def _stop_fetching(self, date_obj: datetime) -> bool:
|
|
@@ -101,7 +107,6 @@ class GitHubFetcher(BaseFetcher):
|
|
|
101
107
|
break
|
|
102
108
|
return entries
|
|
103
109
|
|
|
104
|
-
|
|
105
110
|
def fetch_issues(self) -> List[Dict[str, Any]]:
|
|
106
111
|
entries = []
|
|
107
112
|
issues = self.user.get_issues()
|
|
@@ -117,4 +122,56 @@ class GitHubFetcher(BaseFetcher):
|
|
|
117
122
|
entries.append(entry)
|
|
118
123
|
if self._stop_fetching(issue_date):
|
|
119
124
|
break
|
|
120
|
-
return entries
|
|
125
|
+
return entries
|
|
126
|
+
|
|
127
|
+
def fetch_releases(self) -> List[Dict[str, Any]]:
|
|
128
|
+
"""
|
|
129
|
+
Fetch releases for all repositories accessible to the user.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
List[Dict[str, Any]]: List of releases, each as a structured dictionary with:
|
|
133
|
+
- tag_name: str
|
|
134
|
+
- name: str
|
|
135
|
+
- repo: str
|
|
136
|
+
- author: str
|
|
137
|
+
- published_at: datetime
|
|
138
|
+
- created_at: datetime
|
|
139
|
+
- draft: bool
|
|
140
|
+
- prerelease: bool
|
|
141
|
+
- body: str
|
|
142
|
+
- assets: List[Dict[str, Any]] (each with name, size, download_url, content_type, etc.)
|
|
143
|
+
"""
|
|
144
|
+
releases = []
|
|
145
|
+
for repo in self.repos:
|
|
146
|
+
if self.repo_filter and repo.name not in self.repo_filter:
|
|
147
|
+
continue
|
|
148
|
+
try:
|
|
149
|
+
for rel in repo.get_releases():
|
|
150
|
+
# Compose asset list
|
|
151
|
+
assets = []
|
|
152
|
+
for asset in rel.get_assets():
|
|
153
|
+
assets.append({
|
|
154
|
+
"name": asset.name,
|
|
155
|
+
"size": asset.size,
|
|
156
|
+
"download_url": asset.browser_download_url,
|
|
157
|
+
"content_type": asset.content_type,
|
|
158
|
+
"created_at": asset.created_at,
|
|
159
|
+
"updated_at": asset.updated_at,
|
|
160
|
+
})
|
|
161
|
+
release_entry = {
|
|
162
|
+
"tag_name": rel.tag_name,
|
|
163
|
+
"name": rel.title if hasattr(rel, "title") else rel.name,
|
|
164
|
+
"repo": repo.name,
|
|
165
|
+
"author": rel.author.login if rel.author else None,
|
|
166
|
+
"published_at": rel.published_at,
|
|
167
|
+
"created_at": rel.created_at,
|
|
168
|
+
"draft": rel.draft,
|
|
169
|
+
"prerelease": rel.prerelease,
|
|
170
|
+
"body": rel.body,
|
|
171
|
+
"assets": assets,
|
|
172
|
+
}
|
|
173
|
+
releases.append(release_entry)
|
|
174
|
+
except Exception:
|
|
175
|
+
# If fetching releases fails for a repo, skip it (could be permissions or no releases)
|
|
176
|
+
continue
|
|
177
|
+
return releases
|
|
@@ -4,11 +4,37 @@ from typing import List, Dict, Any
|
|
|
4
4
|
from git_recap.providers.base_fetcher import BaseFetcher
|
|
5
5
|
|
|
6
6
|
class GitLabFetcher(BaseFetcher):
|
|
7
|
-
|
|
7
|
+
"""
|
|
8
|
+
Fetcher implementation for GitLab repositories.
|
|
9
|
+
|
|
10
|
+
Supports fetching commits, merge requests (pull requests), and issues.
|
|
11
|
+
Release fetching is not supported and will raise NotImplementedError.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
pat: str,
|
|
17
|
+
url: str = 'https://gitlab.com',
|
|
18
|
+
start_date=None,
|
|
19
|
+
end_date=None,
|
|
20
|
+
repo_filter=None,
|
|
21
|
+
authors=None
|
|
22
|
+
):
|
|
23
|
+
"""
|
|
24
|
+
Initialize the GitLabFetcher.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
pat (str): Personal Access Token for GitLab.
|
|
28
|
+
url (str): The GitLab instance URL.
|
|
29
|
+
start_date (datetime, optional): Start date for filtering entries.
|
|
30
|
+
end_date (datetime, optional): End date for filtering entries.
|
|
31
|
+
repo_filter (List[str], optional): List of repository names to filter.
|
|
32
|
+
authors (List[str], optional): List of author usernames.
|
|
33
|
+
"""
|
|
8
34
|
super().__init__(pat, start_date, end_date, repo_filter, authors)
|
|
9
35
|
self.gl = gitlab.Gitlab(url, private_token=self.pat)
|
|
10
36
|
self.gl.auth()
|
|
11
|
-
#
|
|
37
|
+
# Retrieve projects where the user is a member.
|
|
12
38
|
self.projects = self.gl.projects.list(membership=True, all=True)
|
|
13
39
|
# Default to the authenticated user's username if no authors are provided.
|
|
14
40
|
if authors is None:
|
|
@@ -17,11 +43,12 @@ class GitLabFetcher(BaseFetcher):
|
|
|
17
43
|
self.authors = authors
|
|
18
44
|
|
|
19
45
|
@property
|
|
20
|
-
def repos_names(self)->List[str]:
|
|
21
|
-
"
|
|
46
|
+
def repos_names(self) -> List[str]:
|
|
47
|
+
"""Return the list of repository names."""
|
|
22
48
|
return [project.name for project in self.projects]
|
|
23
49
|
|
|
24
50
|
def _filter_by_date(self, date_str: str) -> bool:
|
|
51
|
+
"""Check if a date string is within the configured date range."""
|
|
25
52
|
date_obj = datetime.fromisoformat(date_str)
|
|
26
53
|
if self.start_date and date_obj < self.start_date:
|
|
27
54
|
return False
|
|
@@ -30,12 +57,19 @@ class GitLabFetcher(BaseFetcher):
|
|
|
30
57
|
return True
|
|
31
58
|
|
|
32
59
|
def _stop_fetching(self, date_str: str) -> bool:
|
|
60
|
+
"""Determine if fetching should stop based on the date string."""
|
|
33
61
|
date_obj = datetime.fromisoformat(date_str)
|
|
34
62
|
if self.start_date and date_obj < self.start_date:
|
|
35
63
|
return True
|
|
36
64
|
return False
|
|
37
65
|
|
|
38
66
|
def fetch_commits(self) -> List[Dict[str, Any]]:
|
|
67
|
+
"""
|
|
68
|
+
Fetch commits for all projects and authors.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
List[Dict[str, Any]]: List of commit entries.
|
|
72
|
+
"""
|
|
39
73
|
entries = []
|
|
40
74
|
processed_commits = set()
|
|
41
75
|
for project in self.projects:
|
|
@@ -63,6 +97,12 @@ class GitLabFetcher(BaseFetcher):
|
|
|
63
97
|
return entries
|
|
64
98
|
|
|
65
99
|
def fetch_pull_requests(self) -> List[Dict[str, Any]]:
|
|
100
|
+
"""
|
|
101
|
+
Fetch merge requests (pull requests) and their associated commits for all projects and authors.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
List[Dict[str, Any]]: List of pull request and commit_from_pr entries.
|
|
105
|
+
"""
|
|
66
106
|
entries = []
|
|
67
107
|
processed_pr_commits = set()
|
|
68
108
|
for project in self.projects:
|
|
@@ -109,6 +149,12 @@ class GitLabFetcher(BaseFetcher):
|
|
|
109
149
|
return entries
|
|
110
150
|
|
|
111
151
|
def fetch_issues(self) -> List[Dict[str, Any]]:
|
|
152
|
+
"""
|
|
153
|
+
Fetch issues assigned to the authenticated user for all projects.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
List[Dict[str, Any]]: List of issue entries.
|
|
157
|
+
"""
|
|
112
158
|
entries = []
|
|
113
159
|
for project in self.projects:
|
|
114
160
|
if self.repo_filter and project.name not in self.repo_filter:
|
|
@@ -126,4 +172,16 @@ class GitLabFetcher(BaseFetcher):
|
|
|
126
172
|
entries.append(entry)
|
|
127
173
|
if self._stop_fetching(issue_date):
|
|
128
174
|
break
|
|
129
|
-
return entries
|
|
175
|
+
return entries
|
|
176
|
+
|
|
177
|
+
def fetch_releases(self) -> List[Dict[str, Any]]:
|
|
178
|
+
"""
|
|
179
|
+
Fetch releases for GitLab repositories.
|
|
180
|
+
|
|
181
|
+
Not implemented for GitLabFetcher.
|
|
182
|
+
|
|
183
|
+
Raises:
|
|
184
|
+
NotImplementedError: Always, since release fetching is not supported for GitLabFetcher.
|
|
185
|
+
"""
|
|
186
|
+
# If GitLab release fetching is supported in the future, implement logic here.
|
|
187
|
+
raise NotImplementedError("Release fetching is not supported for GitLab (GitLabFetcher).")
|
|
@@ -11,14 +11,14 @@ from git_recap.providers.base_fetcher import BaseFetcher
|
|
|
11
11
|
|
|
12
12
|
class URLFetcher(BaseFetcher):
|
|
13
13
|
"""Fetcher implementation for generic Git repository URLs."""
|
|
14
|
-
|
|
14
|
+
|
|
15
15
|
GIT_URL_PATTERN = re.compile(
|
|
16
16
|
r'^(?:http|https|git|ssh)://' # Protocol
|
|
17
17
|
r'(?:\S+@)?' # Optional username
|
|
18
18
|
r'([^/]+)' # Domain
|
|
19
19
|
r'(?:[:/])([^/]+/[^/]+?)(?:\.git)?$' # Repo path
|
|
20
20
|
)
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
def __init__(
|
|
23
23
|
self,
|
|
24
24
|
url: str,
|
|
@@ -36,13 +36,13 @@ class URLFetcher(BaseFetcher):
|
|
|
36
36
|
)
|
|
37
37
|
self.url = self._normalize_url(url)
|
|
38
38
|
self.temp_dir = None
|
|
39
|
-
self._validate_url()
|
|
39
|
+
# self._validate_url()
|
|
40
40
|
self._clone_repo()
|
|
41
41
|
|
|
42
42
|
def _normalize_url(self, url: str) -> str:
|
|
43
43
|
"""Normalize the Git URL to ensure consistent format."""
|
|
44
44
|
url = url.strip()
|
|
45
|
-
if not url.endswith('.git'):
|
|
45
|
+
if not url.endswith('.git') and "_git" not in url:
|
|
46
46
|
url += '.git'
|
|
47
47
|
if not any(url.startswith(proto) for proto in ('http://', 'https://', 'git://', 'ssh://')):
|
|
48
48
|
url = f'https://{url}'
|
|
@@ -52,7 +52,7 @@ class URLFetcher(BaseFetcher):
|
|
|
52
52
|
"""Validate the Git repository URL using git ls-remote."""
|
|
53
53
|
if not self.GIT_URL_PATTERN.match(self.url):
|
|
54
54
|
raise ValueError(f"Invalid Git repository URL format: {self.url}")
|
|
55
|
-
|
|
55
|
+
|
|
56
56
|
try:
|
|
57
57
|
result = subprocess.run(
|
|
58
58
|
["git", "ls-remote", self.url],
|
|
@@ -80,7 +80,7 @@ class URLFetcher(BaseFetcher):
|
|
|
80
80
|
text=True,
|
|
81
81
|
timeout=300
|
|
82
82
|
)
|
|
83
|
-
|
|
83
|
+
|
|
84
84
|
# Fetch all branches
|
|
85
85
|
subprocess.run(
|
|
86
86
|
["git", "-C", self.temp_dir, "fetch", "--all"],
|
|
@@ -89,7 +89,7 @@ class URLFetcher(BaseFetcher):
|
|
|
89
89
|
text=True,
|
|
90
90
|
timeout=300
|
|
91
91
|
)
|
|
92
|
-
|
|
92
|
+
|
|
93
93
|
# Verify the cloned repository has at least one commit
|
|
94
94
|
verify_result = subprocess.run(
|
|
95
95
|
["git", "-C", self.temp_dir, "rev-list", "--count", "--all"],
|
|
@@ -99,7 +99,7 @@ class URLFetcher(BaseFetcher):
|
|
|
99
99
|
)
|
|
100
100
|
if int(verify_result.stdout.strip()) == 0:
|
|
101
101
|
raise ValueError("Cloned repository has no commits")
|
|
102
|
-
|
|
102
|
+
|
|
103
103
|
except subprocess.TimeoutExpired:
|
|
104
104
|
raise RuntimeError("Repository cloning timed out")
|
|
105
105
|
except subprocess.CalledProcessError as e:
|
|
@@ -113,22 +113,23 @@ class URLFetcher(BaseFetcher):
|
|
|
113
113
|
"""Return list of repository names (single item for URL fetcher)."""
|
|
114
114
|
if not self.temp_dir:
|
|
115
115
|
return []
|
|
116
|
-
|
|
116
|
+
|
|
117
117
|
match = self.GIT_URL_PATTERN.match(self.url)
|
|
118
118
|
if not match:
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
repo_name = self.url.split('/')[-1]
|
|
120
|
+
return [repo_name]
|
|
121
|
+
|
|
121
122
|
repo_name = match.group(2).split('/')[-1]
|
|
122
123
|
if repo_name.endswith(".git"):
|
|
123
124
|
repo_name = repo_name[:-4]
|
|
124
|
-
|
|
125
|
+
|
|
125
126
|
return [repo_name]
|
|
126
127
|
|
|
127
128
|
def _get_all_branches(self) -> List[str]:
|
|
128
129
|
"""Get list of all remote branches in the repository."""
|
|
129
130
|
if not self.temp_dir:
|
|
130
131
|
return []
|
|
131
|
-
|
|
132
|
+
|
|
132
133
|
try:
|
|
133
134
|
result = subprocess.run(
|
|
134
135
|
["git", "-C", self.temp_dir, "branch", "-r", "--format=%(refname:short)"],
|
|
@@ -186,11 +187,11 @@ class URLFetcher(BaseFetcher):
|
|
|
186
187
|
for line in log_output.splitlines():
|
|
187
188
|
if not line.strip():
|
|
188
189
|
continue
|
|
189
|
-
|
|
190
|
+
|
|
190
191
|
try:
|
|
191
192
|
sha, author, date_str, message = line.split("|", 3)
|
|
192
193
|
timestamp = datetime.fromisoformat(date_str)
|
|
193
|
-
|
|
194
|
+
|
|
194
195
|
if self.start_date and timestamp < self.start_date:
|
|
195
196
|
continue
|
|
196
197
|
if self.end_date and timestamp > self.end_date:
|
|
@@ -206,7 +207,7 @@ class URLFetcher(BaseFetcher):
|
|
|
206
207
|
})
|
|
207
208
|
except ValueError:
|
|
208
209
|
continue # Skip malformed log entries
|
|
209
|
-
|
|
210
|
+
|
|
210
211
|
return entries
|
|
211
212
|
|
|
212
213
|
def fetch_commits(self) -> List[Dict[str, Any]]:
|
|
@@ -221,6 +222,17 @@ class URLFetcher(BaseFetcher):
|
|
|
221
222
|
"""Fetch issues (not implemented for generic Git URLs)."""
|
|
222
223
|
return []
|
|
223
224
|
|
|
225
|
+
def fetch_releases(self) -> List[Dict[str, Any]]:
|
|
226
|
+
"""
|
|
227
|
+
Fetch releases for the repository.
|
|
228
|
+
Not implemented for generic Git URLs.
|
|
229
|
+
Raises:
|
|
230
|
+
NotImplementedError: Always, since release fetching is not supported for URLFetcher.
|
|
231
|
+
"""
|
|
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
|
+
raise NotImplementedError("Release fetching is not supported for generic Git URLs (URLFetcher).")
|
|
235
|
+
|
|
224
236
|
def clear(self) -> None:
|
|
225
237
|
"""Clean up temporary directory."""
|
|
226
238
|
if self.temp_dir and os.path.exists(self.temp_dir):
|
git_recap/utils.py
CHANGED
|
@@ -54,40 +54,94 @@ def parse_entries_to_txt(entries: List[Dict[str, Any]]) -> str:
|
|
|
54
54
|
|
|
55
55
|
return "\n".join(lines)
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
"
|
|
87
|
-
|
|
88
|
-
"
|
|
89
|
-
"
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
57
|
+
def parse_releases_to_txt(releases: List[Dict[str, Any]]) -> str:
|
|
58
|
+
"""
|
|
59
|
+
Groups releases by day (YYYY-MM-DD, using published_at or created_at) and produces a plain text summary.
|
|
60
|
+
|
|
61
|
+
Each day's header is the date string, followed by a clear, LLM-friendly separator between releases:
|
|
62
|
+
- tag name and release name
|
|
63
|
+
- repo name
|
|
64
|
+
- author
|
|
65
|
+
- draft/prerelease status
|
|
66
|
+
- body/notes (if present)
|
|
67
|
+
- assets (if present)
|
|
68
|
+
"""
|
|
69
|
+
grouped = defaultdict(list)
|
|
70
|
+
for rel in releases:
|
|
71
|
+
ts = rel.get("published_at") or rel.get("created_at")
|
|
72
|
+
if isinstance(ts, str):
|
|
73
|
+
dt = datetime.fromisoformat(ts)
|
|
74
|
+
else:
|
|
75
|
+
dt = ts
|
|
76
|
+
day = dt.strftime("%Y-%m-%d")
|
|
77
|
+
grouped[day].append(rel)
|
|
78
|
+
|
|
79
|
+
sorted_days = sorted(grouped.keys())
|
|
80
|
+
|
|
81
|
+
lines = []
|
|
82
|
+
for day in sorted_days:
|
|
83
|
+
lines.append(f"Date: {day}")
|
|
84
|
+
day_releases = sorted(grouped[day], key=lambda x: x.get("published_at") or x.get("created_at"))
|
|
85
|
+
for rel in day_releases:
|
|
86
|
+
lines.append("----- Release Start -----")
|
|
87
|
+
tag = rel.get("tag_name", "N/A")
|
|
88
|
+
name = rel.get("name", "")
|
|
89
|
+
repo = rel.get("repo", "N/A")
|
|
90
|
+
author = rel.get("author", "N/A")
|
|
91
|
+
draft = rel.get("draft", False)
|
|
92
|
+
prerelease = rel.get("prerelease", False)
|
|
93
|
+
body = rel.get("body", "")
|
|
94
|
+
assets = rel.get("assets", [])
|
|
95
|
+
|
|
96
|
+
status = []
|
|
97
|
+
if draft:
|
|
98
|
+
status.append("draft")
|
|
99
|
+
if prerelease:
|
|
100
|
+
status.append("prerelease")
|
|
101
|
+
status_str = f" ({', '.join(status)})" if status else ""
|
|
102
|
+
|
|
103
|
+
lines.append(f"Release: {name}")
|
|
104
|
+
lines.append(f"Repository: {repo}")
|
|
105
|
+
lines.append(f"Tag: {tag}")
|
|
106
|
+
lines.append(f"Author: {author}")
|
|
107
|
+
if status_str:
|
|
108
|
+
lines.append(f"Status: {status_str.strip()}")
|
|
109
|
+
if body and body.strip():
|
|
110
|
+
lines.append(f"Notes: {body.strip()}")
|
|
111
|
+
if assets:
|
|
112
|
+
lines.append("Assets:")
|
|
113
|
+
for asset in assets:
|
|
114
|
+
asset_name = asset.get("name", "N/A")
|
|
115
|
+
asset_size = asset.get("size", "N/A")
|
|
116
|
+
asset_url = asset.get("download_url", "")
|
|
117
|
+
lines.append(f" - {asset_name} ({asset_size} bytes){' - ' + asset_url if asset_url else ''}")
|
|
118
|
+
lines.append("----- Release End -----\n") # clear end of release
|
|
119
|
+
|
|
120
|
+
lines.append("") # blank line between days
|
|
121
|
+
|
|
122
|
+
return "\n".join(lines)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# Example usage for releases:
|
|
126
|
+
# if __name__ == "__main__":
|
|
127
|
+
# releases = [
|
|
128
|
+
# {
|
|
129
|
+
# "tag_name": "v1.0.0",
|
|
130
|
+
# "name": "Release 1.0.0",
|
|
131
|
+
# "repo": "test-repo",
|
|
132
|
+
# "author": "testuser",
|
|
133
|
+
# "published_at": "2025-03-15T10:00:00",
|
|
134
|
+
# "created_at": "2025-03-15T09:00:00",
|
|
135
|
+
# "draft": False,
|
|
136
|
+
# "prerelease": False,
|
|
137
|
+
# "body": "This is a test release",
|
|
138
|
+
# "assets": [
|
|
139
|
+
# {
|
|
140
|
+
# "name": "test-asset.zip",
|
|
141
|
+
# "size": 1024,
|
|
142
|
+
# "download_url": "https://github.com/test/releases/download/v1.0.0/test-asset.zip",
|
|
143
|
+
# }
|
|
144
|
+
# ],
|
|
145
|
+
# }
|
|
146
|
+
# ]
|
|
147
|
+
# print(parse_releases_to_txt(releases))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: git-recap
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: A modular Python tool that aggregates and formats user-authored messages from repositories.
|
|
5
5
|
Author: Bruno V.
|
|
6
6
|
Author-email: bruno.vitorino@tecnico.ulisboa.pt
|
|
@@ -21,8 +21,16 @@ Dynamic: license-file
|
|
|
21
21
|
Dynamic: requires-dist
|
|
22
22
|
Dynamic: summary
|
|
23
23
|
|
|
24
|
+
<a href="https://www.uneed.best/tool/gitrecap">
|
|
25
|
+
<img src="https://www.uneed.best/POTD2A.png" style="width: 250px;" alt="Uneed POTD2 Badge" />
|
|
26
|
+
</a>
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
24
30
|
# Git Recap
|
|
25
31
|
|
|
32
|
+
🎉 **Featured in Uneed's Latest Newsletter as a Staff Pick!** 🎉
|
|
33
|
+
|
|
26
34
|
Git Recap is a modular Python tool that aggregates and formats user-authored messages from repositories hosted on GitHub, Azure DevOps, and GitLab. It fetches commit messages, pull requests (along with their associated commits), and issues, then consolidates and sorts these events into a clear, chronological summary. This summary is output as a plain text string that can serve as context for large language models or other analysis tools.
|
|
27
35
|
|
|
28
36
|
## Features
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
git_recap/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
git_recap/fetcher.py,sha256=oRlenzd9OsiBkCtpUgSZGlUoUnVpdkolUZhFOF1USBs,2677
|
|
3
|
+
git_recap/utils.py,sha256=NAG0cvm1sYKEQD5E9PueduB3CZQRTahrioxZNgwwmi4,5481
|
|
4
|
+
git_recap/providers/__init__.py,sha256=njrsu58KB60-wN78P0egm1lkQLIRq_PHLKM4WBt39Os,330
|
|
5
|
+
git_recap/providers/azure_fetcher.py,sha256=nVJr77EYcuVDEAdY1B4UJCfHQTz8rE7s0Bmz3To1XVU,9722
|
|
6
|
+
git_recap/providers/base_fetcher.py,sha256=EIkaqHBl5xyvNigK9jojlVxxbJrWXg0cNnRkmOLoPWk,4980
|
|
7
|
+
git_recap/providers/github_fetcher.py,sha256=k9akil096PO29SNk2xaZPjRipnxS_sAO0GukWjMdSf4,7375
|
|
8
|
+
git_recap/providers/gitlab_fetcher.py,sha256=apfaRrP7Dat0OKjo3InmNFwNEOiJShA8mSHPyW-9yKg,7333
|
|
9
|
+
git_recap/providers/url_fetcher.py,sha256=oQqewY6uo-OgsCvZuodj6vWonHfeANrE-KSiCVH7il8,8741
|
|
10
|
+
git_recap-0.1.4.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
11
|
+
git_recap-0.1.4.dist-info/METADATA,sha256=zC2u1gX9G2_VDmdFrJzE2nJykYfuztkHt9DxCgW6v9Q,4946
|
|
12
|
+
git_recap-0.1.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
13
|
+
git_recap-0.1.4.dist-info/top_level.txt,sha256=1JUKd3WPB8c3LcD1deIW-1UTmYzA0zJqwugAz72YZ_o,10
|
|
14
|
+
git_recap-0.1.4.dist-info/RECORD,,
|
git_recap-0.1.2.dist-info/RECORD
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
git_recap/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
git_recap/fetcher.py,sha256=oRlenzd9OsiBkCtpUgSZGlUoUnVpdkolUZhFOF1USBs,2677
|
|
3
|
-
git_recap/utils.py,sha256=mAGZ6bNklBmE-qpA6RG17_Bka8WRXzjLRAAGS9p3QXk,3646
|
|
4
|
-
git_recap/providers/__init__.py,sha256=njrsu58KB60-wN78P0egm1lkQLIRq_PHLKM4WBt39Os,330
|
|
5
|
-
git_recap/providers/azure_fetcher.py,sha256=PfPEeWTtx1Fqa1jClFPKqXhY5Yvs8MyEkSJkLAl4QvQ,7241
|
|
6
|
-
git_recap/providers/base_fetcher.py,sha256=SS35FP8B9_RwnU6CnMbcV9g5_YRWp7v-S7pwbt402Ng,3067
|
|
7
|
-
git_recap/providers/github_fetcher.py,sha256=rDb00RVy2NMpwv8s8Ry1-UjS6wf1Sh2Ndl0xKOZXMxU,4998
|
|
8
|
-
git_recap/providers/gitlab_fetcher.py,sha256=ch1V9O-MPv5_nyg9yLs6EC9e-yqsFXBXMm6xVevWzuQ,5396
|
|
9
|
-
git_recap/providers/url_fetcher.py,sha256=_4CGHfXt701GUHBjsWiu-2ZMTtNerwp60ZxTQh-Mwtc,8256
|
|
10
|
-
git_recap-0.1.2.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
11
|
-
git_recap-0.1.2.dist-info/METADATA,sha256=USOwnxiUUkGbMt82rPVA2Dw0oAc-YqTl7ow-CvBKyaQ,4721
|
|
12
|
-
git_recap-0.1.2.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
|
13
|
-
git_recap-0.1.2.dist-info/top_level.txt,sha256=1JUKd3WPB8c3LcD1deIW-1UTmYzA0zJqwugAz72YZ_o,10
|
|
14
|
-
git_recap-0.1.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|