git-recap 0.1.3__tar.gz → 0.1.4__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.3 → git_recap-0.1.4}/PKG-INFO +9 -1
  2. {git_recap-0.1.3 → git_recap-0.1.4}/README.md +9 -1
  3. {git_recap-0.1.3 → git_recap-0.1.4}/git_recap/providers/azure_fetcher.py +80 -4
  4. {git_recap-0.1.3 → git_recap-0.1.4}/git_recap/providers/base_fetcher.py +64 -8
  5. {git_recap-0.1.3 → git_recap-0.1.4}/git_recap/providers/github_fetcher.py +61 -4
  6. {git_recap-0.1.3 → git_recap-0.1.4}/git_recap/providers/gitlab_fetcher.py +63 -5
  7. {git_recap-0.1.3 → git_recap-0.1.4}/git_recap/providers/url_fetcher.py +24 -13
  8. git_recap-0.1.4/git_recap/utils.py +147 -0
  9. {git_recap-0.1.3 → git_recap-0.1.4}/git_recap.egg-info/PKG-INFO +9 -1
  10. {git_recap-0.1.3 → git_recap-0.1.4}/setup.py +1 -1
  11. {git_recap-0.1.3 → git_recap-0.1.4}/tests/test_dummy_parser.py +3 -0
  12. git_recap-0.1.4/tests/test_parser.py +237 -0
  13. git_recap-0.1.3/git_recap/utils.py +0 -93
  14. git_recap-0.1.3/tests/test_parser.py +0 -52
  15. {git_recap-0.1.3 → git_recap-0.1.4}/LICENSE +0 -0
  16. {git_recap-0.1.3 → git_recap-0.1.4}/git_recap/__init__.py +0 -0
  17. {git_recap-0.1.3 → git_recap-0.1.4}/git_recap/fetcher.py +0 -0
  18. {git_recap-0.1.3 → git_recap-0.1.4}/git_recap/providers/__init__.py +0 -0
  19. {git_recap-0.1.3 → git_recap-0.1.4}/git_recap.egg-info/SOURCES.txt +0 -0
  20. {git_recap-0.1.3 → git_recap-0.1.4}/git_recap.egg-info/dependency_links.txt +0 -0
  21. {git_recap-0.1.3 → git_recap-0.1.4}/git_recap.egg-info/requires.txt +0 -0
  22. {git_recap-0.1.3 → git_recap-0.1.4}/git_recap.egg-info/top_level.txt +0 -0
  23. {git_recap-0.1.3 → git_recap-0.1.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: git-recap
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Summary: A modular Python tool that aggregates and formats user-authored messages from repositories.
5
5
  Author: Bruno V.
6
6
  Author-email: bruno.vitorino@tecnico.ulisboa.pt
@@ -21,8 +21,16 @@ Dynamic: license-file
21
21
  Dynamic: requires-dist
22
22
  Dynamic: summary
23
23
 
24
+ <a href="https://www.uneed.best/tool/gitrecap">
25
+ <img src="https://www.uneed.best/POTD2A.png" style="width: 250px;" alt="Uneed POTD2 Badge" />
26
+ </a>
27
+
28
+ ---
29
+
24
30
  # Git Recap
25
31
 
32
+ 🎉 **Featured in Uneed's Latest Newsletter as a Staff Pick!** 🎉
33
+
26
34
  Git Recap is a modular Python tool that aggregates and formats user-authored messages from repositories hosted on GitHub, Azure DevOps, and GitLab. It fetches commit messages, pull requests (along with their associated commits), and issues, then consolidates and sorts these events into a clear, chronological summary. This summary is output as a plain text string that can serve as context for large language models or other analysis tools.
27
35
 
28
36
  ## Features
@@ -1,5 +1,13 @@
1
+ <a href="https://www.uneed.best/tool/gitrecap">
2
+ <img src="https://www.uneed.best/POTD2A.png" style="width: 250px;" alt="Uneed POTD2 Badge" />
3
+ </a>
4
+
5
+ ---
6
+
1
7
  # Git Recap
2
8
 
9
+ 🎉 **Featured in Uneed's Latest Newsletter as a Staff Pick!** 🎉
10
+
3
11
  Git Recap is a modular Python tool that aggregates and formats user-authored messages from repositories hosted on GitHub, Azure DevOps, and GitLab. It fetches commit messages, pull requests (along with their associated commits), and issues, then consolidates and sorts these events into a clear, chronological summary. This summary is output as a plain text string that can serve as context for large language models or other analysis tools.
4
12
 
5
13
  ## Features
@@ -110,4 +118,4 @@ This project is licensed under the terms of the [MIT License](LICENSE).
110
118
 
111
119
  - [PyGitHub](https://pygithub.readthedocs.io/en/stable/)
112
120
  - [Azure DevOps Python API](https://github.com/microsoft/azure-devops-python-api)
113
- - [python-gitlab](https://python-gitlab.readthedocs.io/)
121
+ - [python-gitlab](https://python-gitlab.readthedocs.io/)
@@ -5,8 +5,25 @@ from typing import List, Dict, Any
5
5
  from git_recap.providers.base_fetcher import BaseFetcher
6
6
 
7
7
  class AzureFetcher(BaseFetcher):
8
+ """
9
+ Fetcher implementation for Azure DevOps repositories.
10
+
11
+ Supports fetching commits, pull requests, and issues.
12
+ Release fetching is not supported and will raise NotImplementedError.
13
+ """
14
+
8
15
  def __init__(self, pat: str, organization_url: str, start_date=None, end_date=None, repo_filter=None, authors=None):
9
- # authors should be passed as a list of unique names (e.g., email or unique id)
16
+ """
17
+ Initialize the AzureFetcher.
18
+
19
+ Args:
20
+ pat (str): Personal Access Token for Azure DevOps.
21
+ organization_url (str): The Azure DevOps organization URL.
22
+ start_date (datetime, optional): Start date for filtering entries.
23
+ end_date (datetime, optional): End date for filtering entries.
24
+ repo_filter (List[str], optional): List of repository names to filter.
25
+ authors (List[str], optional): List of author identifiers (e.g., email or unique id).
26
+ """
10
27
  super().__init__(pat, start_date, end_date, repo_filter, authors)
11
28
  self.organization_url = organization_url
12
29
  credentials = BasicAuthentication('', self.pat)
@@ -20,17 +37,37 @@ class AzureFetcher(BaseFetcher):
20
37
  self.authors = []
21
38
 
22
39
  def get_repos(self):
40
+ """
41
+ Retrieve all repositories in all projects for the organization.
42
+ Returns:
43
+ List of repository objects.
44
+ """
23
45
  projects = self.core_client.get_projects().value
24
46
  # Get all repositories in each project
25
47
  repos = [self.git_client.get_repositories(project.id) for project in projects]
26
48
  return repos
27
49
 
28
50
  @property
29
- def repos_names(self)->List[str]:
30
- "to be implemented later"
51
+ def repos_names(self) -> List[str]:
52
+ """
53
+ Return the list of repository names.
54
+
55
+ Returns:
56
+ List[str]: List of repository names.
57
+ """
58
+ # To be implemented if needed for UI or listing.
31
59
  ...
32
60
 
33
61
  def _filter_by_date(self, date_obj: datetime) -> bool:
62
+ """
63
+ Check if a datetime object is within the configured date range.
64
+
65
+ Args:
66
+ date_obj (datetime): The datetime to check.
67
+
68
+ Returns:
69
+ bool: True if within range, False otherwise.
70
+ """
34
71
  if self.start_date and date_obj < self.start_date:
35
72
  return False
36
73
  if self.end_date and date_obj > self.end_date:
@@ -38,11 +75,26 @@ class AzureFetcher(BaseFetcher):
38
75
  return True
39
76
 
40
77
  def _stop_fetching(self, date_obj: datetime) -> bool:
78
+ """
79
+ Determine if fetching should stop based on the date.
80
+
81
+ Args:
82
+ date_obj (datetime): The datetime to check.
83
+
84
+ Returns:
85
+ bool: True if should stop, False otherwise.
86
+ """
41
87
  if self.start_date and date_obj < self.start_date:
42
88
  return True
43
89
  return False
44
90
 
45
91
  def fetch_commits(self) -> List[Dict[str, Any]]:
92
+ """
93
+ Fetch commits for all repositories and authors.
94
+
95
+ Returns:
96
+ List[Dict[str, Any]]: List of commit entries.
97
+ """
46
98
  entries = []
47
99
  processed_commits = set()
48
100
  for repo in self.repos:
@@ -77,6 +129,12 @@ class AzureFetcher(BaseFetcher):
77
129
  return entries
78
130
 
79
131
  def fetch_pull_requests(self) -> List[Dict[str, Any]]:
132
+ """
133
+ Fetch pull requests and their associated commits for all repositories and authors.
134
+
135
+ Returns:
136
+ List[Dict[str, Any]]: List of pull request and commit_from_pr entries.
137
+ """
80
138
  entries = []
81
139
  processed_pr_commits = set()
82
140
  projects = self.core_client.get_projects().value
@@ -138,6 +196,12 @@ class AzureFetcher(BaseFetcher):
138
196
  return entries
139
197
 
140
198
  def fetch_issues(self) -> List[Dict[str, Any]]:
199
+ """
200
+ Fetch issues (work items) assigned to the configured authors.
201
+
202
+ Returns:
203
+ List[Dict[str, Any]]: List of issue entries.
204
+ """
141
205
  entries = []
142
206
  wit_client = self.connection.clients.get_work_item_tracking_client()
143
207
  # Query work items for each author using a simplified WIQL query.
@@ -160,4 +224,16 @@ class AzureFetcher(BaseFetcher):
160
224
  entries.append(entry)
161
225
  if self._stop_fetching(created_date):
162
226
  break
163
- return entries
227
+ return entries
228
+
229
+ def fetch_releases(self) -> List[Dict[str, Any]]:
230
+ """
231
+ Fetch releases for Azure DevOps repositories.
232
+
233
+ Not implemented for Azure DevOps.
234
+
235
+ Raises:
236
+ NotImplementedError: Always, since release fetching is not supported for AzureFetcher.
237
+ """
238
+ # If Azure DevOps release fetching is supported in the future, implement logic here.
239
+ raise NotImplementedError("Release fetching is not supported for Azure DevOps (AzureFetcher).")
@@ -9,7 +9,7 @@ class BaseFetcher(ABC):
9
9
  start_date: Optional[datetime] = None,
10
10
  end_date: Optional[datetime] = None,
11
11
  repo_filter: Optional[List[str]] = None,
12
- authors: Optional[List[str]]=None
12
+ authors: Optional[List[str]] = None
13
13
  ):
14
14
  self.pat = pat
15
15
  if start_date is not None:
@@ -28,32 +28,82 @@ class BaseFetcher(ABC):
28
28
 
29
29
  @property
30
30
  @abstractmethod
31
- def repos_names(self)->List[str]:
31
+ def repos_names(self) -> List[str]:
32
+ """
33
+ Return the list of repository names accessible to this fetcher.
34
+
35
+ Returns:
36
+ List[str]: List of repository names.
37
+ """
32
38
  pass
33
-
39
+
34
40
  @abstractmethod
35
- def fetch_commits(self) -> List[str]:
41
+ def fetch_commits(self) -> List[Dict[str, Any]]:
42
+ """
43
+ Fetch commit entries for the configured repositories and authors.
44
+
45
+ Returns:
46
+ List[Dict[str, Any]]: List of commit entries.
47
+ """
36
48
  pass
37
49
 
38
50
  @abstractmethod
39
- def fetch_pull_requests(self) -> List[str]:
51
+ def fetch_pull_requests(self) -> List[Dict[str, Any]]:
52
+ """
53
+ Fetch pull request entries for the configured repositories and authors.
54
+
55
+ Returns:
56
+ List[Dict[str, Any]]: List of pull request entries.
57
+ """
40
58
  pass
41
59
 
42
60
  @abstractmethod
43
- def fetch_issues(self) -> List[str]:
61
+ def fetch_issues(self) -> List[Dict[str, Any]]:
62
+ """
63
+ Fetch issue entries for the configured repositories and authors.
64
+
65
+ Returns:
66
+ List[Dict[str, Any]]: List of issue entries.
67
+ """
44
68
  pass
45
69
 
70
+ @abstractmethod
71
+ def fetch_releases(self) -> List[Dict[str, Any]]:
72
+ """
73
+ Fetch releases for all repositories accessible to this fetcher.
74
+
75
+ Returns:
76
+ List[Dict[str, Any]]: List of releases, each as a structured dictionary.
77
+ The dictionary should include at least:
78
+ - tag_name: str
79
+ - name: str
80
+ - repo: str
81
+ - author: str
82
+ - published_at: datetime
83
+ - created_at: datetime
84
+ - draft: bool
85
+ - prerelease: bool
86
+ - body: str
87
+ - assets: List[Dict[str, Any]] (if available)
88
+ Raises:
89
+ NotImplementedError: If the provider does not support release fetching.
90
+ """
91
+ raise NotImplementedError("Release fetching is not implemented for this provider.")
92
+
46
93
  def get_authored_messages(self) -> List[Dict[str, Any]]:
47
94
  """
48
95
  Aggregates all commit, pull request, and issue entries into a single list,
49
96
  ensuring no duplicate commits (based on SHA) are present, and then sorts
50
97
  them in chronological order based on their timestamp.
98
+
99
+ Returns:
100
+ List[Dict[str, Any]]: Aggregated and sorted list of entries.
51
101
  """
52
102
  commit_entries = self.fetch_commits()
53
103
  pr_entries = self.fetch_pull_requests()
54
104
  try:
55
105
  issue_entries = self.fetch_issues()
56
- except Exception as e:
106
+ except Exception:
57
107
  issue_entries = []
58
108
 
59
109
  all_entries = pr_entries + commit_entries + issue_entries
@@ -75,11 +125,17 @@ class BaseFetcher(ABC):
75
125
  # Sort all entries by their timestamp.
76
126
  final_entries.sort(key=lambda x: x["timestamp"])
77
127
  return self.convert_timestamps_to_str(final_entries)
78
-
128
+
79
129
  @staticmethod
80
130
  def convert_timestamps_to_str(entries: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
81
131
  """
82
132
  Converts the timestamp field from datetime to string format for each entry in the list.
133
+
134
+ Args:
135
+ entries (List[Dict[str, Any]]): List of entries with possible datetime timestamps.
136
+
137
+ Returns:
138
+ List[Dict[str, Any]]: Entries with timestamps as ISO-formatted strings.
83
139
  """
84
140
  for entry in entries:
85
141
  if isinstance(entry.get("timestamp"), datetime):
@@ -4,15 +4,21 @@ from typing import List, Dict, Any
4
4
  from git_recap.providers.base_fetcher import BaseFetcher
5
5
 
6
6
  class GitHubFetcher(BaseFetcher):
7
+ """
8
+ Fetcher implementation for GitHub repositories.
9
+
10
+ Supports fetching commits, pull requests, issues, and releases.
11
+ """
12
+
7
13
  def __init__(self, pat: str, start_date=None, end_date=None, repo_filter=None, authors=None):
8
14
  super().__init__(pat, start_date, end_date, repo_filter, authors)
9
15
  self.github = Github(self.pat)
10
- self.user = self.github.get_user()
16
+ self.user = self.github.get_user()
11
17
  self.repos = self.user.get_repos(affiliation="owner,collaborator,organization_member")
12
18
  self.authors.append(self.user.login)
13
19
 
14
20
  @property
15
- def repos_names(self)->List[str]:
21
+ def repos_names(self) -> List[str]:
16
22
  return [repo.name for repo in self.repos]
17
23
 
18
24
  def _stop_fetching(self, date_obj: datetime) -> bool:
@@ -101,7 +107,6 @@ class GitHubFetcher(BaseFetcher):
101
107
  break
102
108
  return entries
103
109
 
104
-
105
110
  def fetch_issues(self) -> List[Dict[str, Any]]:
106
111
  entries = []
107
112
  issues = self.user.get_issues()
@@ -117,4 +122,56 @@ class GitHubFetcher(BaseFetcher):
117
122
  entries.append(entry)
118
123
  if self._stop_fetching(issue_date):
119
124
  break
120
- return entries
125
+ return entries
126
+
127
+ def fetch_releases(self) -> List[Dict[str, Any]]:
128
+ """
129
+ Fetch releases for all repositories accessible to the user.
130
+
131
+ Returns:
132
+ List[Dict[str, Any]]: List of releases, each as a structured dictionary with:
133
+ - tag_name: str
134
+ - name: str
135
+ - repo: str
136
+ - author: str
137
+ - published_at: datetime
138
+ - created_at: datetime
139
+ - draft: bool
140
+ - prerelease: bool
141
+ - body: str
142
+ - assets: List[Dict[str, Any]] (each with name, size, download_url, content_type, etc.)
143
+ """
144
+ releases = []
145
+ for repo in self.repos:
146
+ if self.repo_filter and repo.name not in self.repo_filter:
147
+ continue
148
+ try:
149
+ for rel in repo.get_releases():
150
+ # Compose asset list
151
+ assets = []
152
+ for asset in rel.get_assets():
153
+ assets.append({
154
+ "name": asset.name,
155
+ "size": asset.size,
156
+ "download_url": asset.browser_download_url,
157
+ "content_type": asset.content_type,
158
+ "created_at": asset.created_at,
159
+ "updated_at": asset.updated_at,
160
+ })
161
+ release_entry = {
162
+ "tag_name": rel.tag_name,
163
+ "name": rel.title if hasattr(rel, "title") else rel.name,
164
+ "repo": repo.name,
165
+ "author": rel.author.login if rel.author else None,
166
+ "published_at": rel.published_at,
167
+ "created_at": rel.created_at,
168
+ "draft": rel.draft,
169
+ "prerelease": rel.prerelease,
170
+ "body": rel.body,
171
+ "assets": assets,
172
+ }
173
+ releases.append(release_entry)
174
+ except Exception:
175
+ # If fetching releases fails for a repo, skip it (could be permissions or no releases)
176
+ continue
177
+ return releases
@@ -4,11 +4,37 @@ from typing import List, Dict, Any
4
4
  from git_recap.providers.base_fetcher import BaseFetcher
5
5
 
6
6
  class GitLabFetcher(BaseFetcher):
7
- def __init__(self, pat: str, url: str = 'https://gitlab.com', start_date=None, end_date=None, repo_filter=None, authors=None):
7
+ """
8
+ Fetcher implementation for GitLab repositories.
9
+
10
+ Supports fetching commits, merge requests (pull requests), and issues.
11
+ Release fetching is not supported and will raise NotImplementedError.
12
+ """
13
+
14
+ def __init__(
15
+ self,
16
+ pat: str,
17
+ url: str = 'https://gitlab.com',
18
+ start_date=None,
19
+ end_date=None,
20
+ repo_filter=None,
21
+ authors=None
22
+ ):
23
+ """
24
+ Initialize the GitLabFetcher.
25
+
26
+ Args:
27
+ pat (str): Personal Access Token for GitLab.
28
+ url (str): The GitLab instance URL.
29
+ start_date (datetime, optional): Start date for filtering entries.
30
+ end_date (datetime, optional): End date for filtering entries.
31
+ repo_filter (List[str], optional): List of repository names to filter.
32
+ authors (List[str], optional): List of author usernames.
33
+ """
8
34
  super().__init__(pat, start_date, end_date, repo_filter, authors)
9
35
  self.gl = gitlab.Gitlab(url, private_token=self.pat)
10
36
  self.gl.auth()
11
- # Instead of only owned projects, retrieve projects where you're a member.
37
+ # Retrieve projects where the user is a member.
12
38
  self.projects = self.gl.projects.list(membership=True, all=True)
13
39
  # Default to the authenticated user's username if no authors are provided.
14
40
  if authors is None:
@@ -17,11 +43,12 @@ class GitLabFetcher(BaseFetcher):
17
43
  self.authors = authors
18
44
 
19
45
  @property
20
- def repos_names(self)->List[str]:
21
- "to be implemented later"
46
+ def repos_names(self) -> List[str]:
47
+ """Return the list of repository names."""
22
48
  return [project.name for project in self.projects]
23
49
 
24
50
  def _filter_by_date(self, date_str: str) -> bool:
51
+ """Check if a date string is within the configured date range."""
25
52
  date_obj = datetime.fromisoformat(date_str)
26
53
  if self.start_date and date_obj < self.start_date:
27
54
  return False
@@ -30,12 +57,19 @@ class GitLabFetcher(BaseFetcher):
30
57
  return True
31
58
 
32
59
  def _stop_fetching(self, date_str: str) -> bool:
60
+ """Determine if fetching should stop based on the date string."""
33
61
  date_obj = datetime.fromisoformat(date_str)
34
62
  if self.start_date and date_obj < self.start_date:
35
63
  return True
36
64
  return False
37
65
 
38
66
  def fetch_commits(self) -> List[Dict[str, Any]]:
67
+ """
68
+ Fetch commits for all projects and authors.
69
+
70
+ Returns:
71
+ List[Dict[str, Any]]: List of commit entries.
72
+ """
39
73
  entries = []
40
74
  processed_commits = set()
41
75
  for project in self.projects:
@@ -63,6 +97,12 @@ class GitLabFetcher(BaseFetcher):
63
97
  return entries
64
98
 
65
99
  def fetch_pull_requests(self) -> List[Dict[str, Any]]:
100
+ """
101
+ Fetch merge requests (pull requests) and their associated commits for all projects and authors.
102
+
103
+ Returns:
104
+ List[Dict[str, Any]]: List of pull request and commit_from_pr entries.
105
+ """
66
106
  entries = []
67
107
  processed_pr_commits = set()
68
108
  for project in self.projects:
@@ -109,6 +149,12 @@ class GitLabFetcher(BaseFetcher):
109
149
  return entries
110
150
 
111
151
  def fetch_issues(self) -> List[Dict[str, Any]]:
152
+ """
153
+ Fetch issues assigned to the authenticated user for all projects.
154
+
155
+ Returns:
156
+ List[Dict[str, Any]]: List of issue entries.
157
+ """
112
158
  entries = []
113
159
  for project in self.projects:
114
160
  if self.repo_filter and project.name not in self.repo_filter:
@@ -126,4 +172,16 @@ class GitLabFetcher(BaseFetcher):
126
172
  entries.append(entry)
127
173
  if self._stop_fetching(issue_date):
128
174
  break
129
- return entries
175
+ return entries
176
+
177
+ def fetch_releases(self) -> List[Dict[str, Any]]:
178
+ """
179
+ Fetch releases for GitLab repositories.
180
+
181
+ Not implemented for GitLabFetcher.
182
+
183
+ Raises:
184
+ NotImplementedError: Always, since release fetching is not supported for GitLabFetcher.
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).")
@@ -11,14 +11,14 @@ from git_recap.providers.base_fetcher import BaseFetcher
11
11
 
12
12
  class URLFetcher(BaseFetcher):
13
13
  """Fetcher implementation for generic Git repository URLs."""
14
-
14
+
15
15
  GIT_URL_PATTERN = re.compile(
16
16
  r'^(?:http|https|git|ssh)://' # Protocol
17
17
  r'(?:\S+@)?' # Optional username
18
18
  r'([^/]+)' # Domain
19
19
  r'(?:[:/])([^/]+/[^/]+?)(?:\.git)?$' # Repo path
20
20
  )
21
-
21
+
22
22
  def __init__(
23
23
  self,
24
24
  url: str,
@@ -52,7 +52,7 @@ class URLFetcher(BaseFetcher):
52
52
  """Validate the Git repository URL using git ls-remote."""
53
53
  if not self.GIT_URL_PATTERN.match(self.url):
54
54
  raise ValueError(f"Invalid Git repository URL format: {self.url}")
55
-
55
+
56
56
  try:
57
57
  result = subprocess.run(
58
58
  ["git", "ls-remote", self.url],
@@ -80,7 +80,7 @@ class URLFetcher(BaseFetcher):
80
80
  text=True,
81
81
  timeout=300
82
82
  )
83
-
83
+
84
84
  # Fetch all branches
85
85
  subprocess.run(
86
86
  ["git", "-C", self.temp_dir, "fetch", "--all"],
@@ -89,7 +89,7 @@ class URLFetcher(BaseFetcher):
89
89
  text=True,
90
90
  timeout=300
91
91
  )
92
-
92
+
93
93
  # Verify the cloned repository has at least one commit
94
94
  verify_result = subprocess.run(
95
95
  ["git", "-C", self.temp_dir, "rev-list", "--count", "--all"],
@@ -99,7 +99,7 @@ class URLFetcher(BaseFetcher):
99
99
  )
100
100
  if int(verify_result.stdout.strip()) == 0:
101
101
  raise ValueError("Cloned repository has no commits")
102
-
102
+
103
103
  except subprocess.TimeoutExpired:
104
104
  raise RuntimeError("Repository cloning timed out")
105
105
  except subprocess.CalledProcessError as e:
@@ -113,23 +113,23 @@ class URLFetcher(BaseFetcher):
113
113
  """Return list of repository names (single item for URL fetcher)."""
114
114
  if not self.temp_dir:
115
115
  return []
116
-
116
+
117
117
  match = self.GIT_URL_PATTERN.match(self.url)
118
118
  if not match:
119
119
  repo_name = self.url.split('/')[-1]
120
120
  return [repo_name]
121
-
121
+
122
122
  repo_name = match.group(2).split('/')[-1]
123
123
  if repo_name.endswith(".git"):
124
124
  repo_name = repo_name[:-4]
125
-
125
+
126
126
  return [repo_name]
127
127
 
128
128
  def _get_all_branches(self) -> List[str]:
129
129
  """Get list of all remote branches in the repository."""
130
130
  if not self.temp_dir:
131
131
  return []
132
-
132
+
133
133
  try:
134
134
  result = subprocess.run(
135
135
  ["git", "-C", self.temp_dir, "branch", "-r", "--format=%(refname:short)"],
@@ -187,11 +187,11 @@ class URLFetcher(BaseFetcher):
187
187
  for line in log_output.splitlines():
188
188
  if not line.strip():
189
189
  continue
190
-
190
+
191
191
  try:
192
192
  sha, author, date_str, message = line.split("|", 3)
193
193
  timestamp = datetime.fromisoformat(date_str)
194
-
194
+
195
195
  if self.start_date and timestamp < self.start_date:
196
196
  continue
197
197
  if self.end_date and timestamp > self.end_date:
@@ -207,7 +207,7 @@ class URLFetcher(BaseFetcher):
207
207
  })
208
208
  except ValueError:
209
209
  continue # Skip malformed log entries
210
-
210
+
211
211
  return entries
212
212
 
213
213
  def fetch_commits(self) -> List[Dict[str, Any]]:
@@ -222,6 +222,17 @@ class URLFetcher(BaseFetcher):
222
222
  """Fetch issues (not implemented for generic Git URLs)."""
223
223
  return []
224
224
 
225
+ def fetch_releases(self) -> List[Dict[str, Any]]:
226
+ """
227
+ Fetch releases for the repository.
228
+ Not implemented for generic Git URLs.
229
+ Raises:
230
+ NotImplementedError: Always, since release fetching is not supported for URLFetcher.
231
+ """
232
+ # If in the future, support for fetching releases from generic git repos is added,
233
+ # implement logic here (e.g., parse tags and annotate with metadata).
234
+ raise NotImplementedError("Release fetching is not supported for generic Git URLs (URLFetcher).")
235
+
225
236
  def clear(self) -> None:
226
237
  """Clean up temporary directory."""
227
238
  if self.temp_dir and os.path.exists(self.temp_dir):
@@ -0,0 +1,147 @@
1
+ from datetime import datetime
2
+ from typing import List, Dict, Any
3
+ from collections import defaultdict
4
+
5
+ def parse_entries_to_txt(entries: List[Dict[str, Any]]) -> str:
6
+ """
7
+ Groups entries by day (YYYY-MM-DD) and produces a plain text summary.
8
+
9
+ Each day's header is the date string, followed by bullet points that list:
10
+ - type (commit, commit_from_pr, pull_request, issue)
11
+ - repo name
12
+ - message text
13
+ - for pull requests: PR number or for commits from PR: pr_title
14
+ """
15
+ # Group entries by date (YYYY-MM-DD)
16
+ grouped = defaultdict(list)
17
+ for entry in entries:
18
+ ts = entry.get("timestamp")
19
+ # Convert timestamp to a datetime object if necessary
20
+ if isinstance(ts, str):
21
+ dt = datetime.fromisoformat(ts)
22
+ else:
23
+ dt = ts
24
+ day = dt.strftime("%Y-%m-%d")
25
+ grouped[day].append(entry)
26
+
27
+ # Sort the days chronologically
28
+ sorted_days = sorted(grouped.keys())
29
+
30
+ # Build the output text
31
+ lines = []
32
+ for day in sorted_days:
33
+ lines.append(day + ":")
34
+ # Optionally, sort the entries for that day if needed (e.g., by timestamp)
35
+ day_entries = sorted(grouped[day], key=lambda x: x["timestamp"])
36
+ for entry in day_entries:
37
+ typ = entry.get("type", "N/A")
38
+ repo = entry.get("repo", "N/A")
39
+ message = entry.get("message", "").strip()
40
+ # Build extra details for pull requests and commits from pull requests
41
+ extra = ""
42
+ if typ == "pull_request":
43
+ pr_number = entry.get("pr_number")
44
+ if pr_number is not None:
45
+ extra = f" (PR #{pr_number})"
46
+ elif typ == "commit_from_pr":
47
+ pr_title = entry.get("pr_title", "")
48
+ if pr_title:
49
+ extra = f" (PR: {pr_title})"
50
+ # Format the bullet point
51
+ bullet = f" - [{typ.replace('_', ' ').title()}] in {repo}: {message}{extra}"
52
+ lines.append(bullet)
53
+ lines.append("") # blank line between days
54
+
55
+ return "\n".join(lines)
56
+
57
+ def parse_releases_to_txt(releases: List[Dict[str, Any]]) -> str:
58
+ """
59
+ Groups releases by day (YYYY-MM-DD, using published_at or created_at) and produces a plain text summary.
60
+
61
+ Each day's header is the date string, followed by a clear, LLM-friendly separator between releases:
62
+ - tag name and release name
63
+ - repo name
64
+ - author
65
+ - draft/prerelease status
66
+ - body/notes (if present)
67
+ - assets (if present)
68
+ """
69
+ grouped = defaultdict(list)
70
+ for rel in releases:
71
+ ts = rel.get("published_at") or rel.get("created_at")
72
+ if isinstance(ts, str):
73
+ dt = datetime.fromisoformat(ts)
74
+ else:
75
+ dt = ts
76
+ day = dt.strftime("%Y-%m-%d")
77
+ grouped[day].append(rel)
78
+
79
+ sorted_days = sorted(grouped.keys())
80
+
81
+ lines = []
82
+ for day in sorted_days:
83
+ lines.append(f"Date: {day}")
84
+ day_releases = sorted(grouped[day], key=lambda x: x.get("published_at") or x.get("created_at"))
85
+ for rel in day_releases:
86
+ lines.append("----- Release Start -----")
87
+ tag = rel.get("tag_name", "N/A")
88
+ name = rel.get("name", "")
89
+ repo = rel.get("repo", "N/A")
90
+ author = rel.get("author", "N/A")
91
+ draft = rel.get("draft", False)
92
+ prerelease = rel.get("prerelease", False)
93
+ body = rel.get("body", "")
94
+ assets = rel.get("assets", [])
95
+
96
+ status = []
97
+ if draft:
98
+ status.append("draft")
99
+ if prerelease:
100
+ status.append("prerelease")
101
+ status_str = f" ({', '.join(status)})" if status else ""
102
+
103
+ lines.append(f"Release: {name}")
104
+ lines.append(f"Repository: {repo}")
105
+ lines.append(f"Tag: {tag}")
106
+ lines.append(f"Author: {author}")
107
+ if status_str:
108
+ lines.append(f"Status: {status_str.strip()}")
109
+ if body and body.strip():
110
+ lines.append(f"Notes: {body.strip()}")
111
+ if assets:
112
+ lines.append("Assets:")
113
+ for asset in assets:
114
+ asset_name = asset.get("name", "N/A")
115
+ asset_size = asset.get("size", "N/A")
116
+ asset_url = asset.get("download_url", "")
117
+ lines.append(f" - {asset_name} ({asset_size} bytes){' - ' + asset_url if asset_url else ''}")
118
+ lines.append("----- Release End -----\n") # clear end of release
119
+
120
+ lines.append("") # blank line between days
121
+
122
+ return "\n".join(lines)
123
+
124
+
125
+ # Example usage for releases:
126
+ # if __name__ == "__main__":
127
+ # releases = [
128
+ # {
129
+ # "tag_name": "v1.0.0",
130
+ # "name": "Release 1.0.0",
131
+ # "repo": "test-repo",
132
+ # "author": "testuser",
133
+ # "published_at": "2025-03-15T10:00:00",
134
+ # "created_at": "2025-03-15T09:00:00",
135
+ # "draft": False,
136
+ # "prerelease": False,
137
+ # "body": "This is a test release",
138
+ # "assets": [
139
+ # {
140
+ # "name": "test-asset.zip",
141
+ # "size": 1024,
142
+ # "download_url": "https://github.com/test/releases/download/v1.0.0/test-asset.zip",
143
+ # }
144
+ # ],
145
+ # }
146
+ # ]
147
+ # print(parse_releases_to_txt(releases))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: git-recap
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Summary: A modular Python tool that aggregates and formats user-authored messages from repositories.
5
5
  Author: Bruno V.
6
6
  Author-email: bruno.vitorino@tecnico.ulisboa.pt
@@ -21,8 +21,16 @@ Dynamic: license-file
21
21
  Dynamic: requires-dist
22
22
  Dynamic: summary
23
23
 
24
+ <a href="https://www.uneed.best/tool/gitrecap">
25
+ <img src="https://www.uneed.best/POTD2A.png" style="width: 250px;" alt="Uneed POTD2 Badge" />
26
+ </a>
27
+
28
+ ---
29
+
24
30
  # Git Recap
25
31
 
32
+ 🎉 **Featured in Uneed's Latest Newsletter as a Staff Pick!** 🎉
33
+
26
34
  Git Recap is a modular Python tool that aggregates and formats user-authored messages from repositories hosted on GitHub, Azure DevOps, and GitLab. It fetches commit messages, pull requests (along with their associated commits), and issues, then consolidates and sorts these events into a clear, chronological summary. This summary is output as a plain text string that can serve as context for large language models or other analysis tools.
27
35
 
28
36
  ## Features
@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
5
5
 
6
6
  setup(
7
7
  name="git-recap",
8
- version="0.1.3",
8
+ version="0.1.4",
9
9
  packages=find_packages(),
10
10
  install_requires=[
11
11
  "PyGithub==2.6.1",
@@ -47,6 +47,9 @@ class DummyFetcher(BaseFetcher):
47
47
  def repos_names(self):
48
48
  ...
49
49
 
50
+ def fetch_releases(self):
51
+ ...
52
+
50
53
  def test_get_authored_messages():
51
54
  # Create a dummy fetcher with a date range covering March 2025.
52
55
  fetcher = DummyFetcher(
@@ -0,0 +1,237 @@
1
+ import pytest
2
+ from datetime import datetime
3
+ from unittest.mock import Mock, patch
4
+ from git_recap.utils import parse_entries_to_txt
5
+
6
+ def test_parse_entries_to_txt():
7
+ # Example list of entries
8
+ entries = [
9
+ {
10
+ "type": "commit_from_pr",
11
+ "repo": "AiCore",
12
+ "message": "feat: update TODOs for ObservabilityDashboard",
13
+ "timestamp": "2025-03-14T00:17:02+00:00",
14
+ "sha": "dummysha1",
15
+ "pr_title": "Unified ai integration error monitoring"
16
+ },
17
+ {
18
+ "type": "commit",
19
+ "repo": "AiCore",
20
+ "message": "Merge pull request #5 from somebranch",
21
+ "timestamp": "2025-03-15T21:47:12+00:00",
22
+ "sha": "dummysha2"
23
+ },
24
+ {
25
+ "type": "pull_request",
26
+ "repo": "AiCore",
27
+ "message": "Unified ai integration error monitoring",
28
+ "timestamp": "2025-03-15T21:47:13+00:00",
29
+ "pr_number": 5
30
+ },
31
+ {
32
+ "type": "issue",
33
+ "repo": "AiCore",
34
+ "message": "Issue: error when launching app",
35
+ "timestamp": "2025-03-15T23:00:00+00:00",
36
+ },
37
+ ]
38
+ txt = parse_entries_to_txt(entries)
39
+
40
+ # Check that day headers are present
41
+ assert "2025-03-14:" in txt
42
+ assert "2025-03-15:" in txt
43
+
44
+ # Check that key message parts appear
45
+ assert "Feat: Update TodoS for Observabilitydashboard" in txt or "update TODOs" in txt
46
+ assert "Unified ai integration error monitoring" in txt
47
+ assert "Merge pull request" in txt
48
+ assert "Issue: error when launching app" in txt
49
+
50
+ # Check that individual timestamps and sha are not in the final output
51
+ assert "dummysha1" not in txt
52
+ assert "dummysha2" not in txt
53
+ assert "T00:17:02" not in txt # individual timestamp should not be printed
54
+
55
+
56
+ @patch('git_recap.providers.github_fetcher.Github')
57
+ def test_fetch_releases_github(mock_github_class):
58
+ """
59
+ Unit test for GitHub release fetching functionality with proper mocking.
60
+ """
61
+ from git_recap.providers.github_fetcher import GitHubFetcher
62
+
63
+ # Create mock objects
64
+ mock_github = Mock()
65
+ mock_user = Mock()
66
+ mock_repo = Mock()
67
+ mock_release = Mock()
68
+ mock_asset = Mock()
69
+
70
+ # Configure the mock hierarchy
71
+ mock_github_class.return_value = mock_github
72
+ mock_github.get_user.return_value = mock_user
73
+ mock_user.login = "testuser"
74
+ mock_user.get_repos.return_value = [mock_repo]
75
+
76
+ # Configure mock repo
77
+ mock_repo.name = "test-repo"
78
+ mock_repo.get_releases.return_value = [mock_release]
79
+
80
+ # Configure mock release
81
+ mock_release.tag_name = "v1.0.0"
82
+ mock_release.name = "Release 1.0.0"
83
+ mock_release.title = "Release 1.0.0" # Some releases use title instead of name
84
+ mock_release.author.login = "testuser"
85
+ mock_release.published_at = datetime(2025, 3, 15, 10, 0, 0)
86
+ mock_release.created_at = datetime(2025, 3, 15, 9, 0, 0)
87
+ mock_release.draft = False
88
+ mock_release.prerelease = False
89
+ mock_release.body = "This is a test release"
90
+
91
+ # Configure mock asset
92
+ mock_asset.name = "test-asset.zip"
93
+ mock_asset.size = 1024
94
+ mock_asset.browser_download_url = "https://github.com/test/releases/download/v1.0.0/test-asset.zip"
95
+ mock_asset.content_type = "application/zip"
96
+ mock_asset.created_at = datetime(2025, 3, 15, 9, 30, 0)
97
+ mock_asset.updated_at = datetime(2025, 3, 15, 9, 30, 0)
98
+
99
+ mock_release.get_assets.return_value = [mock_asset]
100
+
101
+ # Create GitHubFetcher instance and test
102
+ fetcher = GitHubFetcher(pat="dummy_token")
103
+ releases = fetcher.fetch_releases()
104
+
105
+ # Assertions
106
+ assert isinstance(releases, list)
107
+ assert len(releases) == 1
108
+
109
+ release = releases[0]
110
+ assert release["tag_name"] == "v1.0.0"
111
+ assert release["name"] == "Release 1.0.0"
112
+ assert release["repo"] == "test-repo"
113
+ assert release["author"] == "testuser"
114
+ assert release["published_at"] == datetime(2025, 3, 15, 10, 0, 0)
115
+ assert release["created_at"] == datetime(2025, 3, 15, 9, 0, 0)
116
+ assert release["draft"] is False
117
+ assert release["prerelease"] is False
118
+ assert release["body"] == "This is a test release"
119
+ assert len(release["assets"]) == 1
120
+
121
+ asset = release["assets"][0]
122
+ assert asset["name"] == "test-asset.zip"
123
+ assert asset["size"] == 1024
124
+ assert asset["download_url"] == "https://github.com/test/releases/download/v1.0.0/test-asset.zip"
125
+ assert asset["content_type"] == "application/zip"
126
+
127
+
128
+ @patch('git_recap.providers.github_fetcher.Github')
129
+ def test_fetch_releases_github_with_repo_filter(mock_github_class):
130
+ """
131
+ Test fetch_releases with repo_filter applied.
132
+ """
133
+ from git_recap.providers.github_fetcher import GitHubFetcher
134
+
135
+ # Create mock objects
136
+ mock_github = Mock()
137
+ mock_user = Mock()
138
+ mock_repo1 = Mock()
139
+ mock_repo2 = Mock()
140
+
141
+ # Configure the mock hierarchy
142
+ mock_github_class.return_value = mock_github
143
+ mock_github.get_user.return_value = mock_user
144
+ mock_user.login = "testuser"
145
+ mock_user.get_repos.return_value = [mock_repo1, mock_repo2]
146
+
147
+ # Configure mock repos
148
+ mock_repo1.name = "allowed-repo"
149
+ mock_repo2.name = "filtered-repo"
150
+ mock_repo1.get_releases.return_value = []
151
+ mock_repo2.get_releases.return_value = []
152
+
153
+ # Create GitHubFetcher instance with repo filter
154
+ fetcher = GitHubFetcher(pat="dummy_token", repo_filter=["allowed-repo"])
155
+ releases = fetcher.fetch_releases()
156
+
157
+ # Assertions
158
+ assert isinstance(releases, list)
159
+ # Only allowed-repo should have been processed
160
+ mock_repo1.get_releases.assert_called_once()
161
+ mock_repo2.get_releases.assert_not_called()
162
+
163
+
164
+ @patch('git_recap.providers.github_fetcher.Github')
165
+ def test_fetch_releases_github_exception_handling(mock_github_class):
166
+ """
167
+ Test fetch_releases handles exceptions gracefully when a repo fails.
168
+ """
169
+ from git_recap.providers.github_fetcher import GitHubFetcher
170
+
171
+ # Create mock objects
172
+ mock_github = Mock()
173
+ mock_user = Mock()
174
+ mock_repo1 = Mock()
175
+ mock_repo2 = Mock()
176
+
177
+ # Configure the mock hierarchy
178
+ mock_github_class.return_value = mock_github
179
+ mock_github.get_user.return_value = mock_user
180
+ mock_user.login = "testuser"
181
+ mock_user.get_repos.return_value = [mock_repo1, mock_repo2]
182
+
183
+ # Configure mock repos - one fails, one succeeds
184
+ mock_repo1.name = "failing-repo"
185
+ mock_repo2.name = "working-repo"
186
+ mock_repo1.get_releases.side_effect = Exception("Permission denied")
187
+ mock_repo2.get_releases.return_value = []
188
+
189
+ # Create GitHubFetcher instance and test
190
+ fetcher = GitHubFetcher(pat="dummy_token")
191
+ releases = fetcher.fetch_releases()
192
+
193
+ # Should return empty list and not raise exception
194
+ assert isinstance(releases, list)
195
+ assert len(releases) == 0
196
+
197
+
198
+ def test_fetch_releases_not_implemented_providers():
199
+ """
200
+ Test that other providers raise NotImplementedError for releases.
201
+ """
202
+ from git_recap.providers.gitlab_fetcher import GitLabFetcher
203
+ from git_recap.providers.azure_fetcher import AzureFetcher
204
+ from git_recap.providers.url_fetcher import URLFetcher
205
+
206
+ # These should raise NotImplementedError or similar
207
+ # Note: You may need to adjust this based on your actual implementation
208
+
209
+ # GitLabFetcher test (assuming it doesn't implement fetch_releases yet)
210
+ try:
211
+ gitlab_fetcher = GitLabFetcher(pat="dummy", base_url="https://gitlab.com")
212
+ if hasattr(gitlab_fetcher, 'fetch_releases'):
213
+ with pytest.raises(NotImplementedError):
214
+ gitlab_fetcher.fetch_releases()
215
+ except Exception:
216
+ # If GitLabFetcher can't be instantiated with dummy data, that's fine
217
+ pass
218
+
219
+ # AzureFetcher test (assuming it doesn't implement fetch_releases yet)
220
+ try:
221
+ azure_fetcher = AzureFetcher(pat="dummy", organization="test", project="test")
222
+ if hasattr(azure_fetcher, 'fetch_releases'):
223
+ with pytest.raises(NotImplementedError):
224
+ azure_fetcher.fetch_releases()
225
+ except Exception:
226
+ # If AzureFetcher can't be instantiated with dummy data, that's fine
227
+ pass
228
+
229
+ # URLFetcher test (assuming it doesn't implement fetch_releases yet)
230
+ try:
231
+ url_fetcher = URLFetcher(pat="dummy", base_url="https://example.com")
232
+ if hasattr(url_fetcher, 'fetch_releases'):
233
+ with pytest.raises(NotImplementedError):
234
+ url_fetcher.fetch_releases()
235
+ except Exception:
236
+ # If URLFetcher can't be instantiated with dummy data, that's fine
237
+ pass
@@ -1,93 +0,0 @@
1
- from datetime import datetime
2
- from typing import List, Dict, Any
3
- from collections import defaultdict
4
-
5
- def parse_entries_to_txt(entries: List[Dict[str, Any]]) -> str:
6
- """
7
- Groups entries by day (YYYY-MM-DD) and produces a plain text summary.
8
-
9
- Each day's header is the date string, followed by bullet points that list:
10
- - type (commit, commit_from_pr, pull_request, issue)
11
- - repo name
12
- - message text
13
- - for pull requests: PR number or for commits from PR: pr_title
14
- """
15
- # Group entries by date (YYYY-MM-DD)
16
- grouped = defaultdict(list)
17
- for entry in entries:
18
- ts = entry.get("timestamp")
19
- # Convert timestamp to a datetime object if necessary
20
- if isinstance(ts, str):
21
- dt = datetime.fromisoformat(ts)
22
- else:
23
- dt = ts
24
- day = dt.strftime("%Y-%m-%d")
25
- grouped[day].append(entry)
26
-
27
- # Sort the days chronologically
28
- sorted_days = sorted(grouped.keys())
29
-
30
- # Build the output text
31
- lines = []
32
- for day in sorted_days:
33
- lines.append(day + ":")
34
- # Optionally, sort the entries for that day if needed (e.g., by timestamp)
35
- day_entries = sorted(grouped[day], key=lambda x: x["timestamp"])
36
- for entry in day_entries:
37
- typ = entry.get("type", "N/A")
38
- repo = entry.get("repo", "N/A")
39
- message = entry.get("message", "").strip()
40
- # Build extra details for pull requests and commits from pull requests
41
- extra = ""
42
- if typ == "pull_request":
43
- pr_number = entry.get("pr_number")
44
- if pr_number is not None:
45
- extra = f" (PR #{pr_number})"
46
- elif typ == "commit_from_pr":
47
- pr_title = entry.get("pr_title", "")
48
- if pr_title:
49
- extra = f" (PR: {pr_title})"
50
- # Format the bullet point
51
- bullet = f" - [{typ.replace('_', ' ').title()}] in {repo}: {message}{extra}"
52
- lines.append(bullet)
53
- lines.append("") # blank line between days
54
-
55
- return "\n".join(lines)
56
-
57
- # Example usage:
58
- if __name__ == "__main__":
59
- # Assuming `output` is the list of dict entries from your fetcher.
60
- output = [
61
- {
62
- "type": "commit_from_pr",
63
- "repo": "AiCore",
64
- "message": "feat: update TODOs for ObservabilityDashboard with new input/output tokens and cross workspace analysis",
65
- "timestamp": "2025-03-14T00:17:02+00:00",
66
- "sha": "d1f185f09e4fcb775374b9468755b8463c94a605",
67
- "pr_title": "Unified ai integration error monitoring"
68
- },
69
- {
70
- "type": "commit_from_pr",
71
- "repo": "AiCore",
72
- "message": "feat: enhance token usage visualization in ObservabilityDashboard with grouped bar chart",
73
- "timestamp": "2025-03-15T00:20:15+00:00",
74
- "sha": "875457b9c80076d821f36cc646ec354ef5124088",
75
- "pr_title": "Unified ai integration error monitoring"
76
- },
77
- {
78
- "type": "pull_request",
79
- "repo": "AiCore",
80
- "message": "Unified ai integration error monitoring",
81
- "timestamp": "2025-03-15T21:47:13+00:00",
82
- "pr_number": 5
83
- },
84
- {
85
- "type": "commit",
86
- "repo": "AiCore",
87
- "message": "feat: update openai package version to 1.66.3 in requirements.txt and setup.py",
88
- "timestamp": "2025-03-15T23:22:28+00:00",
89
- "sha": "9f7e30ebcca8c909274dd8ca91fcfbd17bbf9195"
90
- },
91
- ]
92
- context_txt = parse_entries_to_txt(output)
93
- print(context_txt)
@@ -1,52 +0,0 @@
1
- import pytest
2
- from datetime import datetime
3
- from git_recap.utils import parse_entries_to_txt # assuming you placed the parser function in utils.py
4
-
5
- def test_parse_entries_to_txt():
6
- # Example list of entries
7
- entries = [
8
- {
9
- "type": "commit_from_pr",
10
- "repo": "AiCore",
11
- "message": "feat: update TODOs for ObservabilityDashboard",
12
- "timestamp": "2025-03-14T00:17:02+00:00",
13
- "sha": "dummysha1",
14
- "pr_title": "Unified ai integration error monitoring"
15
- },
16
- {
17
- "type": "commit",
18
- "repo": "AiCore",
19
- "message": "Merge pull request #5 from somebranch",
20
- "timestamp": "2025-03-15T21:47:12+00:00",
21
- "sha": "dummysha2"
22
- },
23
- {
24
- "type": "pull_request",
25
- "repo": "AiCore",
26
- "message": "Unified ai integration error monitoring",
27
- "timestamp": "2025-03-15T21:47:13+00:00",
28
- "pr_number": 5
29
- },
30
- {
31
- "type": "issue",
32
- "repo": "AiCore",
33
- "message": "Issue: error when launching app",
34
- "timestamp": "2025-03-15T23:00:00+00:00",
35
- },
36
- ]
37
- txt = parse_entries_to_txt(entries)
38
-
39
- # Check that day headers are present
40
- assert "2025-03-14:" in txt
41
- assert "2025-03-15:" in txt
42
-
43
- # Check that key message parts appear
44
- assert "Feat: Update TodoS for Observabilitydashboard" in txt or "update TODOs" in txt
45
- assert "Unified ai integration error monitoring" in txt
46
- assert "Merge pull request" in txt
47
- assert "Issue: error when launching app" in txt
48
-
49
- # Check that individual timestamps and sha are not in the final output
50
- assert "dummysha1" not in txt
51
- assert "dummysha2" not in txt
52
- assert "T00:17:02" not in txt # individual timestamp should not be printed
File without changes
File without changes