Source code for generate_changelog.git_ops

"""git information access."""

import datetime
import os
import re
from dataclasses import dataclass
from typing import List, Optional, Union

from git import Actor, Commit, Repo

from generate_changelog.configuration import get_config

GIT_FORMAT_KEYS = {
    "sha1": "%H",
    "sha1_short": "%h",
    "author_name": "%an",
    "author_email": "%ae",
    "author_date_timestamp": "%at",
    "committer_name": "%cn",
    "committer_date_timestamp": "%ct",
    "summary": "%s",
    "body": "%b",
}
GIT_FULL_FORMAT_STRING = "%x00".join(GIT_FORMAT_KEYS.values()) + "%x1F"


[docs] @dataclass(frozen=True) class TagInfo: """Simple storage of tag information.""" name: str commit: str tagger: Union[str, Actor] tagged_datetime: datetime.datetime @property def date_string(self) -> str: """Convenience method to return an ISO8601 date string.""" return self.tagged_datetime.strftime("%Y-%m-%d")
[docs] @dataclass(frozen=True) class GitTag: """A git tag with information and commits.""" tag_name: str tag_info: TagInfo commits: List[Commit]
[docs] def get_repo(repo_path: Optional[str] = None) -> Repo: """ Get the git repo from a specific path or the current working directory. Args: repo_path: The path to the directory with git repository. If None, the current working directory is used. Returns: Repository object """ return Repo(repo_path or os.getcwd())
[docs] def parse_commits(repository: Repo, starting_rev: Optional[str] = None, ending_rev: Optional[str] = None) -> list: """ Parse the commits for later processing. Args: repository: The repository object. starting_rev: Include all commits after this revision. ending_rev: include all commmits before and including this revision. Returns: A list of CommitInfo objects. """ if starting_rev and ending_rev: revs = f"{starting_rev}..{ending_rev}" elif starting_rev: revs = f"{starting_rev}..HEAD" elif ending_rev: revs = ending_rev else: revs = "HEAD" log_opts = ["-z", "--topo-order", "--pretty=tformat:%H"] if get_config().include_merges: log_opts.append("--no-merges") log_opts.append(revs) out: str = repository.git.log(*log_opts) commits = out.split("\x00") return [repository.commit(commit) for commit in commits if commit]
[docs] def get_tags(repository: Repo) -> List[TagInfo]: """ Get all the tags in a repository. Args: repository: The repository object containing the tags Returns: A list of TagInfo objects with the most recent first """ tags = repository.tags tags_list = [] for tag in tags: commit = tag.commit if tag.tag: tzoffset = datetime.timedelta(seconds=-tag.tag.tagger_tz_offset) tzone = datetime.timezone(tzoffset) tag_datetime = datetime.datetime.utcfromtimestamp(tag.tag.tagged_date).astimezone(tzone) tagger = tag.tag.tagger else: tag_datetime = tag.commit.committed_datetime tagger = tag.commit.committer tag_info = TagInfo( name=tag.name, commit=commit.hexsha, tagger=tagger, tagged_datetime=tag_datetime, ) tags_list.append(tag_info) tags_list.sort(key=lambda t: t.tagged_datetime, reverse=True) return tags_list
[docs] def get_commits_by_tags(repository: Repo, tag_filter_pattern: str, starting_tag: Optional[str] = None) -> List[GitTag]: """ Group commits by the tags they belong to. Args: repository: The git repository object tag_filter_pattern: A regular expression pattern that matches valid tags as versions starting_tag: Only include tags after this one Returns: A list of dictionaries with tag information with most recent first """ from generate_changelog.utilities import pairs tags = [tag for tag in get_tags(repository) if re.match(tag_filter_pattern, tag.name)] head_commit = repository.commit("HEAD") head_tagger = head_commit.committer.name if head_commit.committer.email: head_tagger += f" <{head_commit.committer.email}>" head = TagInfo( name="HEAD", commit=head_commit.hexsha, tagger=head_tagger, tagged_datetime=head_commit.committed_datetime, ) tags.insert(0, head) groups = [] for end_tag, start_tag in pairs(tags): start_tag_name = getattr(start_tag, "name", None) groups.append( GitTag( tag_name=end_tag.name, tag_info=end_tag, commits=parse_commits(repository, start_tag_name, end_tag.name), ) ) if starting_tag and start_tag_name == starting_tag: break return groups