PK!e4, README.md# git-changelog Automatic changelog generator. From git logs to change logs. - Installation: `sudo pip3 install git-changelog` - Features: - [Jinja2][jinja2] templates! You get full control over the rendering. Built-in [Keep a Changelog][keep-a-changelog] and [Angular][angular] templates (also see [Conventional Changelog][conventional-changelog]). - Commit styles/conventions parsing. Built-in [Angular][angular-style], [Atom][atom-style] and basic styles. - Git service/provider agnostic, plus references parsing (issues, commits, etc.). Built-in [GitHub][github-refs] and [Gitlab][gitlab-refs] support. - Understands [Semantic Versioning][semantic-versioning]: major/minor/patch for versions and commits. Guesses next version based on last commits. - Todo: - [Plugin architecture][issue-7], to support more commit styles and git services. - [Template context injection][issue-4], to furthermore customize how your changelog will be rendered. - [Easy access to "Breaking Changes"][issue-1] in the templates. - [Update changelog in-place][issue-2], paired with [commits/dates/versions range limitation ability][issue-3]. ## Command-line ```console $ git-changelog --help usage: git-changelog [-h] [-o OUTPUT] [-s {angular,atom,basic}] [-t {angular,keepachangelog}] [-v] REPOSITORY Command line tool for git-changelog Python package. positional arguments: REPOSITORY The repository path, relative or absolute. optional arguments: -h, --help Show this help message and exit. -o OUTPUT, --output OUTPUT Output to given file. Default: stdout. -s {angular,atom,basic}, --style {angular,atom,basic} The commit style to match against. -t {angular,keepachangelog}, --template {angular,keepachangelog} The Jinja2 template to use. Prefix with "path:" to specify the path to a directory containing a file named "changelog.md". -v, --version Show the current version of the program and exit. ``` [jinja2]: http://jinja.pocoo.org/ [keep-a-changelog]: http://keepachangelog.com/en/1.0.0/ [angular]: https://github.com/angular/angular/blob/master/CHANGELOG.md [conventional-changelog]: https://github.com/conventional-changelog/conventional-changelog [semantic-versioning]: http://semver.org/spec/v2.0.0.html [atom-style]: https://github.com/atom/atom/blob/master/CONTRIBUTING.md#git-commit-messages [angular-style]: https://github.com/angular/angular/blob/master/CONTRIBUTING.md#commit [github-refs]: https://help.github.com/articles/autolinked-references-and-urls/ [gitlab-refs]: https://docs.gitlab.com/ce/user/markdown.html#special-gitlab-references [issue-1]: https://gitlab.com/pawamoy/git-changelog/issues/1 [issue-2]: https://gitlab.com/pawamoy/git-changelog/issues/2 [issue-3]: https://gitlab.com/pawamoy/git-changelog/issues/3 [issue-4]: https://gitlab.com/pawamoy/git-changelog/issues/4 [issue-5]: https://gitlab.com/pawamoy/git-changelog/issues/5 [issue-6]: https://gitlab.com/pawamoy/git-changelog/issues/6 [issue-7]: https://gitlab.com/pawamoy/git-changelog/issues/7 PK!Kbbgit_changelog/__init__.py""" Git Changelog package. """ from .build import GitHub, GitLab __all__ = ["GitHub", "GitLab"] PK!Fcgit_changelog/__main__.py""" Entrypoint module, in case you use `python -m git_changelog`. Why does this file exist, and why __main__? For more info, read: - https://www.python.org/dev/peps/pep-0338/ - https://docs.python.org/2/using/cmdline.html#cmdoption-m - https://docs.python.org/3/using/cmdline.html#cmdoption-m """ import sys from git_changelog.cli import main if __name__ == "__main__": main(sys.argv[1:]) PK!E|**git_changelog/build.pyfrom __future__ import print_function import sys from datetime import datetime from subprocess import check_output # nosec from .providers import GitHub, GitLab from .style import AngularStyle, AtomStyle, BasicStyle, CommitStyle def bump(version, part="patch"): major, minor, patch = version.split(".", 2) patch = patch.split("-", 1) pre = "" if len(patch) > 1: patch, pre = patch else: patch = patch[0] if part == "major": major = str(int(major) + 1) minor = patch = "0" elif part == "minor": minor = str(int(minor) + 1) patch = "0" elif part == "patch" and not pre: patch = str(int(patch) + 1) return ".".join((major, minor, patch)) class Commit: def __init__( self, hash, author_name="", author_email="", author_date="", committer_name="", committer_email="", committer_date="", refs="", subject="", body=None, url="", ): self.hash = hash self.author_name = author_name self.author_email = author_email self.author_date = datetime.utcfromtimestamp(float(author_date)) self.committer_name = committer_name self.committer_email = committer_email self.committer_date = datetime.utcfromtimestamp(float(committer_date)) self.subject = subject self.body = body or [] self.url = url tag = "" for ref in refs.split(","): ref = ref.strip() if ref.startswith("tag: "): tag = ref.replace("tag: ", "") break self.tag = self.version = tag self.text_refs = {} self.style = {} def update_with_style(self, style): self.style.update(style.parse_commit(self)) def update_with_provider(self, provider): # set the commit url based on provider # FIXME: hardcoded 'commits' if "commits" in provider.REF: self.url = provider.build_ref_url("commits", {"ref": self.hash}) else: # use default "commit" url (could be wrong) self.url = "%s/%s/%s/commit/%s" % (provider.url, provider.namespace, provider.project, self.hash) # build commit text references from its subject and body for ref_type in provider.REF.keys(): self.text_refs[ref_type] = provider.get_refs(ref_type, "\n".join([self.subject] + self.body)) if "issues" in self.text_refs: self.text_refs["issues_not_in_subject"] = [] for issue in self.text_refs["issues"]: if issue.ref not in self.subject: self.text_refs["issues_not_in_subject"].append(issue) class Section: def __init__(self, type="", commits=None): self.type = type self.commits = commits or [] class Version: def __init__(self, tag="", date="", sections=None, commits=None, url="", compare_url=""): self.tag = tag self.date = date self.sections_list = sections or [] self.sections_dict = {s.type: s for s in self.sections_list} self.commits = commits or [] self.url = url self.compare_url = compare_url self.previous_version = None self.next_version = None @property def typed_sections(self): return [s for s in self.sections_list if s.type] @property def untyped_section(self): return self.sections_dict.get("", None) @property def is_major(self): return self.tag.split(".", 1)[1].startswith("0.0") @property def is_minor(self): return self.tag.split(".", 2)[2] class Changelog: MARKER = "--GITOLOG MARKER--" FORMAT = ( "%H%n" # commit hash "%an%n" # author name "%ae%n" # author email "%ad%n" # author date "%cn%n" # committer name "%ce%n" # committer email "%cd%n" # committer date "%D%n" # tag "%s%n" # subject "%b%n" + MARKER # body ) STYLE = {"basic": BasicStyle, "angular": AngularStyle, "atom": AtomStyle} def __init__(self, repository, provider=None, style=None): self.repository = repository # set provider if not provider: remote_url = self.get_remote_url() split = remote_url.split("/") provider_url = "/".join(split[:3]) namespace, project = split[3], split[4] if "github" in provider_url: provider = GitHub(namespace, project, url=provider_url) elif "gitlab" in provider_url: provider = GitLab(namespace, project, url=provider_url) self.remote_url = remote_url self.provider = provider # set style if isinstance(style, str): try: style = self.STYLE[style]() except KeyError: print("git-changelog: no such style available: %s, " "using default style" % style, file=sys.stderr) style = BasicStyle() elif style is None: style = BasicStyle() elif issubclass(style, CommitStyle): style = style() elif isinstance(style, CommitStyle): pass self.style = style # get git log and parse it into list of commits self.raw_log = self.get_log() self.commits = self.parse_commits() # apply dates to commits and group them by version dates = self.apply_versions_to_commits() versions = self.group_commits_by_version(dates) self.versions_list = versions["as_list"] self.versions_dict = versions["as_dict"] # guess the next version number based on last version and recent commits last_version = self.versions_list[0] if not last_version.tag and last_version.previous_version: last_tag = last_version.previous_version.tag major = minor = False for commit in last_version.commits: if commit.style["is_major"]: major = True break elif commit.style["is_minor"]: minor = True if major: planned_tag = bump(last_tag, "major") elif minor: planned_tag = bump(last_tag, "minor") else: planned_tag = bump(last_tag, "patch") last_version.planned_tag = planned_tag last_version.url = self.provider.get_tag_url(tag=planned_tag) last_version.compare_url = self.provider.get_compare_url( base=last_version.previous_version.tag, target=last_version.planned_tag ) def get_remote_url(self): git_url = ( check_output(["git", "config", "--get", "remote.origin.url"], cwd=self.repository) # nosec .decode("utf-8") .rstrip("\n") ) if git_url.startswith("git@"): git_url = git_url.replace(":", "/", 1).replace("git@", "https://", 1) if git_url.endswith(".git"): git_url = git_url[:-4] return git_url def get_log(self): return check_output( ["git", "log", "--date=unix", "--format=" + self.FORMAT], cwd=self.repository # nosec ).decode("utf-8") def parse_commits(self): lines = self.raw_log.split("\n") size = len(lines) - 1 # don't count last blank line commits = [] pos = 0 while pos < size: commit = Commit( hash=lines[pos], author_name=lines[pos + 1], author_email=lines[pos + 2], author_date=lines[pos + 3], committer_name=lines[pos + 4], committer_email=lines[pos + 5], committer_date=lines[pos + 6], refs=lines[pos + 7], subject=lines[pos + 8], body=[lines[pos + 9]], ) # append body lines nbl_index = 10 while lines[pos + nbl_index] != self.MARKER: commit.body.append(lines[pos + nbl_index]) nbl_index += 1 pos += nbl_index + 1 # expand commit object with provider parsing if self.provider: commit.update_with_provider(self.provider) elif self.remote_url: # set the commit url based on remote_url (could be wrong) commit.url = self.remote_url + "/commit/" + commit.hash # expand commit object with style parsing if self.style: commit.update_with_style(self.style) commits.append(commit) return commits def apply_versions_to_commits(self): versions_dates = {"": None} version = None for commit in self.commits: if commit.version: version = commit.version versions_dates[version] = commit.committer_date.date() elif version: commit.version = version return versions_dates def group_commits_by_version(self, dates): versions_list = [] versions_dict = {} versions_types_dict = {} next_version = None for commit in self.commits: if commit.version not in versions_dict: version = versions_dict[commit.version] = Version(tag=commit.version, date=dates[commit.version]) version.url = self.provider.get_tag_url(tag=commit.version) if next_version: version.next_version = next_version next_version.previous_version = version next_version.compare_url = self.provider.get_compare_url( base=version.tag, target=next_version.tag or "HEAD" ) next_version = version versions_list.append(version) versions_types_dict[commit.version] = {} versions_dict[commit.version].commits.append(commit) if "type" in commit.style and commit.style["type"] not in versions_types_dict[commit.version]: section = versions_types_dict[commit.version][commit.style["type"]] = Section(type=commit.style["type"]) versions_dict[commit.version].sections_list.append(section) versions_dict[commit.version].sections_dict = versions_types_dict[commit.version] versions_types_dict[commit.version][commit.style["type"]].commits.append(commit) if next_version is not None: next_version.compare_url = self.provider.get_compare_url( base=versions_list[-1].commits[-1].hash, target=next_version.tag or "HEAD" ) return {"as_list": versions_list, "as_dict": versions_dict} PK!)Rp p git_changelog/cli.py""" Module that contains the command line application. Why does this file exist, and why not put this in __main__? You might be tempted to import things from __main__ later, but that will cause problems: the code will get executed twice: - When you run `python -m git_changelog` python will execute ``__main__.py`` as a script. That means there won't be any ``git_changelog.__main__`` in ``sys.modules``. - When you import __main__ it will get executed again (as a module) because there's no ``git_changelog.__main__`` in ``sys.modules``. Also see http://click.pocoo.org/5/setuptools/#setuptools-integration. """ from __future__ import print_function import argparse import sys from . import templates from .build import Changelog STYLES = ("angular", "atom", "basic") class Templates(tuple): def __contains__(self, item): return item.startswith("path:") or super(Templates, self).__contains__(item) def get_parser(): """Return a parser for the command-line arguments.""" parser = argparse.ArgumentParser( add_help=False, prog="git-changelog", description="Command line tool for git-changelog Python package." ) parser.add_argument("repository", metavar="REPOSITORY", help="The repository path, relative or absolute.") parser.add_argument( "-h", "--help", action="help", default=argparse.SUPPRESS, help="Show this help message and exit." ) parser.add_argument( "-o", "--output", action="store", dest="output", default=sys.stdout, help="Output to given file. Default: stdout.", ) parser.add_argument( "-s", "--style", choices=STYLES, default="basic", dest="style", help="The commit style to match against." ) parser.add_argument( "-t", "--template", choices=Templates(("angular", "keepachangelog")), default="keepachangelog", dest="template", help='The Jinja2 template to use. Prefix with "path:" to specify the path ' 'to a directory containing a file named "changelog.md".', ) parser.add_argument( "-v", "--version", action="version", version="git-changelog 0.1.0", help="Show the current version of the program and exit.", ) return parser def main(args=None): parser = get_parser() args = parser.parse_args(args=args) # get template if args.template.startswith("path:"): path = args.template.replace("path:", "", 1) try: template = templates.get_custom_template(path) except FileNotFoundError: print("git-changelog: no such directory, " "or missing changelog.md: %s" % path, file=sys.stderr) return 1 else: template = templates.get_template(args.template) # build data changelog = Changelog(args.repository, style=args.style) # get rendered contents rendered = template.render(changelog=changelog) # write result in specified output if args.output is sys.stdout: sys.stdout.write(rendered) else: with open(args.output, "w") as stream: stream.write(rendered) return 0 PK! a  git_changelog/providers.pyimport re class RefRe: BB = r"(?:^|[\s,])" # blank before BA = r"(?:[\s,]|$)" # blank after NP = r"(?:(?P[-\w]+)/)?(?P[-\w]+)" # namespace and project ID = r"{symbol}(?P[1-9]\d*)" ONE_WORD = r"{symbol}(?P\w*[-a-z_ ][-\w]*)" MULTI_WORD = r'{symbol}(?P"\w[- \w]*")' COMMIT = r"(?P[0-9a-f]{{{min},{max}}})" COMMIT_RANGE = r"(?P[0-9a-f]{{{min},{max}}}\.\.\.[0-9a-f]{{{min},{max}}})" MENTION = r"@(?P\w[-\w]*)" class Ref: def __init__(self, ref, url): self.ref = ref self.url = url def __str__(self): return self.ref + ": " + self.url class ProviderRefParser(object): REF = {} def get_refs(self, ref_type, text): return [ Ref(ref=match.group().strip(), url=self.build_ref_url(ref_type, match.groupdict())) for match in self.parse_refs(ref_type, text) ] def parse_refs(self, ref_type, text): if ref_type not in self.REF: refs = [k for k in self.REF.keys() if k.startswith(ref_type)] return [m for ref in refs for m in self.REF[ref].finditer(text)] return [m for m in self.REF[ref_type]["regex"].finditer(text)] def build_ref_url(self, ref_type, match_dict): return self.REF[ref_type]["url"].format(**match_dict) class GitHub(ProviderRefParser): url = "https://github.com" project_url = "{base_url}/{namespace}/{project}" tag_url = "{base_url}/{namespace}/{project}/releases/tag/{ref}" REF = dict( issues=dict( regex=re.compile(RefRe.BB + RefRe.NP + "?" + RefRe.ID.format(symbol="#"), re.I), url="{base_url}/{namespace}/{project}/issues/{ref}", ), commits=dict( regex=re.compile( RefRe.BB + r"(?:{np}@)?{commit}{ba}".format(np=RefRe.NP, commit=RefRe.COMMIT.format(min=7, max=40), ba=RefRe.BA), re.I, ), url="{base_url}/{namespace}/{project}/commit/{ref}", ), commits_ranges=dict( regex=re.compile( RefRe.BB + r"(?:{np}@)?{commit_range}".format( np=RefRe.NP, commit_range=RefRe.COMMIT_RANGE.format(min=7, max=40) ), re.I, ), url="{base_url}/{namespace}/{project}/compare/{ref}", ), mentions=dict(regex=re.compile(RefRe.BB + RefRe.MENTION, re.I), url="{base_url}/{ref}"), ) def __init__(self, namespace, project, url=url): self.namespace = namespace self.project = project self.url = url def build_ref_url(self, ref_type, match_dict): match_dict["base_url"] = self.url if not match_dict.get("namespace"): match_dict["namespace"] = self.namespace if not match_dict.get("project"): match_dict["project"] = self.project return super(GitHub, self).build_ref_url(ref_type, match_dict) def get_tag_url(self, tag=""): return self.tag_url.format(base_url=self.url, namespace=self.namespace, project=self.project, ref=tag) def get_compare_url(self, base, target): return self.build_ref_url("commits_ranges", {"ref": "%s...%s" % (base, target)}) class GitLab(ProviderRefParser): url = "https://gitlab.com" project_url = "{base_url}/{namespace}/{project}" tag_url = "{base_url}/{namespace}/{project}/tags/{ref}" REF = dict( issues=dict( regex=re.compile(RefRe.BB + RefRe.NP + "?" + RefRe.ID.format(symbol="#"), re.I), url="{base_url}/{namespace}/{project}/issues/{ref}", ), merge_requests=dict( regex=re.compile(RefRe.BB + RefRe.NP + "?" + RefRe.ID.format(symbol=r"!"), re.I), url="{base_url}/{namespace}/{project}/merge_requests/{ref}", ), snippets=dict( regex=re.compile(RefRe.BB + RefRe.NP + "?" + RefRe.ID.format(symbol=r"\$"), re.I), url="{base_url}/{namespace}/{project}/snippets/{ref}", ), labels_ids=dict( regex=re.compile(RefRe.BB + RefRe.NP + "?" + RefRe.ID.format(symbol=r"~"), re.I), url="{base_url}/{namespace}/{project}/issues?label_name[]={ref}", # no label_id param? ), labels_one_word=dict( regex=re.compile( # also matches label IDs RefRe.BB + RefRe.NP + "?" + RefRe.ONE_WORD.format(symbol=r"~"), re.I ), url="{base_url}/{namespace}/{project}/issues?label_name[]={ref}", ), labels_multi_word=dict( regex=re.compile(RefRe.BB + RefRe.NP + "?" + RefRe.MULTI_WORD.format(symbol=r"~"), re.I), url="{base_url}/{namespace}/{project}/issues?label_name[]={ref}", ), milestones_ids=dict( regex=re.compile( # also matches milestones IDs RefRe.BB + RefRe.NP + "?" + RefRe.ID.format(symbol=r"%"), re.I ), url="{base_url}/{namespace}/{project}/milestones/{ref}", ), milestones_one_word=dict( regex=re.compile(RefRe.BB + RefRe.NP + "?" + RefRe.ONE_WORD.format(symbol=r"%"), re.I), url="{base_url}/{namespace}/{project}/milestones", # cannot guess ID ), milestones_multi_word=dict( regex=re.compile(RefRe.BB + RefRe.NP + "?" + RefRe.MULTI_WORD.format(symbol=r"%"), re.I), url="{base_url}/{namespace}/{project}/milestones", # cannot guess ID ), commits=dict( regex=re.compile( RefRe.BB + r"(?:{np}@)?{commit}{ba}".format(np=RefRe.NP, commit=RefRe.COMMIT.format(min=8, max=40), ba=RefRe.BA), re.I, ), url="{base_url}/{namespace}/{project}/commit/{ref}", ), commits_ranges=dict( regex=re.compile( RefRe.BB + r"(?:{np}@)?{commit_range}".format( np=RefRe.NP, commit_range=RefRe.COMMIT_RANGE.format(min=8, max=40) ), re.I, ), url="{base_url}/{namespace}/{project}/compare/{ref}", ), mentions=dict(regex=re.compile(RefRe.BB + RefRe.MENTION, re.I), url="{base_url}/{ref}"), ) def __init__(self, namespace, project, url=url): self.namespace = namespace self.project = project self.url = url def build_ref_url(self, ref_type, match_dict): match_dict["base_url"] = self.url if not match_dict.get("namespace"): match_dict["namespace"] = self.namespace if not match_dict.get("project"): match_dict["project"] = self.project if ref_type.startswith("label"): match_dict["ref"] = match_dict["ref"].replace('"', "").replace(" ", "+") return super(GitLab, self).build_ref_url(ref_type, match_dict) def get_tag_url(self, tag=""): return self.tag_url.format(base_url=self.url, namespace=self.namespace, project=self.project, ref=tag) def get_compare_url(self, base, target): return self.build_ref_url("commits_ranges", {"ref": "%s...%s" % (base, target)}) PK!igit_changelog/style.pyimport re class CommitStyle: def parse_commit(self, commit): raise NotImplementedError class BasicStyle(CommitStyle): TYPES = { "add": "Added", "fix": "Fixed", "change": "Changed", "remove": "Removed", "merge": "Merged", "doc": "Documented", } TYPE_REGEX = re.compile(r"^(?P(%s))" % "|".join(TYPES.keys()), re.I) BREAK_REGEX = re.compile(r"^break(s|ing changes)?[ :].+$", re.I) def parse_commit(self, commit): commit_type = self.parse_type(commit.subject) message = "\n".join([commit.subject] + commit.body) is_major = self.is_major(message) is_minor = not is_major and self.is_minor(commit_type) is_patch = not any((is_major, is_minor)) return dict(type=commit_type, is_major=is_major, is_minor=is_minor, is_patch=is_patch) def parse_type(self, commit_subject): type_match = self.TYPE_REGEX.match(commit_subject) if type_match: return self.TYPES.get(type_match.groupdict().get("type").lower()) return "" def is_minor(self, commit_type): return commit_type == self.TYPES["add"] def is_major(self, commit_message): return bool(self.BREAK_REGEX.match(commit_message)) class AngularStyle(CommitStyle): TYPES = { # 'build': 'Build', # 'ci': 'CI', "perf": "Performance Improvements", "feat": "Features", "fix": "Bug Fixes", "revert": "Reverts", # 'docs': 'Docs', # 'style': '', "refactor": "Code Refactoring", # 'test': '', # 'chore': '', } SUBJECT_REGEX = re.compile(r"^(?P(%s))(?:\((?P.+)\))?: (?P.+)$" % ("|".join(TYPES.keys()))) BREAK_REGEX = re.compile(r"^break(s|ing changes)?[ :].+$", re.I) def parse_commit(self, commit): subject = self.parse_subject(commit.subject) message = "\n".join([commit.subject] + commit.body) is_major = self.is_major(message) is_minor = not is_major and self.is_minor(subject["type"]) is_patch = not any((is_major, is_minor)) return dict( type=subject["type"], scope=subject["scope"], subject=subject["subject"], is_major=is_major, is_minor=is_minor, is_patch=is_patch, ) def parse_subject(self, commit_subject): subject_match = self.SUBJECT_REGEX.match(commit_subject) if subject_match: dct = subject_match.groupdict() dct["type"] = self.TYPES[dct["type"]] return dct return {"type": "", "scope": "", "subject": commit_subject} @staticmethod def is_minor(commit_type): return commit_type == "feat" def is_major(self, commit_message): return bool(self.BREAK_REGEX.match(commit_message)) class AtomStyle(CommitStyle): TYPES = { ":art:": "", # when improving the format/structure of the code ":racehorse:": "", # when improving performance ":non-potable_water:": "", # when plugging memory leaks ":memo:": "", # when writing docs ":penguin:": "", # when fixing something on Linux ":apple:": "", # when fixing something on Mac OS ":checkered_flag:": "", # when fixing something on Windows ":bug:": "", # when fixing a bug ":fire:": "", # when removing code or files ":green_heart:": "", # when fixing the CI build ":white_check_mark:": "", # when adding tests ":lock:": "", # when dealing with security ":arrow_up:": "", # when upgrading dependencies ":arrow_down:": "", # when downgrading dependencies ":shirt:": "", # when removing linter warnings } PK!nfe###git_changelog/templates/__init__.pyimport os from jinja2 import Environment, FileSystemLoader from jinja2.exceptions import TemplateNotFound def get_path(): return os.path.dirname(os.path.abspath(__file__)) def get_env(path): return Environment(loader=FileSystemLoader(path)) # nosec def get_custom_template(path): try: return get_env(os.path.abspath(path)).get_template("changelog.md") except TemplateNotFound: raise FileNotFoundError def get_template(name): return get_env(os.path.join(get_path(), name)).get_template("changelog.md") PK!b;dd,git_changelog/templates/angular/changelog.md{% for version in changelog.versions_list -%} {% include 'version.md' with context %} {% endfor -%} PK!~pyy)git_changelog/templates/angular/commit.md- {% if commit.style.scope %}**{{ commit.style.scope }}:** {% endif %}{{ commit.style.subject }} ([{{ commit.hash|truncate(7, True, '') }}]({{ commit.url }})) {%- if commit.text_refs.issues_not_in_subject %}, related to {% for issue in commit.text_refs.issues_not_in_subject -%} [{{ issue.ref }}]({{ issue.url }}){% if not loop.last %}, {% endif -%} {%- endfor -%}{%- endif -%}PK!eso*git_changelog/templates/angular/section.md### {{ section.type or "Misc" }} {% for commit in section.commits|sort(attribute='subject') -%} {% include 'commit.md' with context %} {% endfor %} PK!*git_changelog/templates/angular/version.md{%- if version.tag or version.planned_tag -%} ## [{{ version.tag or version.planned_tag }}]({{ version.compare_url }}) {%- else -%} ## Unrealeased ([compare]({{ version.compare_url }})) {%- endif -%} {% if version.date %} ({{ version.date }}){% endif %} {% for type, section in version.sections_dict|dictsort -%} {%- if type -%} {% include 'section.md' with context %} {% endif -%} {%- endfor -%} PK!Q ]]3git_changelog/templates/keepachangelog/changelog.md# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). {% for version in changelog.versions_list -%} {% include 'version.md' with context %} {% endfor -%} PK!e%%0git_changelog/templates/keepachangelog/commit.md- {{ commit.subject }} ([{{ commit.hash|truncate(7, True, '') }}]({{ commit.url }})). {%- if commit.text_refs.issues_not_in_subject %} Related issues/PRs: {% for issue in commit.text_refs.issues_not_in_subject -%} {{ issue.ref }}{% if not loop.last %}, {% endif -%} {%- endfor -%}{%- endif -%}PK!eso1git_changelog/templates/keepachangelog/section.md### {{ section.type or "Misc" }} {% for commit in section.commits|sort(attribute='subject') -%} {% include 'commit.md' with context %} {% endfor %} PK!kd@YY1git_changelog/templates/keepachangelog/version.md{%- if version.tag or version.planned_tag -%} ## [{{ version.tag or version.planned_tag }}]({{ version.url }}) ([compare]({{ version.compare_url }})) {%- else -%} ## Unrealeased ([compare]({{ version.compare_url }})) {%- endif -%} {% if version.date %} - {{ version.date }}{% endif %} {% for type, section in version.sections_dict|dictsort -%} {%- if type and type != 'Merged' -%} {% include 'section.md' with context %} {% endif -%} {%- endfor -%} {%- if version.untyped_section -%} {%- with section = version.untyped_section -%} {% include 'section.md' with context %} {% endwith -%} {%- endif -%} PK!CJ|pyproject.toml[build-system] requires = ["poetry>=0.12"] build-backend = "poetry.masonry.api" [tool.poetry] name = "git-changelog" version = "0.1.0" description = "Automatic Changelog generator using Jinja2 templates." authors = ["Timothée Mazzucotelli "] license = "ISC" readme = "README.md" repository = "https://github.com/pawamoy/git-changelog" homepage = "https://github.com/pawamoy/git-changelog" keywords = ["git", "changelog", "changelog-generator", "commit-style", "commit-convention"] packages = [ { include = "git_changelog", from = "src" } ] include = [ "README.md", "pyproject.toml" ] [tool.poetry.dependencies] python = "^3.6" Jinja2 = "^2.10" [tool.poetry.dev-dependencies] bandit = "^1.5" black = { version = "^18.3-alpha.0", allows-prereleases = true} flake8 = "^3.7" ipython = "^7.3" isort = { version = "^4.3", extras = ["pyproject"] } jinja2-cli = { git = "https://github.com/mattrobenolt/jinja2-cli.git" } pytest = "^4.3" pytest-cov = "^2.6" pytest-sugar = "^0.9.2" pytest-xdist = "^1.26" recommonmark = "^0.4.0" safety = "^1.8" sphinx = "^1.8" sphinxcontrib-spelling = "^4.2" sphinx-rtd-theme = "^0.4.3" toml = "^0.10.0" [tool.poetry.scripts] git-changelog = "git_changelog.cli:main" [tool.black] line-length = 120 [tool.isort] line_length = 120 not_skip = "__init__.py" multi_line_output = 3 force_single_line = false balanced_wrapping = true default_section = "THIRDPARTY" known_first_party = "git_changelog" include_trailing_comma = true PK!H18.git_changelog-0.1.0.dist-info/entry_points.txtN+I/N.,()J,MHKOOZW\h]& 8n7x77*KLݍ rH4 }M?H7޼? 9*@Y2tؐiAx l0@da<@-ޣ#&+/jCSaPHq|.D14v\*5p84FMrTJ!Z=RHrUWq8M]J-@qLrI# bHwNqvo\'ie+ niۉdhp 0S=j|X^tQusȄYXc]W2🗉r{{ giqj7$vo.1^܌.oF[?">v:?Em`w 6w3itqj AйlL 8u05r +{eq#ۉp`SHc2Yp=0Ԏ\ƽifRɑCvYkWQr[/+SV9.j*$ t*2|ʪ Q8{s$W`oNZ.O@1;bJV6@a%J; K•8&(؅vdk9nE*&tSϓ煹5mk`ېB{ֺԫRzز%#vbRRRxN^w+/)HMޓLN)iU0x|~~!&e{3cy-{8k<#˫@֚bܚzS7%"SׄՂn2ö Y:ŰΎJe: +kFDگyz5]ɆnKVO!tMͧ3nx:0l~t;ә_PK!Hrs$git_changelog-0.1.0.dist-info/RECORDK8F[Ђ=@ŋ("LX3<#o{p{Yުu{de󝕜%q{eH,W)ڢi2y:ֈk> ‡a ̏v^(*y=e=}ޯT "Y۩S>>ZVr?: ! !S.`<؈yַrAqUɫཇEB3ux7[7ǰjmw[W$r.mςzC)|pP v-J3_d\OGNƻm `ࡀ)Vè/n:Öctt2<>d`XÜX|Cn*bFJ}r̥Iv(%/%V7\uw]* j*Kb< m KT,#~߽;$=Ir UY;vt*w 3nQ%^+g2W8,jэwTmh<%ptB/)C1ùhzW,-0T Xbax̯f\^9(r,PZBZCܧ yˬ&Dk{X`НHjS,A3w{,a|xȱV@&O[[ifR̷ztD_C2Tpj:9VuV`L%gNS+&sKGR?