git-recap 0.1.3__tar.gz → 0.1.5__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 (25) hide show
  1. {git_recap-0.1.3 → git_recap-0.1.5}/PKG-INFO +9 -1
  2. {git_recap-0.1.3 → git_recap-0.1.5}/README.md +9 -1
  3. {git_recap-0.1.3 → git_recap-0.1.5}/git_recap/providers/azure_fetcher.py +158 -5
  4. git_recap-0.1.5/git_recap/providers/base_fetcher.py +209 -0
  5. git_recap-0.1.5/git_recap/providers/github_fetcher.py +408 -0
  6. {git_recap-0.1.3 → git_recap-0.1.5}/git_recap/providers/gitlab_fetcher.py +126 -6
  7. {git_recap-0.1.3 → git_recap-0.1.5}/git_recap/providers/url_fetcher.py +81 -13
  8. git_recap-0.1.5/git_recap/utils.py +147 -0
  9. {git_recap-0.1.3 → git_recap-0.1.5}/git_recap.egg-info/PKG-INFO +9 -1
  10. {git_recap-0.1.3 → git_recap-0.1.5}/setup.py +1 -1
  11. {git_recap-0.1.3 → git_recap-0.1.5}/tests/test_dummy_parser.py +13 -1
  12. git_recap-0.1.5/tests/test_parser.py +630 -0
  13. git_recap-0.1.3/git_recap/providers/base_fetcher.py +0 -87
  14. git_recap-0.1.3/git_recap/providers/github_fetcher.py +0 -120
  15. git_recap-0.1.3/git_recap/utils.py +0 -93
  16. git_recap-0.1.3/tests/test_parser.py +0 -52
  17. {git_recap-0.1.3 → git_recap-0.1.5}/LICENSE +0 -0
  18. {git_recap-0.1.3 → git_recap-0.1.5}/git_recap/__init__.py +0 -0
  19. {git_recap-0.1.3 → git_recap-0.1.5}/git_recap/fetcher.py +0 -0
  20. {git_recap-0.1.3 → git_recap-0.1.5}/git_recap/providers/__init__.py +0 -0
  21. {git_recap-0.1.3 → git_recap-0.1.5}/git_recap.egg-info/SOURCES.txt +0 -0
  22. {git_recap-0.1.3 → git_recap-0.1.5}/git_recap.egg-info/dependency_links.txt +0 -0
  23. {git_recap-0.1.3 → git_recap-0.1.5}/git_recap.egg-info/requires.txt +0 -0
  24. {git_recap-0.1.3 → git_recap-0.1.5}/git_recap.egg-info/top_level.txt +0 -0
  25. {git_recap-0.1.3 → git_recap-0.1.5}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: git-recap
3
- Version: 0.1.3
3
+ Version: 0.1.5
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
@@ -1,5 +1,13 @@
1
+ <a href="https://www.uneed.best/tool/gitrecap">
2
+ <img src="https://www.uneed.best/POTD2A.png" style="width: 250px;" alt="Uneed POTD2 Badge" />
3
+ </a>
4
+
5
+ ---
6
+
1
7
  # Git Recap
2
8
 
9
+ 🎉 **Featured in Uneed's Latest Newsletter as a Staff Pick!** 🎉
10
+
3
11
  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.
4
12
 
5
13
  ## Features
@@ -110,4 +118,4 @@ This project is licensed under the terms of the [MIT License](LICENSE).
110
118
 
111
119
  - [PyGitHub](https://pygithub.readthedocs.io/en/stable/)
112
120
  - [Azure DevOps Python API](https://github.com/microsoft/azure-devops-python-api)
113
- - [python-gitlab](https://python-gitlab.readthedocs.io/)
121
+ - [python-gitlab](https://python-gitlab.readthedocs.io/)
@@ -1,12 +1,29 @@
1
1
  from azure.devops.connection import Connection
2
2
  from msrest.authentication import BasicAuthentication
3
3
  from datetime import datetime
4
- from typing import List, Dict, Any
4
+ from typing import List, Dict, Any, Optional
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
- # authors should be passed as a list of unique names (e.g., email or unique id)
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
- "to be implemented later"
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,93 @@ 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).")
240
+
241
+ def get_branches(self) -> List[str]:
242
+ """
243
+ Get all branches in the repository.
244
+
245
+ Returns:
246
+ List[str]: List of branch names.
247
+
248
+ Raises:
249
+ NotImplementedError: Always, since branch listing is not yet implemented for AzureFetcher.
250
+ """
251
+ # TODO: Implement get_branches() for Azure DevOps support
252
+ # This would use: git_client.get_branches(repository_id, project)
253
+ # and extract branch names from the returned objects
254
+ raise NotImplementedError("Branch listing is not yet implemented for Azure DevOps (AzureFetcher).")
255
+
256
+ def get_valid_target_branches(self, source_branch: str) -> List[str]:
257
+ """
258
+ Get branches that can receive a pull request from the source branch.
259
+
260
+ Validates that the source branch exists, filters out branches with existing
261
+ open PRs from source, excludes the source branch itself, and optionally
262
+ checks if source is ahead of target.
263
+
264
+ Args:
265
+ source_branch (str): The source branch name.
266
+
267
+ Returns:
268
+ List[str]: List of valid target branch names.
269
+
270
+ Raises:
271
+ NotImplementedError: Always, since PR target validation is not yet implemented for AzureFetcher.
272
+ """
273
+ # TODO: Implement get_valid_target_branches() for Azure DevOps support
274
+ # This would require:
275
+ # 1. Verify source_branch exists using git_client.get_branch()
276
+ # 2. Get all branches using get_branches()
277
+ # 3. Filter out source branch
278
+ # 4. Check for existing pull requests using git_client.get_pull_requests()
279
+ # 5. Filter out branches with existing open PRs from source
280
+ # 6. Optionally check branch policies and protection rules
281
+ raise NotImplementedError("Pull request target branch validation is not yet implemented for Azure DevOps (AzureFetcher).")
282
+
283
+ def create_pull_request(
284
+ self,
285
+ head_branch: str,
286
+ base_branch: str,
287
+ title: str,
288
+ body: str,
289
+ draft: bool = False,
290
+ reviewers: Optional[List[str]] = None,
291
+ assignees: Optional[List[str]] = None,
292
+ labels: Optional[List[str]] = None
293
+ ) -> Dict[str, Any]:
294
+ """
295
+ Create a pull request between two branches with optional metadata.
296
+
297
+ Args:
298
+ head_branch: Source branch for the PR.
299
+ base_branch: Target branch for the PR.
300
+ title: PR title.
301
+ body: PR description.
302
+ draft: Whether to create as draft PR (default: False).
303
+ reviewers: List of reviewer usernames (optional).
304
+ assignees: List of assignee usernames (optional).
305
+ labels: List of label names (optional).
306
+
307
+ Returns:
308
+ Dict[str, Any]: Dictionary containing PR metadata (url, number, state, success) or error information.
309
+
310
+ Raises:
311
+ NotImplementedError: Always, since PR creation is not yet implemented for AzureFetcher.
312
+ """
313
+ # TODO: Implement create_pull_request() for Azure DevOps support
314
+ # This would use: git_client.create_pull_request() with appropriate parameters
315
+ # Would need to handle reviewers, work item links (assignees), labels, and draft status
316
+ raise NotImplementedError("Pull request creation is not yet implemented for Azure DevOps (AzureFetcher).")
@@ -0,0 +1,209 @@
1
+ from abc import ABC, abstractmethod
2
+ from datetime import datetime, timezone
3
+ from typing import List, Optional, Dict, Any
4
+
5
+ class BaseFetcher(ABC):
6
+ def __init__(
7
+ self,
8
+ pat: str,
9
+ start_date: Optional[datetime] = None,
10
+ end_date: Optional[datetime] = None,
11
+ repo_filter: Optional[List[str]] = None,
12
+ authors: Optional[List[str]] = None
13
+ ):
14
+ self.pat = pat
15
+ if start_date is not None:
16
+ self.start_date = start_date if start_date.tzinfo is not None else start_date.replace(tzinfo=timezone.utc)
17
+ else:
18
+ self.start_date = None
19
+
20
+ if end_date is not None:
21
+ self.end_date = end_date if end_date.tzinfo is not None else end_date.replace(tzinfo=timezone.utc)
22
+ else:
23
+ self.end_date = None
24
+
25
+ self.repo_filter = repo_filter or []
26
+ self.limit = -1
27
+ self.authors = [] if authors is None else authors
28
+
29
+ @property
30
+ @abstractmethod
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
+ """
38
+ pass
39
+
40
+ @abstractmethod
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
+ """
48
+ pass
49
+
50
+ @abstractmethod
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
+ """
58
+ pass
59
+
60
+ @abstractmethod
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
+ """
68
+ pass
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
+
93
+ @abstractmethod
94
+ def get_branches(self) -> List[str]:
95
+ """
96
+ Get all branches in the repository.
97
+
98
+ Returns:
99
+ List[str]: List of branch names.
100
+
101
+ Raises:
102
+ NotImplementedError: Subclasses must implement this method.
103
+ """
104
+ raise NotImplementedError("Subclasses must implement get_branches() to return all repository branches")
105
+
106
+ @abstractmethod
107
+ def get_valid_target_branches(self, source_branch: str) -> List[str]:
108
+ """
109
+ Get branches that can receive a pull request from the source branch.
110
+
111
+ Validates that the source branch exists, filters out branches with existing
112
+ open PRs from source, excludes the source branch itself, and optionally
113
+ checks if source is ahead of target.
114
+
115
+ Args:
116
+ source_branch (str): The source branch name.
117
+
118
+ Returns:
119
+ List[str]: List of valid target branch names.
120
+
121
+ Raises:
122
+ NotImplementedError: Subclasses must implement this method.
123
+ """
124
+ raise NotImplementedError("Subclasses must implement get_valid_target_branches() to return valid PR target branches for the given source branch")
125
+
126
+ @abstractmethod
127
+ def create_pull_request(
128
+ self,
129
+ head_branch: str,
130
+ base_branch: str,
131
+ title: str,
132
+ body: str,
133
+ draft: bool = False,
134
+ reviewers: Optional[List[str]] = None,
135
+ assignees: Optional[List[str]] = None,
136
+ labels: Optional[List[str]] = None
137
+ ) -> Dict[str, Any]:
138
+ """
139
+ Create a pull request between two branches with optional metadata.
140
+
141
+ Args:
142
+ head_branch: Source branch for the PR.
143
+ base_branch: Target branch for the PR.
144
+ title: PR title.
145
+ body: PR description.
146
+ draft: Whether to create as draft PR (default: False).
147
+ reviewers: List of reviewer usernames (optional).
148
+ assignees: List of assignee usernames (optional).
149
+ labels: List of label names (optional).
150
+
151
+ Returns:
152
+ Dict[str, Any]: Dictionary containing PR metadata (url, number, state, success) or error information.
153
+
154
+ Raises:
155
+ NotImplementedError: Subclasses must implement this method.
156
+ """
157
+ raise NotImplementedError("Subclasses must implement create_pull_request() to create a pull request with the specified parameters")
158
+
159
+ def get_authored_messages(self) -> List[Dict[str, Any]]:
160
+ """
161
+ Aggregates all commit, pull request, and issue entries into a single list,
162
+ ensuring no duplicate commits (based on SHA) are present, and then sorts
163
+ them in chronological order based on their timestamp.
164
+
165
+ Returns:
166
+ List[Dict[str, Any]]: Aggregated and sorted list of entries.
167
+ """
168
+ commit_entries = self.fetch_commits()
169
+ pr_entries = self.fetch_pull_requests()
170
+ try:
171
+ issue_entries = self.fetch_issues()
172
+ except Exception:
173
+ issue_entries = []
174
+
175
+ all_entries = pr_entries + commit_entries + issue_entries
176
+
177
+ # For commit-related entries, remove duplicates (if any) based on SHA.
178
+ unique_entries = {}
179
+ for entry in all_entries:
180
+ if entry.get("type") in ["commit", "commit_from_pr"]:
181
+ sha = entry.get("sha")
182
+ if sha in unique_entries:
183
+ continue
184
+ unique_entries[sha] = entry
185
+ else:
186
+ # For pull requests and issues, we can create a unique key.
187
+ key = f"{entry['type']}_{entry['repo']}_{entry['timestamp']}"
188
+ unique_entries[key] = entry
189
+
190
+ final_entries = list(unique_entries.values())
191
+ # Sort all entries by their timestamp.
192
+ final_entries.sort(key=lambda x: x["timestamp"])
193
+ return self.convert_timestamps_to_str(final_entries)
194
+
195
+ @staticmethod
196
+ def convert_timestamps_to_str(entries: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
197
+ """
198
+ Converts the timestamp field from datetime to string format for each entry in the list.
199
+
200
+ Args:
201
+ entries (List[Dict[str, Any]]): List of entries with possible datetime timestamps.
202
+
203
+ Returns:
204
+ List[Dict[str, Any]]: Entries with timestamps as ISO-formatted strings.
205
+ """
206
+ for entry in entries:
207
+ if isinstance(entry.get("timestamp"), datetime):
208
+ entry["timestamp"] = entry["timestamp"].isoformat()
209
+ return entries