git-recap 0.1.4__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 (23) hide show
  1. {git_recap-0.1.4 → git_recap-0.1.5}/PKG-INFO +1 -1
  2. {git_recap-0.1.4 → git_recap-0.1.5}/git_recap/providers/azure_fetcher.py +79 -2
  3. {git_recap-0.1.4 → git_recap-0.1.5}/git_recap/providers/base_fetcher.py +66 -0
  4. git_recap-0.1.5/git_recap/providers/github_fetcher.py +408 -0
  5. {git_recap-0.1.4 → git_recap-0.1.5}/git_recap/providers/gitlab_fetcher.py +65 -3
  6. {git_recap-0.1.4 → git_recap-0.1.5}/git_recap/providers/url_fetcher.py +59 -2
  7. {git_recap-0.1.4 → git_recap-0.1.5}/git_recap.egg-info/PKG-INFO +1 -1
  8. {git_recap-0.1.4 → git_recap-0.1.5}/setup.py +1 -1
  9. {git_recap-0.1.4 → git_recap-0.1.5}/tests/test_dummy_parser.py +10 -1
  10. git_recap-0.1.5/tests/test_parser.py +630 -0
  11. git_recap-0.1.4/git_recap/providers/github_fetcher.py +0 -177
  12. git_recap-0.1.4/tests/test_parser.py +0 -237
  13. {git_recap-0.1.4 → git_recap-0.1.5}/LICENSE +0 -0
  14. {git_recap-0.1.4 → git_recap-0.1.5}/README.md +0 -0
  15. {git_recap-0.1.4 → git_recap-0.1.5}/git_recap/__init__.py +0 -0
  16. {git_recap-0.1.4 → git_recap-0.1.5}/git_recap/fetcher.py +0 -0
  17. {git_recap-0.1.4 → git_recap-0.1.5}/git_recap/providers/__init__.py +0 -0
  18. {git_recap-0.1.4 → git_recap-0.1.5}/git_recap/utils.py +0 -0
  19. {git_recap-0.1.4 → git_recap-0.1.5}/git_recap.egg-info/SOURCES.txt +0 -0
  20. {git_recap-0.1.4 → git_recap-0.1.5}/git_recap.egg-info/dependency_links.txt +0 -0
  21. {git_recap-0.1.4 → git_recap-0.1.5}/git_recap.egg-info/requires.txt +0 -0
  22. {git_recap-0.1.4 → git_recap-0.1.5}/git_recap.egg-info/top_level.txt +0 -0
  23. {git_recap-0.1.4 → 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.4
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
@@ -1,7 +1,7 @@
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):
@@ -236,4 +236,81 @@ class AzureFetcher(BaseFetcher):
236
236
  NotImplementedError: Always, since release fetching is not supported for AzureFetcher.
237
237
  """
238
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).")
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).")
@@ -90,6 +90,72 @@ class BaseFetcher(ABC):
90
90
  """
91
91
  raise NotImplementedError("Release fetching is not implemented for this provider.")
92
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
+
93
159
  def get_authored_messages(self) -> List[Dict[str, Any]]:
94
160
  """
95
161
  Aggregates all commit, pull request, and issue entries into a single list,
@@ -0,0 +1,408 @@
1
+ from github import Github
2
+ from github import GithubException
3
+ from datetime import datetime
4
+ from typing import List, Dict, Any, Optional
5
+ from git_recap.providers.base_fetcher import BaseFetcher
6
+ import logging
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class GitHubFetcher(BaseFetcher):
12
+ """
13
+ Fetcher implementation for GitHub repositories.
14
+
15
+ Supports fetching commits, pull requests, issues, and releases.
16
+ """
17
+
18
+ def __init__(self, pat: str, start_date=None, end_date=None, repo_filter=None, authors=None):
19
+ super().__init__(pat, start_date, end_date, repo_filter, authors)
20
+ self.github = Github(self.pat)
21
+ self.user = self.github.get_user()
22
+ self.repos = self.user.get_repos(affiliation="owner,collaborator,organization_member")
23
+ self.authors.append(self.user.login)
24
+
25
+ @property
26
+ def repos_names(self) -> List[str]:
27
+ return [repo.name for repo in self.repos]
28
+
29
+ def _stop_fetching(self, date_obj: datetime) -> bool:
30
+ if self.start_date and date_obj < self.start_date:
31
+ return True
32
+ return False
33
+
34
+ def _filter_by_date(self, date_obj: datetime) -> bool:
35
+ if self.start_date and date_obj < self.start_date:
36
+ return False
37
+ if self.end_date and date_obj > self.end_date:
38
+ return False
39
+ return True
40
+
41
+ def fetch_commits(self) -> List[Dict[str, Any]]:
42
+ entries = []
43
+ processed_commits = set()
44
+ for repo in self.repos:
45
+ if self.repo_filter and repo.name not in self.repo_filter:
46
+ continue
47
+ for author in self.authors:
48
+ commits = repo.get_commits(author=author)
49
+ for i, commit in enumerate(commits, start=1):
50
+ commit_date = commit.commit.author.date
51
+ if self._filter_by_date(commit_date):
52
+ sha = commit.sha
53
+ if sha not in processed_commits:
54
+ entry = {
55
+ "type": "commit",
56
+ "repo": repo.name,
57
+ "message": commit.commit.message.strip(),
58
+ "timestamp": commit_date,
59
+ "sha": sha,
60
+ }
61
+ entries.append(entry)
62
+ processed_commits.add(sha)
63
+ if self._stop_fetching(commit_date):
64
+ break
65
+ return entries
66
+
67
+ def fetch_branch_diff_commits(self, source_branch: str, target_branch: str) -> List[Dict[str, Any]]:
68
+ entries = []
69
+ processed_commits = set()
70
+ for repo in self.repos:
71
+ if self.repo_filter and repo.name not in self.repo_filter:
72
+ continue
73
+ try:
74
+ comparison = repo.compare(target_branch, source_branch)
75
+ for commit in comparison.commits:
76
+ commit_date = commit.commit.author.date
77
+ sha = commit.sha
78
+ if sha not in processed_commits:
79
+ entry = {
80
+ "type": "commit",
81
+ "repo": repo.name,
82
+ "message": commit.commit.message.strip(),
83
+ "timestamp": commit_date,
84
+ "sha": sha,
85
+ }
86
+ entries.append(entry)
87
+ processed_commits.add(sha)
88
+ except GithubException as e:
89
+ logger.error(f"Failed to compare branches in {repo.name}: {str(e)}")
90
+ continue
91
+ return entries
92
+
93
+ def fetch_pull_requests(self) -> List[Dict[str, Any]]:
94
+ entries = []
95
+ # Maintain a local set to skip duplicate commits already captured in a PR.
96
+ processed_pr_commits = set()
97
+ # Retrieve repos where you're owner, a collaborator, or an organization member.
98
+ for repo in self.repos:
99
+ if self.repo_filter and repo.name not in self.repo_filter:
100
+ continue
101
+ pulls = repo.get_pulls(state='all')
102
+ for i, pr in enumerate(pulls, start=1):
103
+ if pr.user.login not in self.authors:
104
+ continue
105
+ pr_date = pr.updated_at # alternatively, use pr.created_at
106
+ if not self._filter_by_date(pr_date):
107
+ continue
108
+
109
+ # Add the pull request itself.
110
+ pr_entry = {
111
+ "type": "pull_request",
112
+ "repo": repo.name,
113
+ "message": pr.title,
114
+ "timestamp": pr_date,
115
+ "pr_number": pr.number,
116
+ }
117
+ entries.append(pr_entry)
118
+
119
+ # Now, add commits associated with this pull request.
120
+ pr_commits = pr.get_commits()
121
+ for pr_commit in pr_commits:
122
+ commit_date = pr_commit.commit.author.date
123
+ if self._filter_by_date(commit_date):
124
+ sha = pr_commit.sha
125
+ if sha in processed_pr_commits:
126
+ continue
127
+ pr_commit_entry = {
128
+ "type": "commit_from_pr",
129
+ "repo": repo.name,
130
+ "message": pr_commit.commit.message.strip(),
131
+ "timestamp": commit_date,
132
+ "sha": sha,
133
+ "pr_title": pr.title,
134
+ }
135
+ entries.append(pr_commit_entry)
136
+ processed_pr_commits.add(sha)
137
+ if self._stop_fetching(pr_date):
138
+ break
139
+ return entries
140
+
141
+ def fetch_issues(self) -> List[Dict[str, Any]]:
142
+ entries = []
143
+ issues = self.user.get_issues()
144
+ for i, issue in enumerate(issues, start=1):
145
+ issue_date = issue.created_at
146
+ if self._filter_by_date(issue_date):
147
+ entry = {
148
+ "type": "issue",
149
+ "repo": issue.repository.name,
150
+ "message": issue.title,
151
+ "timestamp": issue_date,
152
+ }
153
+ entries.append(entry)
154
+ if self._stop_fetching(issue_date):
155
+ break
156
+ return entries
157
+
158
+ def fetch_releases(self) -> List[Dict[str, Any]]:
159
+ """
160
+ Fetch releases for all repositories accessible to the user.
161
+
162
+ Returns:
163
+ List[Dict[str, Any]]: List of releases, each as a structured dictionary with:
164
+ - tag_name: str
165
+ - name: str
166
+ - repo: str
167
+ - author: str
168
+ - published_at: datetime
169
+ - created_at: datetime
170
+ - draft: bool
171
+ - prerelease: bool
172
+ - body: str
173
+ - assets: List[Dict[str, Any]] (each with name, size, download_url, content_type, etc.)
174
+ """
175
+ releases = []
176
+ for repo in self.repos:
177
+ if self.repo_filter and repo.name not in self.repo_filter:
178
+ continue
179
+ try:
180
+ for rel in repo.get_releases():
181
+ # Compose asset list
182
+ assets = []
183
+ for asset in rel.get_assets():
184
+ assets.append({
185
+ "name": asset.name,
186
+ "size": asset.size,
187
+ "download_url": asset.browser_download_url,
188
+ "content_type": asset.content_type,
189
+ "created_at": asset.created_at,
190
+ "updated_at": asset.updated_at,
191
+ })
192
+ release_entry = {
193
+ "tag_name": rel.tag_name,
194
+ "name": rel.title if hasattr(rel, "title") else rel.name,
195
+ "repo": repo.name,
196
+ "author": rel.author.login if rel.author else None,
197
+ "published_at": rel.published_at,
198
+ "created_at": rel.created_at,
199
+ "draft": rel.draft,
200
+ "prerelease": rel.prerelease,
201
+ "body": rel.body,
202
+ "assets": assets,
203
+ }
204
+ releases.append(release_entry)
205
+ except Exception:
206
+ # If fetching releases fails for a repo, skip it (could be permissions or no releases)
207
+ continue
208
+ return releases
209
+
210
+ def get_branches(self) -> List[str]:
211
+ """
212
+ Get all branches in the repository.
213
+ Returns:
214
+ List[str]: List of branch names.
215
+ Raises:
216
+ Exception: If API rate limits are exceeded or authentication fails.
217
+ """
218
+ logger.debug("Fetching branches from all accessible repositories")
219
+ try:
220
+ branches = []
221
+ for repo in self.repos:
222
+ if self.repo_filter and repo.name not in self.repo_filter:
223
+ continue
224
+ logger.debug(f"Fetching branches for repository: {repo.name}")
225
+ repo_branches = repo.get_branches()
226
+ for branch in repo_branches:
227
+ branches.append(branch.name)
228
+ logger.debug(f"Successfully fetched {len(branches)} branches")
229
+ return branches
230
+ except GithubException as e:
231
+ if e.status == 403:
232
+ logger.error(f"Rate limit exceeded or authentication failed: {str(e)}")
233
+ raise Exception(f"Failed to fetch branches: Rate limit exceeded or authentication failed - {str(e)}")
234
+ elif e.status == 401:
235
+ logger.error(f"Authentication failed: {str(e)}")
236
+ raise Exception(f"Failed to fetch branches: Authentication failed - {str(e)}")
237
+ else:
238
+ logger.error(f"GitHub API error while fetching branches: {str(e)}")
239
+ raise Exception(f"Failed to fetch branches: {str(e)}")
240
+ except Exception as e:
241
+ logger.error(f"Unexpected error while fetching branches: {str(e)}")
242
+ raise Exception(f"Failed to fetch branches: {str(e)}")
243
+
244
+ def get_valid_target_branches(self, source_branch: str) -> List[str]:
245
+ """
246
+ Get branches that can receive a pull request from the source branch.
247
+ Validates that the source branch exists, filters out branches with existing
248
+ open PRs from source, excludes the source branch itself, and optionally
249
+ checks if source is ahead of target.
250
+ Args:
251
+ source_branch (str): The source branch name.
252
+ Returns:
253
+ List[str]: List of valid target branch names.
254
+ Raises:
255
+ ValueError: If source branch does not exist.
256
+ Exception: If API errors occur during validation.
257
+ """
258
+ logger.debug(f"Validating target branches for source branch: {source_branch}")
259
+ try:
260
+ all_branches = self.get_branches()
261
+ if source_branch not in all_branches:
262
+ logger.error(f"Source branch '{source_branch}' does not exist")
263
+ raise ValueError(f"Source branch '{source_branch}' does not exist")
264
+ valid_targets = []
265
+ for repo in self.repos:
266
+ if self.repo_filter and repo.name not in self.repo_filter:
267
+ continue
268
+ logger.debug(f"Processing repository: {repo.name}")
269
+ repo_branches = [branch.name for branch in repo.get_branches()]
270
+ # Get existing open PRs from source branch
271
+ try:
272
+ open_prs = repo.get_pulls(state='open', head=source_branch)
273
+ except GithubException as e:
274
+ logger.error(f"GitHub API error while getting PRs: {str(e)}")
275
+ raise Exception(f"Failed to validate target branches: {str(e)}")
276
+ existing_pr_targets = set()
277
+ for pr in open_prs:
278
+ existing_pr_targets.add(pr.base.ref)
279
+ logger.debug(f"Found existing PR from {source_branch} to {pr.base.ref}")
280
+ for branch_name in repo_branches:
281
+ if branch_name == source_branch:
282
+ logger.debug(f"Excluding source branch: {branch_name}")
283
+ continue
284
+ if branch_name in existing_pr_targets:
285
+ logger.debug(f"Excluding branch with existing PR: {branch_name}")
286
+ continue
287
+ # Optionally check if source is ahead of target (performance cost)
288
+ valid_targets.append(branch_name)
289
+ logger.debug(f"Valid target branch: {branch_name}")
290
+ logger.debug(f"Found {len(valid_targets)} valid target branches")
291
+ return valid_targets
292
+ except ValueError:
293
+ raise
294
+ except GithubException as e:
295
+ logger.error(f"GitHub API error while validating target branches: {str(e)}")
296
+ raise Exception(f"Failed to validate target branches: {str(e)}")
297
+ except Exception as e:
298
+ logger.error(f"Unexpected error while validating target branches: {str(e)}")
299
+ raise Exception(f"Failed to validate target branches: {str(e)}")
300
+
301
+ def create_pull_request(
302
+ self,
303
+ head_branch: str,
304
+ base_branch: str,
305
+ title: str,
306
+ body: str,
307
+ draft: bool = False,
308
+ reviewers: Optional[List[str]] = None,
309
+ assignees: Optional[List[str]] = None,
310
+ labels: Optional[List[str]] = None
311
+ ) -> Dict[str, Any]:
312
+ """
313
+ Create a pull request between two branches with optional metadata.
314
+ Args:
315
+ head_branch: Source branch for the PR.
316
+ base_branch: Target branch for the PR.
317
+ title: PR title.
318
+ body: PR description.
319
+ draft: Whether to create as draft PR (default: False).
320
+ reviewers: List of reviewer usernames (optional).
321
+ assignees: List of assignee usernames (optional).
322
+ labels: List of label names (optional).
323
+ Returns:
324
+ Dict[str, Any]: Dictionary containing PR metadata or error information.
325
+ Raises:
326
+ ValueError: If branches don't exist or PR already exists.
327
+ """
328
+ logger.info(f"Creating pull request from {head_branch} to {base_branch}")
329
+ try:
330
+ all_branches = self.get_branches()
331
+ if head_branch not in all_branches:
332
+ logger.error(f"Head branch '{head_branch}' does not exist")
333
+ raise ValueError(f"Head branch '{head_branch}' does not exist")
334
+ if base_branch not in all_branches:
335
+ logger.error(f"Base branch '{base_branch}' does not exist")
336
+ raise ValueError(f"Base branch '{base_branch}' does not exist")
337
+ for repo in self.repos:
338
+ if self.repo_filter and repo.name not in self.repo_filter:
339
+ continue
340
+ logger.debug(f"Checking for existing PRs in repository: {repo.name}")
341
+ try:
342
+ existing_prs = repo.get_pulls(state='open', head=head_branch, base=base_branch)
343
+ except GithubException as e:
344
+ logger.error(f"GitHub API error while getting PRs: {str(e)}")
345
+ raise
346
+ if hasattr(existing_prs, "totalCount") and existing_prs.totalCount > 0:
347
+ logger.error(f"Pull request already exists from {head_branch} to {base_branch}")
348
+ raise ValueError(f"Pull request already exists from {head_branch} to {base_branch}")
349
+ elif isinstance(existing_prs, list) and len(existing_prs) > 0:
350
+ logger.error(f"Pull request already exists from {head_branch} to {base_branch}")
351
+ raise ValueError(f"Pull request already exists from {head_branch} to {base_branch}")
352
+ logger.info(f"Creating pull request in repository: {repo.name}")
353
+ try:
354
+ pr = repo.create_pull(
355
+ title=title,
356
+ body=body,
357
+ head=head_branch,
358
+ base=base_branch,
359
+ draft=draft
360
+ )
361
+ logger.info(f"Pull request created successfully: {pr.html_url}")
362
+ if reviewers and len(reviewers) > 0:
363
+ try:
364
+ logger.debug(f"Adding reviewers: {reviewers}")
365
+ pr.create_review_request(reviewers=reviewers)
366
+ logger.info(f"Successfully added reviewers: {reviewers}")
367
+ except GithubException as e:
368
+ logger.warning(f"Failed to add reviewers: {str(e)}")
369
+ if assignees and len(assignees) > 0:
370
+ try:
371
+ logger.debug(f"Adding assignees: {assignees}")
372
+ pr.add_to_assignees(*assignees)
373
+ logger.info(f"Successfully added assignees: {assignees}")
374
+ except GithubException as e:
375
+ logger.warning(f"Failed to add assignees: {str(e)}")
376
+ if labels and len(labels) > 0:
377
+ try:
378
+ logger.debug(f"Adding labels: {labels}")
379
+ pr.add_to_labels(*labels)
380
+ logger.info(f"Successfully added labels: {labels}")
381
+ except GithubException as e:
382
+ logger.warning(f"Failed to add labels: {str(e)}")
383
+ return {
384
+ "url": pr.html_url,
385
+ "number": pr.number,
386
+ "state": pr.state,
387
+ "success": True
388
+ }
389
+ except GithubException as e:
390
+ if e.status == 404:
391
+ logger.error(f"Branch not found: {str(e)}")
392
+ raise ValueError(f"Branch not found: {str(e)}")
393
+ elif e.status == 403:
394
+ logger.error(f"Permission denied: {str(e)}")
395
+ raise GithubException(e.status, f"Permission denied: {str(e)}", e.headers)
396
+ elif e.status == 422:
397
+ logger.error(f"Merge conflict or validation error: {str(e)}")
398
+ raise ValueError(f"Merge conflict or validation error: {str(e)}")
399
+ else:
400
+ logger.error(f"GitHub API error: {str(e)}")
401
+ raise
402
+ logger.error("No repository found to create pull request")
403
+ raise ValueError("No repository found to create pull request")
404
+ except (ValueError, GithubException):
405
+ raise
406
+ except Exception as e:
407
+ logger.error(f"Unexpected error while creating pull request: {str(e)}")
408
+ raise Exception(f"Failed to create pull request: {str(e)}")
@@ -1,6 +1,6 @@
1
1
  import gitlab
2
2
  from datetime import datetime
3
- from typing import List, Dict, Any
3
+ from typing import List, Dict, Any, Optional
4
4
  from git_recap.providers.base_fetcher import BaseFetcher
5
5
 
6
6
  class GitLabFetcher(BaseFetcher):
@@ -183,5 +183,67 @@ class GitLabFetcher(BaseFetcher):
183
183
  Raises:
184
184
  NotImplementedError: Always, since release fetching is not supported for GitLabFetcher.
185
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).")
186
+ raise NotImplementedError("Release fetching is not supported for GitLab (GitLabFetcher).")
187
+
188
+ def get_branches(self) -> List[str]:
189
+ """
190
+ Get all branches in the repository.
191
+
192
+ Returns:
193
+ List[str]: List of branch names.
194
+
195
+ Raises:
196
+ NotImplementedError: Always, since branch listing is not yet implemented for GitLabFetcher.
197
+ """
198
+ raise NotImplementedError("Branch listing is not yet implemented for GitLab (GitLabFetcher).")
199
+
200
+ def get_valid_target_branches(self, source_branch: str) -> List[str]:
201
+ """
202
+ Get branches that can receive a pull request from the source branch.
203
+
204
+ Validates that the source branch exists, filters out branches with existing
205
+ open PRs from source, excludes the source branch itself, and optionally
206
+ checks if source is ahead of target.
207
+
208
+ Args:
209
+ source_branch (str): The source branch name.
210
+
211
+ Returns:
212
+ List[str]: List of valid target branch names.
213
+
214
+ Raises:
215
+ NotImplementedError: Always, since PR target validation is not yet implemented for GitLabFetcher.
216
+ """
217
+ raise NotImplementedError("Pull request target branch validation is not yet implemented for GitLab (GitLabFetcher).")
218
+
219
+ def create_pull_request(
220
+ self,
221
+ head_branch: str,
222
+ base_branch: str,
223
+ title: str,
224
+ body: str,
225
+ draft: bool = False,
226
+ reviewers: Optional[List[str]] = None,
227
+ assignees: Optional[List[str]] = None,
228
+ labels: Optional[List[str]] = None
229
+ ) -> Dict[str, Any]:
230
+ """
231
+ Create a pull request (merge request) between two branches with optional metadata.
232
+
233
+ Args:
234
+ head_branch: Source branch for the PR.
235
+ base_branch: Target branch for the PR.
236
+ title: PR title.
237
+ body: PR description.
238
+ draft: Whether to create as draft PR (default: False).
239
+ reviewers: List of reviewer usernames (optional).
240
+ assignees: List of assignee usernames (optional).
241
+ labels: List of label names (optional).
242
+
243
+ Returns:
244
+ Dict[str, Any]: Dictionary containing PR metadata (url, number, state, success) or error information.
245
+
246
+ Raises:
247
+ NotImplementedError: Always, since PR creation is not yet implemented for GitLabFetcher.
248
+ """
249
+ raise NotImplementedError("Pull request (merge request) creation is not yet implemented for GitLab (GitLabFetcher).")