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 ADDED
@@ -0,0 +1,270 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ GitRecap CLI - LLM-Friendly Command Line Interface
4
+
5
+ This CLI tool fetches and summarizes git commit history from local repositories.
6
+ It's designed to be easily used by LLMs and automated tools with clear, structured output.
7
+
8
+ Usage Examples:
9
+ # Get commits from current directory (last 7 days)
10
+ git-recap .
11
+
12
+ # Get commits from multiple repositories
13
+ git-recap /path/to/repo1 /path/to/repo2
14
+
15
+ # Filter by author
16
+ git-recap . --author "John Doe"
17
+
18
+ # Filter by date range
19
+ git-recap . --start-date "2025-01-01" --end-date "2025-01-31"
20
+
21
+ # Save output to file
22
+ git-recap . --output summary.txt
23
+
24
+ # Combine filters
25
+ git-recap /path/to/repo1 /path/to/repo2 --author "Jane Smith" --start-date "2025-01-01" --output commits.txt
26
+ """
27
+
28
+ import argparse
29
+ import sys
30
+ from datetime import datetime
31
+ from pathlib import Path
32
+ from typing import List, Dict, Any, Optional
33
+
34
+ from git_recap.providers.local_fetcher import LocalFetcher
35
+ from git_recap.utils import parse_entries_to_txt
36
+
37
+
38
+ def parse_date(date_string: str) -> datetime:
39
+ """
40
+ Parse date string in ISO format (YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS).
41
+
42
+ Args:
43
+ date_string: Date string to parse
44
+
45
+ Returns:
46
+ datetime: Parsed datetime object
47
+
48
+ Raises:
49
+ argparse.ArgumentTypeError: If date format is invalid
50
+ """
51
+ try:
52
+ # Try parsing with time first
53
+ return datetime.fromisoformat(date_string)
54
+ except ValueError:
55
+ # Try parsing as date only (YYYY-MM-DD)
56
+ try:
57
+ return datetime.strptime(date_string, "%Y-%m-%d")
58
+ except ValueError:
59
+ raise argparse.ArgumentTypeError(
60
+ f"Invalid date format: '{date_string}'. "
61
+ f"Use ISO format: YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS"
62
+ )
63
+
64
+
65
+ def create_parser() -> argparse.ArgumentParser:
66
+ """
67
+ Create and configure the argument parser for the CLI.
68
+
69
+ Returns:
70
+ argparse.ArgumentParser: Configured parser with all arguments
71
+ """
72
+ parser = argparse.ArgumentParser(
73
+ prog='git-recap',
74
+ description=(
75
+ 'GitRecap CLI - Fetch and summarize git commits from local repositories.\n\n'
76
+ 'This tool aggregates commit history from multiple local git repositories, '
77
+ 'filters by author and date range, and outputs structured text summaries. '
78
+ 'Designed for easy integration with LLMs and automated workflows.'
79
+ ),
80
+ epilog=(
81
+ 'Examples:\n'
82
+ ' git-recap . # Current directory, last 7 days\n'
83
+ ' git-recap /path/to/repo1 /path/to/repo2 # Multiple repositories\n'
84
+ ' git-recap . --author "John Doe" # Filter by author\n'
85
+ ' git-recap . --start-date "2025-01-01" # From specific date\n'
86
+ ' git-recap . --output summary.txt # Save to file\n'
87
+ ' git-recap . --author "Jane" --start-date "2025-01-01" --end-date "2025-01-31" --output commits.txt'
88
+ ),
89
+ formatter_class=argparse.RawDescriptionHelpFormatter
90
+ )
91
+
92
+ parser.add_argument(
93
+ 'paths',
94
+ nargs='+',
95
+ help=(
96
+ 'One or more paths to local git repositories. '
97
+ 'Each path must be a valid git repository (contains .git directory). '
98
+ 'Can be absolute or relative paths. Multiple paths can be provided.'
99
+ )
100
+ )
101
+
102
+ parser.add_argument(
103
+ '--author',
104
+ type=str,
105
+ help=(
106
+ 'Filter commits by author name. '
107
+ 'Partial matching is supported (e.g., "John" matches "John Doe"). '
108
+ 'If not specified, commits from all authors are included.'
109
+ )
110
+ )
111
+
112
+ parser.add_argument(
113
+ '--start-date',
114
+ type=parse_date,
115
+ help=(
116
+ 'Start date for filtering commits (inclusive). '
117
+ 'Format: YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS. '
118
+ 'If not specified, defaults to 7 days before current date.'
119
+ )
120
+ )
121
+
122
+ parser.add_argument(
123
+ '--end-date',
124
+ type=parse_date,
125
+ help=(
126
+ 'End date for filtering commits (inclusive). '
127
+ 'Format: YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS. '
128
+ 'If not specified, defaults to current date and time.'
129
+ )
130
+ )
131
+
132
+ parser.add_argument(
133
+ '--output', '-o',
134
+ type=str,
135
+ help=(
136
+ 'Output file path to save the summary. '
137
+ 'If not specified, results are printed to stdout. '
138
+ 'The file will be created or overwritten if it exists.'
139
+ )
140
+ )
141
+
142
+ return parser
143
+
144
+
145
+ def filter_entries_by_author(
146
+ entries: List[Dict[str, Any]],
147
+ author: str
148
+ ) -> List[Dict[str, Any]]:
149
+ """
150
+ Filter entries by author name (case-insensitive partial match).
151
+
152
+ Args:
153
+ entries: List of commit entries
154
+ author: Author name to filter by
155
+
156
+ Returns:
157
+ List[Dict[str, Any]]: Filtered entries matching the author
158
+ """
159
+ author_lower = author.lower()
160
+ return [
161
+ entry for entry in entries
162
+ if author_lower in entry.get('author', '').lower()
163
+ ]
164
+
165
+
166
+ def fetch_from_repos(
167
+ repo_paths: List[str],
168
+ authors: Optional[List[str]] = None,
169
+ start_date: Optional[datetime] = None,
170
+ end_date: Optional[datetime] = None
171
+ ) -> List[Dict[str, Any]]:
172
+ """
173
+ Fetch commits from multiple local repositories.
174
+
175
+ Args:
176
+ repo_paths: List of repository paths
177
+ authors: Optional list of author names to filter by
178
+ start_date: Optional start date for filtering
179
+ end_date: Optional end date for filtering
180
+
181
+ Returns:
182
+ List[Dict[str, Any]]: Aggregated list of commit entries from all repos
183
+ """
184
+ all_entries = []
185
+
186
+ for repo_path in repo_paths:
187
+ try:
188
+ print(f"Fetching from: {repo_path}", file=sys.stderr)
189
+ fetcher = LocalFetcher(
190
+ repo_path=repo_path,
191
+ authors=authors,
192
+ start_date=start_date,
193
+ end_date=end_date,
194
+ validate_repo=True
195
+ )
196
+ entries = fetcher.fetch_commits()
197
+ all_entries.extend(entries)
198
+ print(f" Found {len(entries)} commits", file=sys.stderr)
199
+ except ValueError as e:
200
+ print(f"Error: {e}", file=sys.stderr)
201
+ continue
202
+ except Exception as e:
203
+ print(f"Unexpected error processing {repo_path}: {e}", file=sys.stderr)
204
+ continue
205
+
206
+ return all_entries
207
+
208
+
209
+ def main() -> int:
210
+ """
211
+ Main entry point for the CLI.
212
+
213
+ Returns:
214
+ int: Exit code (0 for success, 1 for error)
215
+ """
216
+ parser = create_parser()
217
+ args = parser.parse_args()
218
+
219
+ # Set default date range if not provided
220
+ if not args.start_date and not args.end_date:
221
+ # Default: last 7 days
222
+ from datetime import timedelta
223
+ args.end_date = datetime.now()
224
+ args.start_date = args.end_date - timedelta(days=7)
225
+ elif not args.start_date:
226
+ # Only end date provided, start from 7 days before end
227
+ from datetime import timedelta
228
+ args.start_date = args.end_date - timedelta(days=7)
229
+ elif not args.end_date:
230
+ # Only start date provided, use current time as end
231
+ args.end_date = datetime.now()
232
+
233
+ # Prepare authors list
234
+ authors = [args.author] if args.author else None
235
+
236
+ # Fetch commits from all repositories
237
+ entries = fetch_from_repos(
238
+ repo_paths=args.paths,
239
+ authors=authors,
240
+ start_date=args.start_date,
241
+ end_date=args.end_date
242
+ )
243
+
244
+ # Check if we found any entries
245
+ if not entries:
246
+ print("No commits found matching the specified criteria.", file=sys.stderr)
247
+ return 0
248
+
249
+ # Convert entries to text format
250
+ output_text = parse_entries_to_txt(entries)
251
+
252
+ # Output to file or stdout
253
+ if args.output:
254
+ try:
255
+ output_path = Path(args.output)
256
+ # Create parent directories if they don't exist
257
+ output_path.parent.mkdir(parents=True, exist_ok=True)
258
+ output_path.write_text(output_text, encoding='utf-8')
259
+ print(f"Summary saved to: {args.output}", file=sys.stderr)
260
+ except Exception as e:
261
+ print(f"Error writing to file {args.output}: {e}", file=sys.stderr)
262
+ return 1
263
+ else:
264
+ print(output_text)
265
+
266
+ return 0
267
+
268
+
269
+ if __name__ == '__main__':
270
+ sys.exit(main())
git_recap/fetcher.py CHANGED
@@ -3,6 +3,7 @@ from datetime import datetime, timedelta
3
3
  from git_recap.providers.github_fetcher import GitHubFetcher
4
4
  from git_recap.providers.azure_fetcher import AzureFetcher
5
5
  from git_recap.providers.gitlab_fetcher import GitLabFetcher
6
+ from git_recap.providers.local_fetcher import LocalFetcher
6
7
 
7
8
  def main():
8
9
  parser = argparse.ArgumentParser(
@@ -11,10 +12,14 @@ def main():
11
12
  parser.add_argument(
12
13
  '--provider',
13
14
  required=True,
14
- choices=['github', 'azure', 'gitlab'],
15
- help='Platform name (github, azure, or gitlab)'
15
+ choices=['github', 'azure', 'gitlab', 'local'],
16
+ help='Platform name (github, azure, gitlab, or local)'
17
+ )
18
+ parser.add_argument('--pat', help='Personal Access Token (not required for local provider)')
19
+ parser.add_argument(
20
+ '--repo-path',
21
+ help='Path to local git repository (required for local provider)'
16
22
  )
17
- parser.add_argument('--pat', required=True, help='Personal Access Token')
18
23
  parser.add_argument(
19
24
  '--organization-url',
20
25
  help='Organization URL for Azure DevOps'
@@ -51,6 +56,9 @@ def main():
51
56
 
52
57
  fetcher = None
53
58
  if args.provider == 'github':
59
+ if not args.pat:
60
+ print("PAT is required for GitHub provider")
61
+ exit(1)
54
62
  fetcher = GitHubFetcher(
55
63
  pat=args.pat,
56
64
  start_date=args.start_date,
@@ -58,6 +66,9 @@ def main():
58
66
  repo_filter=args.repos
59
67
  )
60
68
  elif args.provider == 'azure':
69
+ if not args.pat:
70
+ print("PAT is required for Azure DevOps provider")
71
+ exit(1)
61
72
  if not args.organization_url:
62
73
  print("Organization URL is required for Azure DevOps")
63
74
  exit(1)
@@ -69,6 +80,9 @@ def main():
69
80
  repo_filter=args.repos
70
81
  )
71
82
  elif args.provider == 'gitlab':
83
+ if not args.pat:
84
+ print("PAT is required for GitLab provider")
85
+ exit(1)
72
86
  gitlab_url = args.gitlab_url if args.gitlab_url else 'https://gitlab.com'
73
87
  fetcher = GitLabFetcher(
74
88
  pat=args.pat,
@@ -77,6 +91,16 @@ def main():
77
91
  end_date=args.end_date,
78
92
  repo_filter=args.repos
79
93
  )
94
+ elif args.provider == 'local':
95
+ if not args.repo_path:
96
+ print("--repo-path is required for local provider")
97
+ exit(1)
98
+ fetcher = LocalFetcher(
99
+ repo_path=args.repo_path,
100
+ start_date=args.start_date,
101
+ end_date=args.end_date,
102
+ repo_filter=args.repos
103
+ )
80
104
 
81
105
  messages = fetcher.get_authored_messages(limit=args.limit)
82
106
  for msg in messages:
@@ -2,10 +2,12 @@ from git_recap.providers.azure_fetcher import AzureFetcher
2
2
  from git_recap.providers.github_fetcher import GitHubFetcher
3
3
  from git_recap.providers.gitlab_fetcher import GitLabFetcher
4
4
  from git_recap.providers.url_fetcher import URLFetcher
5
+ from git_recap.providers.local_fetcher import LocalFetcher
5
6
 
6
7
  __all__ = [
7
8
  "AzureFetcher",
8
9
  "GitHubFetcher",
9
10
  "GitLabFetcher",
10
- "URLFetcher"
11
+ "URLFetcher",
12
+ "LocalFetcher"
11
13
  ]
@@ -1,14 +1,16 @@
1
1
  from azure.devops.connection import Connection
2
2
  from msrest.authentication import BasicAuthentication
3
+ from azure.devops.exceptions import AzureDevOpsServiceError
3
4
  from datetime import datetime
4
- from typing import List, Dict, Any
5
+ from typing import List, Dict, Any, Optional
5
6
  from git_recap.providers.base_fetcher import BaseFetcher
6
7
 
8
+
7
9
  class AzureFetcher(BaseFetcher):
8
10
  """
9
11
  Fetcher implementation for Azure DevOps repositories.
10
12
 
11
- Supports fetching commits, pull requests, and issues.
13
+ Supports fetching commits, pull requests, issues, and authors.
12
14
  Release fetching is not supported and will raise NotImplementedError.
13
15
  """
14
16
 
@@ -30,9 +32,12 @@ class AzureFetcher(BaseFetcher):
30
32
  self.connection = Connection(base_url=self.organization_url, creds=credentials)
31
33
  self.core_client = self.connection.clients.get_core_client()
32
34
  self.git_client = self.connection.clients.get_git_client()
35
+
36
+ # Extract project name from organization URL or use first project
37
+ projects = self.core_client.get_projects().value
38
+ self.project_name = projects[0].name if projects else None
39
+
33
40
  self.repos = self.get_repos()
34
- # Azure DevOps doesn't provide an affiliation filter;
35
- # we'll iterate over all repos in each project.
36
41
  if authors is None:
37
42
  self.authors = []
38
43
 
@@ -43,8 +48,10 @@ class AzureFetcher(BaseFetcher):
43
48
  List of repository objects.
44
49
  """
45
50
  projects = self.core_client.get_projects().value
46
- # Get all repositories in each project
47
- repos = [self.git_client.get_repositories(project.id) for project in projects]
51
+ repos = []
52
+ for project in projects:
53
+ project_repos = self.git_client.get_repositories(project.id)
54
+ repos.extend(project_repos)
48
55
  return repos
49
56
 
50
57
  @property
@@ -55,8 +62,7 @@ class AzureFetcher(BaseFetcher):
55
62
  Returns:
56
63
  List[str]: List of repository names.
57
64
  """
58
- # To be implemented if needed for UI or listing.
59
- ...
65
+ return [repo.name for repo in self.repos]
60
66
 
61
67
  def _filter_by_date(self, date_obj: datetime) -> bool:
62
68
  """
@@ -103,15 +109,14 @@ class AzureFetcher(BaseFetcher):
103
109
  for author in self.authors:
104
110
  try:
105
111
  commits = self.git_client.get_commits(
106
- project=repo.id,
112
+ project=repo.project.id,
107
113
  repository_id=repo.id,
108
114
  search_criteria={"author": author}
109
115
  )
110
116
  except Exception:
111
117
  continue
112
118
  for commit in commits:
113
- # Azure DevOps returns a commit with an 'author' property.
114
- commit_date = commit.author.date # assumed datetime
119
+ commit_date = commit.author.date
115
120
  if self._filter_by_date(commit_date):
116
121
  sha = commit.commit_id
117
122
  if sha not in processed_commits:
@@ -151,10 +156,9 @@ class AzureFetcher(BaseFetcher):
151
156
  except Exception:
152
157
  continue
153
158
  for pr in pull_requests:
154
- # Check that the PR creator is one of our authors.
155
159
  if pr.created_by.unique_name not in self.authors:
156
160
  continue
157
- pr_date = pr.creation_date # type: datetime
161
+ pr_date = pr.creation_date
158
162
  if not self._filter_by_date(pr_date):
159
163
  continue
160
164
 
@@ -204,7 +208,6 @@ class AzureFetcher(BaseFetcher):
204
208
  """
205
209
  entries = []
206
210
  wit_client = self.connection.clients.get_work_item_tracking_client()
207
- # Query work items for each author using a simplified WIQL query.
208
211
  for author in self.authors:
209
212
  wiql = f"SELECT [System.Id], [System.Title], [System.CreatedDate] FROM WorkItems WHERE [System.AssignedTo] CONTAINS '{author}'"
210
213
  try:
@@ -235,5 +238,136 @@ class AzureFetcher(BaseFetcher):
235
238
  Raises:
236
239
  NotImplementedError: Always, since release fetching is not supported for AzureFetcher.
237
240
  """
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).")
241
+ raise NotImplementedError("Release fetching is not supported for Azure DevOps (AzureFetcher).")
242
+
243
+ def get_branches(self) -> List[str]:
244
+ """
245
+ Get all branches in the repository.
246
+
247
+ Returns:
248
+ List[str]: List of branch names.
249
+
250
+ Raises:
251
+ NotImplementedError: Always, since branch listing is not yet implemented for AzureFetcher.
252
+ """
253
+ raise NotImplementedError("Branch listing is not yet implemented for Azure DevOps (AzureFetcher).")
254
+
255
+ def get_valid_target_branches(self, source_branch: str) -> List[str]:
256
+ """
257
+ Get branches that can receive a pull request from the source branch.
258
+
259
+ Validates that the source branch exists, filters out branches with existing
260
+ open PRs from source, excludes the source branch itself, and optionally
261
+ checks if source is ahead of target.
262
+
263
+ Args:
264
+ source_branch (str): The source branch name.
265
+
266
+ Returns:
267
+ List[str]: List of valid target branch names.
268
+
269
+ Raises:
270
+ NotImplementedError: Always, since PR target validation is not yet implemented for AzureFetcher.
271
+ """
272
+ raise NotImplementedError("Pull request target branch validation is not yet implemented for Azure DevOps (AzureFetcher).")
273
+
274
+ def create_pull_request(
275
+ self,
276
+ head_branch: str,
277
+ base_branch: str,
278
+ title: str,
279
+ body: str,
280
+ draft: bool = False,
281
+ reviewers: Optional[List[str]] = None,
282
+ assignees: Optional[List[str]] = None,
283
+ labels: Optional[List[str]] = None
284
+ ) -> Dict[str, Any]:
285
+ """
286
+ Create a pull request between two branches with optional metadata.
287
+
288
+ Args:
289
+ head_branch: Source branch for the PR.
290
+ base_branch: Target branch for the PR.
291
+ title: PR title.
292
+ body: PR description.
293
+ draft: Whether to create as draft PR (default: False).
294
+ reviewers: List of reviewer usernames (optional).
295
+ assignees: List of assignee usernames (optional).
296
+ labels: List of label names (optional).
297
+
298
+ Returns:
299
+ Dict[str, Any]: Dictionary containing PR metadata (url, number, state, success) or error information.
300
+
301
+ Raises:
302
+ NotImplementedError: Always, since PR creation is not yet implemented for AzureFetcher.
303
+ """
304
+ raise NotImplementedError("Pull request creation is not yet implemented for Azure DevOps (AzureFetcher).")
305
+
306
+ def get_authors(self, repo_names: List[str]) -> List[Dict[str, str]]:
307
+ """
308
+ Retrieve unique authors from specified Azure DevOps repositories.
309
+
310
+ Args:
311
+ repo_names: List of repository names.
312
+ Empty list fetches from all accessible repositories.
313
+
314
+ Returns:
315
+ List of unique author dictionaries with name and email.
316
+ """
317
+ authors_set = set()
318
+
319
+ try:
320
+ git_client = self.connection.clients.get_git_client()
321
+
322
+ if not repo_names:
323
+ repos = self.repos
324
+ else:
325
+ repos = [repo for repo in self.repos if repo.name in repo_names]
326
+
327
+ for repo in repos:
328
+ if self.repo_filter and repo.name not in self.repo_filter:
329
+ continue
330
+
331
+ try:
332
+ commits = git_client.get_commits(
333
+ repository_id=repo.id,
334
+ search_criteria={'$top': 1000}
335
+ )
336
+
337
+ for commit in commits:
338
+ if commit.author:
339
+ author_name = commit.author.name or "Unknown"
340
+ author_email = commit.author.email or "unknown@example.com"
341
+ authors_set.add((author_name, author_email))
342
+
343
+ if commit.committer:
344
+ committer_name = commit.committer.name or "Unknown"
345
+ committer_email = commit.committer.email or "unknown@example.com"
346
+ authors_set.add((committer_name, committer_email))
347
+
348
+ except AzureDevOpsServiceError as e:
349
+ print(f"Error fetching authors from {repo.name}: {e}")
350
+ continue
351
+
352
+ authors_list = [
353
+ {"name": name, "email": email}
354
+ for name, email in sorted(authors_set)
355
+ ]
356
+
357
+ return authors_list
358
+
359
+ except Exception as e:
360
+ print(f"Error in get_authors: {e}")
361
+ return []
362
+
363
+ def get_current_author(self) -> Optional[Dict[str, str]]:
364
+ """
365
+ Retrieve the current authenticated user's information.
366
+
367
+ For Azure DevOps, default author functionality is not currently implemented,
368
+ so this method returns None.
369
+
370
+ Returns:
371
+ None: Azure DevOps fetcher does not support default author retrieval.
372
+ """
373
+ return None
@@ -90,6 +90,102 @@ 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
+
159
+ @abstractmethod
160
+ def get_authors(self, repo_names: List[str]) -> List[Dict[str, str]]:
161
+ """
162
+ Retrieve unique authors from specified repositories.
163
+
164
+ Args:
165
+ repo_names: List of repository names to fetch authors from.
166
+ Empty list means fetch from all available repositories.
167
+
168
+ Returns:
169
+ List of dictionaries containing author information:
170
+ [{"name": "John Doe", "email": "john@example.com"}, ...]
171
+ """
172
+ pass
173
+
174
+ @abstractmethod
175
+ def get_current_author(self) -> Optional[Dict[str, str]]:
176
+ """
177
+ Retrieve the current authenticated user's information.
178
+
179
+ Returns the default authenticated user's name and email if available
180
+ for the current fetcher session, or None if not applicable for the provider.
181
+
182
+ Returns:
183
+ Optional[Dict[str, str]]: Dictionary with 'name' and 'email' keys,
184
+ or None if no default author is available.
185
+ Example: {"name": "John Doe", "email": "john@example.com"}
186
+ """
187
+ pass
188
+
93
189
  def get_authored_messages(self) -> List[Dict[str, Any]]:
94
190
  """
95
191
  Aggregates all commit, pull request, and issue entries into a single list,