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.
- {git_recap-0.1.5 → git_recap-0.1.6}/PKG-INFO +15 -10
- git_recap-0.1.6/git_recap/cli.py +270 -0
- {git_recap-0.1.5 → git_recap-0.1.6}/git_recap/fetcher.py +27 -3
- {git_recap-0.1.5 → git_recap-0.1.6}/git_recap/providers/__init__.py +3 -1
- {git_recap-0.1.5 → git_recap-0.1.6}/git_recap/providers/azure_fetcher.py +86 -29
- {git_recap-0.1.5 → git_recap-0.1.6}/git_recap/providers/base_fetcher.py +30 -0
- {git_recap-0.1.5 → git_recap-0.1.6}/git_recap/providers/github_fetcher.py +79 -11
- {git_recap-0.1.5 → git_recap-0.1.6}/git_recap/providers/gitlab_fetcher.py +67 -2
- git_recap-0.1.6/git_recap/providers/local_fetcher.py +390 -0
- {git_recap-0.1.5 → git_recap-0.1.6}/git_recap/providers/url_fetcher.py +88 -12
- {git_recap-0.1.5 → git_recap-0.1.6}/git_recap.egg-info/PKG-INFO +15 -10
- {git_recap-0.1.5 → git_recap-0.1.6}/git_recap.egg-info/SOURCES.txt +6 -1
- git_recap-0.1.6/git_recap.egg-info/entry_points.txt +2 -0
- git_recap-0.1.6/git_recap.egg-info/requires.txt +10 -0
- git_recap-0.1.6/pyproject.toml +63 -0
- git_recap-0.1.6/tests/test_cli.py +341 -0
- {git_recap-0.1.5 → git_recap-0.1.6}/tests/test_dummy_parser.py +6 -0
- git_recap-0.1.6/tests/test_local_fetcher.py +498 -0
- {git_recap-0.1.5 → git_recap-0.1.6}/tests/test_parser.py +123 -1
- git_recap-0.1.5/git_recap.egg-info/requires.txt +0 -3
- git_recap-0.1.5/setup.py +0 -25
- {git_recap-0.1.5 → git_recap-0.1.6}/LICENSE +0 -0
- {git_recap-0.1.5 → git_recap-0.1.6}/README.md +0 -0
- {git_recap-0.1.5 → git_recap-0.1.6}/git_recap/__init__.py +0 -0
- {git_recap-0.1.5 → git_recap-0.1.6}/git_recap/utils.py +0 -0
- {git_recap-0.1.5 → git_recap-0.1.6}/git_recap.egg-info/dependency_links.txt +0 -0
- {git_recap-0.1.5 → git_recap-0.1.6}/git_recap.egg-info/top_level.txt +0 -0
- {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.
|
|
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
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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,
|