git-recap 0.1.5__tar.gz → 0.1.6__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 (28) hide show
  1. {git_recap-0.1.5 → git_recap-0.1.6}/PKG-INFO +15 -10
  2. git_recap-0.1.6/git_recap/cli.py +270 -0
  3. {git_recap-0.1.5 → git_recap-0.1.6}/git_recap/fetcher.py +27 -3
  4. {git_recap-0.1.5 → git_recap-0.1.6}/git_recap/providers/__init__.py +3 -1
  5. {git_recap-0.1.5 → git_recap-0.1.6}/git_recap/providers/azure_fetcher.py +86 -29
  6. {git_recap-0.1.5 → git_recap-0.1.6}/git_recap/providers/base_fetcher.py +30 -0
  7. {git_recap-0.1.5 → git_recap-0.1.6}/git_recap/providers/github_fetcher.py +79 -11
  8. {git_recap-0.1.5 → git_recap-0.1.6}/git_recap/providers/gitlab_fetcher.py +67 -2
  9. git_recap-0.1.6/git_recap/providers/local_fetcher.py +390 -0
  10. {git_recap-0.1.5 → git_recap-0.1.6}/git_recap/providers/url_fetcher.py +88 -12
  11. {git_recap-0.1.5 → git_recap-0.1.6}/git_recap.egg-info/PKG-INFO +15 -10
  12. {git_recap-0.1.5 → git_recap-0.1.6}/git_recap.egg-info/SOURCES.txt +6 -1
  13. git_recap-0.1.6/git_recap.egg-info/entry_points.txt +2 -0
  14. git_recap-0.1.6/git_recap.egg-info/requires.txt +10 -0
  15. git_recap-0.1.6/pyproject.toml +63 -0
  16. git_recap-0.1.6/tests/test_cli.py +341 -0
  17. {git_recap-0.1.5 → git_recap-0.1.6}/tests/test_dummy_parser.py +6 -0
  18. git_recap-0.1.6/tests/test_local_fetcher.py +498 -0
  19. {git_recap-0.1.5 → git_recap-0.1.6}/tests/test_parser.py +123 -1
  20. git_recap-0.1.5/git_recap.egg-info/requires.txt +0 -3
  21. git_recap-0.1.5/setup.py +0 -25
  22. {git_recap-0.1.5 → git_recap-0.1.6}/LICENSE +0 -0
  23. {git_recap-0.1.5 → git_recap-0.1.6}/README.md +0 -0
  24. {git_recap-0.1.5 → git_recap-0.1.6}/git_recap/__init__.py +0 -0
  25. {git_recap-0.1.5 → git_recap-0.1.6}/git_recap/utils.py +0 -0
  26. {git_recap-0.1.5 → git_recap-0.1.6}/git_recap.egg-info/dependency_links.txt +0 -0
  27. {git_recap-0.1.5 → git_recap-0.1.6}/git_recap.egg-info/top_level.txt +0 -0
  28. {git_recap-0.1.5 → git_recap-0.1.6}/setup.cfg +0 -0
@@ -1,25 +1,30 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: git-recap
3
- Version: 0.1.5
3
+ Version: 0.1.6
4
4
  Summary: A modular Python tool that aggregates and formats user-authored messages from repositories.
5
- Author: Bruno V.
6
- Author-email: bruno.vitorino@tecnico.ulisboa.pt
5
+ Author-email: "Bruno V." <bruno.vitorino@tecnico.ulisboa.pt>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/BrunoV21/GitRecap
8
+ Project-URL: Documentation, https://github.com/BrunoV21/GitRecap#readme
9
+ Project-URL: Repository, https://github.com/BrunoV21/GitRecap.git
10
+ Project-URL: Issues, https://github.com/BrunoV21/GitRecap/issues
11
+ Keywords: git,github,gitlab,azure-devops,version-control,repository
7
12
  Classifier: Programming Language :: Python :: 3
8
13
  Classifier: License :: OSI Approved :: MIT License
9
14
  Classifier: Operating System :: OS Independent
15
+ Requires-Python: >=3.10
10
16
  Description-Content-Type: text/markdown
11
17
  License-File: LICENSE
12
18
  Requires-Dist: PyGithub==2.6.1
13
19
  Requires-Dist: azure-devops==7.1.0b4
14
20
  Requires-Dist: python-gitlab==5.6.0
15
- Dynamic: author
16
- Dynamic: author-email
17
- Dynamic: classifier
18
- Dynamic: description
19
- Dynamic: description-content-type
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
23
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
24
+ Requires-Dist: black>=22.0.0; extra == "dev"
25
+ Requires-Dist: flake8>=5.0.0; extra == "dev"
26
+ Requires-Dist: mypy>=0.990; extra == "dev"
20
27
  Dynamic: license-file
21
- Dynamic: requires-dist
22
- Dynamic: summary
23
28
 
24
29
  <a href="https://www.uneed.best/tool/gitrecap">
25
30
  <img src="https://www.uneed.best/POTD2A.png" style="width: 250px;" alt="Uneed POTD2 Badge" />
@@ -0,0 +1,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())
@@ -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
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,7 +238,6 @@ 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
241
  raise NotImplementedError("Release fetching is not supported for Azure DevOps (AzureFetcher).")
240
242
 
241
243
  def get_branches(self) -> List[str]:
@@ -248,9 +250,6 @@ class AzureFetcher(BaseFetcher):
248
250
  Raises:
249
251
  NotImplementedError: Always, since branch listing is not yet implemented for AzureFetcher.
250
252
  """
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
253
  raise NotImplementedError("Branch listing is not yet implemented for Azure DevOps (AzureFetcher).")
255
254
 
256
255
  def get_valid_target_branches(self, source_branch: str) -> List[str]:
@@ -270,14 +269,6 @@ class AzureFetcher(BaseFetcher):
270
269
  Raises:
271
270
  NotImplementedError: Always, since PR target validation is not yet implemented for AzureFetcher.
272
271
  """
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
272
  raise NotImplementedError("Pull request target branch validation is not yet implemented for Azure DevOps (AzureFetcher).")
282
273
 
283
274
  def create_pull_request(
@@ -310,7 +301,73 @@ class AzureFetcher(BaseFetcher):
310
301
  Raises:
311
302
  NotImplementedError: Always, since PR creation is not yet implemented for AzureFetcher.
312
303
  """
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).")
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
@@ -156,6 +156,36 @@ class BaseFetcher(ABC):
156
156
  """
157
157
  raise NotImplementedError("Subclasses must implement create_pull_request() to create a pull request with the specified parameters")
158
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
+
159
189
  def get_authored_messages(self) -> List[Dict[str, Any]]:
160
190
  """
161
191
  Aggregates all commit, pull request, and issue entries into a single list,