Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion examples/fetch_github.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,7 @@
messages = fetcher.get_authored_messages()
txt = parse_entries_to_txt(messages)

print(txt)
print(txt)

releases = fetcher.fetch_releases()
print(releases)
84 changes: 80 additions & 4 deletions git_recap/providers/azure_fetcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,25 @@
from git_recap.providers.base_fetcher import BaseFetcher

class AzureFetcher(BaseFetcher):
"""
Fetcher implementation for Azure DevOps repositories.

Supports fetching commits, pull requests, and issues.
Release fetching is not supported and will raise NotImplementedError.
"""

def __init__(self, pat: str, organization_url: str, start_date=None, end_date=None, repo_filter=None, authors=None):
# authors should be passed as a list of unique names (e.g., email or unique id)
"""
Initialize the AzureFetcher.

Args:
pat (str): Personal Access Token for Azure DevOps.
organization_url (str): The Azure DevOps organization URL.
start_date (datetime, optional): Start date for filtering entries.
end_date (datetime, optional): End date for filtering entries.
repo_filter (List[str], optional): List of repository names to filter.
authors (List[str], optional): List of author identifiers (e.g., email or unique id).
"""
super().__init__(pat, start_date, end_date, repo_filter, authors)
self.organization_url = organization_url
credentials = BasicAuthentication('', self.pat)
Expand All @@ -20,29 +37,64 @@ def __init__(self, pat: str, organization_url: str, start_date=None, end_date=No
self.authors = []

def get_repos(self):
"""
Retrieve all repositories in all projects for the organization.
Returns:
List of repository objects.
"""
projects = self.core_client.get_projects().value
# Get all repositories in each project
repos = [self.git_client.get_repositories(project.id) for project in projects]
return repos

@property
def repos_names(self)->List[str]:
"to be implemented later"
def repos_names(self) -> List[str]:
"""
Return the list of repository names.

Returns:
List[str]: List of repository names.
"""
# To be implemented if needed for UI or listing.
...

def _filter_by_date(self, date_obj: datetime) -> bool:
"""
Check if a datetime object is within the configured date range.

Args:
date_obj (datetime): The datetime to check.

Returns:
bool: True if within range, False otherwise.
"""
if self.start_date and date_obj < self.start_date:
return False
if self.end_date and date_obj > self.end_date:
return False
return True

def _stop_fetching(self, date_obj: datetime) -> bool:
"""
Determine if fetching should stop based on the date.

Args:
date_obj (datetime): The datetime to check.

Returns:
bool: True if should stop, False otherwise.
"""
if self.start_date and date_obj < self.start_date:
return True
return False

def fetch_commits(self) -> List[Dict[str, Any]]:
"""
Fetch commits for all repositories and authors.

Returns:
List[Dict[str, Any]]: List of commit entries.
"""
entries = []
processed_commits = set()
for repo in self.repos:
Expand Down Expand Up @@ -77,6 +129,12 @@ def fetch_commits(self) -> List[Dict[str, Any]]:
return entries

def fetch_pull_requests(self) -> List[Dict[str, Any]]:
"""
Fetch pull requests and their associated commits for all repositories and authors.

Returns:
List[Dict[str, Any]]: List of pull request and commit_from_pr entries.
"""
entries = []
processed_pr_commits = set()
projects = self.core_client.get_projects().value
Expand Down Expand Up @@ -138,6 +196,12 @@ def fetch_pull_requests(self) -> List[Dict[str, Any]]:
return entries

def fetch_issues(self) -> List[Dict[str, Any]]:
"""
Fetch issues (work items) assigned to the configured authors.

Returns:
List[Dict[str, Any]]: List of issue entries.
"""
entries = []
wit_client = self.connection.clients.get_work_item_tracking_client()
# Query work items for each author using a simplified WIQL query.
Expand All @@ -160,4 +224,16 @@ def fetch_issues(self) -> List[Dict[str, Any]]:
entries.append(entry)
if self._stop_fetching(created_date):
break
return entries
return entries

def fetch_releases(self) -> List[Dict[str, Any]]:
"""
Fetch releases for Azure DevOps repositories.

Not implemented for Azure DevOps.

Raises:
NotImplementedError: Always, since release fetching is not supported for AzureFetcher.
"""
# If Azure DevOps release fetching is supported in the future, implement logic here.
raise NotImplementedError("Release fetching is not supported for Azure DevOps (AzureFetcher).")
72 changes: 64 additions & 8 deletions git_recap/providers/base_fetcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ def __init__(
start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None,
repo_filter: Optional[List[str]] = None,
authors: Optional[List[str]]=None
authors: Optional[List[str]] = None
):
self.pat = pat
if start_date is not None:
Expand All @@ -28,32 +28,82 @@ def __init__(

@property
@abstractmethod
def repos_names(self)->List[str]:
def repos_names(self) -> List[str]:
"""
Return the list of repository names accessible to this fetcher.

Returns:
List[str]: List of repository names.
"""
pass

@abstractmethod
def fetch_commits(self) -> List[str]:
def fetch_commits(self) -> List[Dict[str, Any]]:
"""
Fetch commit entries for the configured repositories and authors.

Returns:
List[Dict[str, Any]]: List of commit entries.
"""
pass

@abstractmethod
def fetch_pull_requests(self) -> List[str]:
def fetch_pull_requests(self) -> List[Dict[str, Any]]:
"""
Fetch pull request entries for the configured repositories and authors.

Returns:
List[Dict[str, Any]]: List of pull request entries.
"""
pass

@abstractmethod
def fetch_issues(self) -> List[str]:
def fetch_issues(self) -> List[Dict[str, Any]]:
"""
Fetch issue entries for the configured repositories and authors.

Returns:
List[Dict[str, Any]]: List of issue entries.
"""
pass

@abstractmethod
def fetch_releases(self) -> List[Dict[str, Any]]:
"""
Fetch releases for all repositories accessible to this fetcher.

Returns:
List[Dict[str, Any]]: List of releases, each as a structured dictionary.
The dictionary should include at least:
- tag_name: str
- name: str
- repo: str
- author: str
- published_at: datetime
- created_at: datetime
- draft: bool
- prerelease: bool
- body: str
- assets: List[Dict[str, Any]] (if available)
Raises:
NotImplementedError: If the provider does not support release fetching.
"""
raise NotImplementedError("Release fetching is not implemented for this provider.")

def get_authored_messages(self) -> List[Dict[str, Any]]:
"""
Aggregates all commit, pull request, and issue entries into a single list,
ensuring no duplicate commits (based on SHA) are present, and then sorts
them in chronological order based on their timestamp.

Returns:
List[Dict[str, Any]]: Aggregated and sorted list of entries.
"""
commit_entries = self.fetch_commits()
pr_entries = self.fetch_pull_requests()
try:
issue_entries = self.fetch_issues()
except Exception as e:
except Exception:
issue_entries = []

all_entries = pr_entries + commit_entries + issue_entries
Expand All @@ -75,11 +125,17 @@ def get_authored_messages(self) -> List[Dict[str, Any]]:
# Sort all entries by their timestamp.
final_entries.sort(key=lambda x: x["timestamp"])
return self.convert_timestamps_to_str(final_entries)

@staticmethod
def convert_timestamps_to_str(entries: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""
Converts the timestamp field from datetime to string format for each entry in the list.

Args:
entries (List[Dict[str, Any]]): List of entries with possible datetime timestamps.

Returns:
List[Dict[str, Any]]: Entries with timestamps as ISO-formatted strings.
"""
for entry in entries:
if isinstance(entry.get("timestamp"), datetime):
Expand Down
65 changes: 61 additions & 4 deletions git_recap/providers/github_fetcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,21 @@
from git_recap.providers.base_fetcher import BaseFetcher

class GitHubFetcher(BaseFetcher):
"""
Fetcher implementation for GitHub repositories.

Supports fetching commits, pull requests, issues, and releases.
"""

def __init__(self, pat: str, start_date=None, end_date=None, repo_filter=None, authors=None):
super().__init__(pat, start_date, end_date, repo_filter, authors)
self.github = Github(self.pat)
self.user = self.github.get_user()
self.user = self.github.get_user()
self.repos = self.user.get_repos(affiliation="owner,collaborator,organization_member")
self.authors.append(self.user.login)

@property
def repos_names(self)->List[str]:
def repos_names(self) -> List[str]:
return [repo.name for repo in self.repos]

def _stop_fetching(self, date_obj: datetime) -> bool:
Expand Down Expand Up @@ -101,7 +107,6 @@ def fetch_pull_requests(self) -> List[Dict[str, Any]]:
break
return entries


def fetch_issues(self) -> List[Dict[str, Any]]:
entries = []
issues = self.user.get_issues()
Expand All @@ -117,4 +122,56 @@ def fetch_issues(self) -> List[Dict[str, Any]]:
entries.append(entry)
if self._stop_fetching(issue_date):
break
return entries
return entries

def fetch_releases(self) -> List[Dict[str, Any]]:
"""
Fetch releases for all repositories accessible to the user.

Returns:
List[Dict[str, Any]]: List of releases, each as a structured dictionary with:
- tag_name: str
- name: str
- repo: str
- author: str
- published_at: datetime
- created_at: datetime
- draft: bool
- prerelease: bool
- body: str
- assets: List[Dict[str, Any]] (each with name, size, download_url, content_type, etc.)
"""
releases = []
for repo in self.repos:
if self.repo_filter and repo.name not in self.repo_filter:
continue
try:
for rel in repo.get_releases():
# Compose asset list
assets = []
for asset in rel.get_assets():
assets.append({
"name": asset.name,
"size": asset.size,
"download_url": asset.browser_download_url,
"content_type": asset.content_type,
"created_at": asset.created_at,
"updated_at": asset.updated_at,
})
release_entry = {
"tag_name": rel.tag_name,
"name": rel.title if hasattr(rel, "title") else rel.name,
"repo": repo.name,
"author": rel.author.login if rel.author else None,
"published_at": rel.published_at,
"created_at": rel.created_at,
"draft": rel.draft,
"prerelease": rel.prerelease,
"body": rel.body,
"assets": assets,
}
releases.append(release_entry)
except Exception:
# If fetching releases fails for a repo, skip it (could be permissions or no releases)
continue
return releases
Loading
Loading