From 69d50025c0e2a8fe2a43258f12c24c2115605df5 Mon Sep 17 00:00:00 2001 From: Daniel Edgy Edgecombe Date: Fri, 23 Aug 2024 22:48:52 +0200 Subject: [PATCH] feat: Support special github scenarios via configuration (#52) Define behaviour for stripping PR information from github descriptions. Extract PR information as a footer, and extract common github keywords as footers. closes #50 --- changelog_gen/config.py | 21 +++++-- changelog_gen/extractor.py | 22 ++++++- docs/configuration.md | 46 ++++++++++++++ pyproject.toml | 5 ++ tests/cli/test_config.py | 2 - tests/test_config.py | 19 ++++++ tests/test_extractor.py | 120 +++++++++++++++++++++++++++---------- 7 files changed, 195 insertions(+), 40 deletions(-) diff --git a/changelog_gen/config.py b/changelog_gen/config.py index 9f21d2b..8281eed 100644 --- a/changelog_gen/config.py +++ b/changelog_gen/config.py @@ -27,10 +27,6 @@ FOOTER_PARSERS = [ r"(Refs)(: )(#?[\w-]+)", - # TODO(edgy): Parse github behind a github config - # https://github.com/NRWLDev/changelog-gen/issues/50 - r"(closes)( )(#[\w-]+)", - r"(fixes)( )(#[\w-]+)", r"(Authors)(: )(.*)", ] @@ -51,6 +47,15 @@ class PostProcessConfig: auth_env: str | None = None +@dataclasses.dataclass +class GithubConfig: + """Github specific helpers.""" + + strip_pr_from_description: bool = False + extract_pr_from_description: bool = False + extract_common_footers: bool = False + + STRICT_VALIDATOR = re.compile( r"^(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?$", ) @@ -88,6 +93,9 @@ class Config: # Version bumping files: dict = dataclasses.field(default_factory=dict) + # Github helpers + github: GithubConfig | None = None + # Changelog configuration date_format: str | None = None version_string: str = "v{new_version}" @@ -186,6 +194,7 @@ def _process_pyproject(pyproject: Path) -> dict: data["tool"]["changelog_gen"]["footer_parsers"] = footer_parsers data["tool"]["changelog_gen"]["commit_types"] = commit_types data["tool"]["changelog_gen"]["type_headers"] = type_headers + return data["tool"]["changelog_gen"] @@ -227,6 +236,10 @@ def read(path: str = "pyproject.toml", **kwargs) -> Config: msg = f"Failed to create post_process: {e!s}" raise RuntimeError(msg) from e + if "github" in cfg: + github = GithubConfig(**cfg["github"]) + cfg["github"] = github + try: return Config(**cfg) except TypeError as e: diff --git a/changelog_gen/extractor.py b/changelog_gen/extractor.py index 68404e8..6c498f4 100644 --- a/changelog_gen/extractor.py +++ b/changelog_gen/extractor.py @@ -102,8 +102,12 @@ def extract(self: t.Self) -> list[Change]: # noqa: C901, PLR0912, PLR0915 prm = re.search(r"\(#\d+\)$", description) if prm is not None: # Strip githubs additional link information from description. - description = re.sub(r" \(#\d+\)$", "", description) - footers["pr"] = Footer("PR", ": ", prm.group()[1:-1]) + if self.context.config.github and self.context.config.github.strip_pr_from_description: + description = re.sub(r" \(#\d+\)$", "", description) + + if self.context.config.github and self.context.config.github.extract_pr_from_description: + footers["pr"] = Footer("PR", ": ", prm.group()[1:-1]) + details = m[5] or "" # Handle missing refs in commit message, skip link generation in writer @@ -118,6 +122,20 @@ def extract(self: t.Self) -> list[Change]: # noqa: C901, PLR0912, PLR0915 if breaking: self.context.info(" Breaking change detected:\n %s: %s", commit_type, description) + footer_parsers = self.context.config.footer_parsers + if self.context.config.github and self.context.config.github.extract_common_footers: + footer_parsers.extend([ + r"(close)( )(#[\w-]+)", + r"(closes)( )(#[\w-]+)", + r"(closed)( )(#[\w-]+)", + r"(fix)( )(#[\w-]+)", + r"(fixes)( )(#[\w-]+)", + r"(fixed)( )(#[\w-]+)", + r"(resolve)( )(#[\w-]+)", + r"(resolves)( )(#[\w-]+)", + r"(resolved)( )(#[\w-]+)", + ]) + for line in details.split("\n"): for parser in self.context.config.footer_parsers: m = re.match(parser, line, re.IGNORECASE) diff --git a/docs/configuration.md b/docs/configuration.md index 7f9d381..951b112 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -418,6 +418,52 @@ pre_l = ["dev", "rc"] Enforce strict rules based on SemVer 2.0.0 and error if non conforming parser or serialisers are configured. + +## Github + +### `strip_pr_from_description` + _**[optional]**_
+ **default**: False + + Strip the `(#\d+)` from the end of github PR commit descriptions. + + Example: + +```toml +[tool.changelog_gen.github] +strip_pr_from_description = true +``` + +### `extract_pr_from_description` + _**[optional]**_
+ **default**: False + + Extract the `(#\d+)` from the end of github PR commit descriptions, and track + it as a footer for later extraction and link generation. Creates a `PR` footer entry. + + Example: + +```toml +[tool.changelog_gen.github] +extract_pr_from_description = true +``` + +### `extract_common_footers` + _**[optional]**_
+ **default**: False + + Extract supported keyword footers from github commits, `closes #1` etc. + + Supported footers can be found + [here](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue). + + Example: + +```toml +[tool.changelog_gen.github] +extract_common_footers = true +``` + ## Post processing ### `post_process` diff --git a/pyproject.toml b/pyproject.toml index e2f8c50..5b4b61a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,6 +71,11 @@ allowed_branches = [ ] date_format = "- %Y-%m-%d" +[tool.changelog_gen.github] +strip_pr_from_description = true +extract_pr_from_description = true +extract_common_footers = true + [[tool.changelog_gen.extractors]] footer = ["closes", "fixes", "Refs"] pattern = '#(?P\d+)' diff --git a/tests/cli/test_config.py b/tests/cli/test_config.py index 60d1011..7500dfa 100644 --- a/tests/cli/test_config.py +++ b/tests/cli/test_config.py @@ -35,8 +35,6 @@ def test_config_displayed(cli_runner): serialisers = ['{major}.{minor}.{patch}'] footer_parsers = [ '(Refs)(: )(#?[\w-]+)', - '(closes)( )(#[\w-]+)', - '(fixes)( )(#[\w-]+)', '(Authors)(: )(.*)', ] extractors = [] diff --git a/tests/test_config.py b/tests/test_config.py index 0ac4bdb..390d925 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -179,6 +179,25 @@ def test_read_picks_up_type_headers(self, config_factory): "test": "Miscellaneous", } + def test_read_picks_up_github_config(self, config_factory): + config_factory( + """ +[tool.changelog_gen] +current_version = "0.0.0" +[tool.changelog_gen.github] +strip_pr_from_description = true +extract_pr_from_description = true +extract_common_footers = true +""", + ) + + c = config.read() + assert c.github == config.GithubConfig( + strip_pr_from_description=True, + extract_pr_from_description=True, + extract_common_footers=True, + ) + class TestPostProcessConfig: def test_read_picks_up_no_post_process_config(self, config_factory): diff --git a/tests/test_extractor.py b/tests/test_extractor.py index 8b1dfac..509145b 100644 --- a/tests/test_extractor.py +++ b/tests/test_extractor.py @@ -3,7 +3,7 @@ import pytest from changelog_gen import extractor -from changelog_gen.config import Config +from changelog_gen.config import Config, GithubConfig from changelog_gen.context import Context from changelog_gen.extractor import Change, ChangeExtractor, Footer, Link from changelog_gen.vcs import Git @@ -51,7 +51,7 @@ def conventional_commits(multiversion_repo): Refs: #1 """, "update readme", - """feat: Detail about 2 + """feat: Detail about 2 (#2) Authors: @tom, @edgy closes #2 @@ -106,13 +106,12 @@ def test_git_commit_extraction(conventional_commits): assert changes == [ Change( "Features and Improvements", - "Detail about 2", + "Detail about 2 (#2)", short_hash=hashes[5][:7], commit_hash=hashes[5], commit_type="feat", footers=[ Footer("Authors", ": ", "@tom, @edgy"), - Footer("closes", " ", "#2"), ], ), Change( @@ -134,9 +133,7 @@ def test_git_commit_extraction(conventional_commits): short_hash=hashes[2][:7], commit_hash=hashes[2], commit_type="feat", - footers=[ - Footer("Fixes", " ", "#3"), - ], + footers=[], ), Change( "Bug fixes", @@ -164,13 +161,12 @@ def test_git_commit_extraction_include_all(conventional_commits): assert changes == [ Change( "Features and Improvements", - "Detail about 2", + "Detail about 2 (#2)", short_hash=hashes[5][:7], commit_hash=hashes[5], commit_type="feat", footers=[ Footer("Authors", ": ", "@tom, @edgy"), - Footer("closes", " ", "#2"), ], ), Change( @@ -200,9 +196,7 @@ def test_git_commit_extraction_include_all(conventional_commits): short_hash=hashes[2][:7], commit_hash=hashes[2], commit_type="feat", - footers=[ - Footer("Fixes", " ", "#3"), - ], + footers=[], ), Change( "Miscellaneous", @@ -241,13 +235,12 @@ def test_git_commit_extraction_extractors(conventional_commits): assert changes == [ Change( "Features and Improvements", - "Detail about 2", + "Detail about 2 (#2)", short_hash=hashes[5][:7], commit_hash=hashes[5], commit_type="feat", footers=[ Footer("Authors", ": ", "@tom, @edgy"), - Footer("closes", " ", "#2"), ], extractions={"author": ["tom", "edgy"]}, ), @@ -271,10 +264,6 @@ def test_git_commit_extraction_extractors(conventional_commits): short_hash=hashes[2][:7], commit_hash=hashes[2], commit_type="feat", - footers=[ - Footer("Fixes", " ", "#3"), - ], - extractions={"issue_ref": ["3"]}, ), Change( "Bug fixes", @@ -317,13 +306,12 @@ def test_git_commit_extraction_link_generators(conventional_commits): assert changes == [ Change( "Features and Improvements", - "Detail about 2", + "Detail about 2 (#2)", short_hash=hashes[5][:7], commit_hash=hashes[5], commit_type="feat", footers=[ Footer("Authors", ": ", "@tom, @edgy"), - Footer("closes", " ", "#2"), ], links=[ Link("@tom", "https://github.com/tom"), @@ -356,14 +344,9 @@ def test_git_commit_extraction_link_generators(conventional_commits): short_hash=hashes[2][:7], commit_hash=hashes[2], commit_type="feat", - footers=[ - Footer("Fixes", " ", "#3"), - ], links=[ - Link("3", "https://github.com/NRWLDev/changelog-gen/issues/3"), Link(hashes[2][:7], f"https://github.com/NRWLDev/changelog-gen/commit/{hashes[2]}"), ], - extractions={"issue_ref": ["3"]}, ), Change( "Bug fixes", @@ -384,6 +367,83 @@ def test_git_commit_extraction_link_generators(conventional_commits): ] +def test_git_commit_extraction_with_github_optionals(conventional_commits): + hashes = conventional_commits + extractors = [ + {"footer": ["Refs", "fixes"], "pattern": r"#(?P\d+)"}, + {"footer": "Authors", "pattern": r"@(?P\w+)"}, + ] + ctx = Context( + Config( + current_version="0.0.2", + extractors=extractors, + github=GithubConfig( + strip_pr_from_description=True, + extract_pr_from_description=True, + extract_common_footers=True, + ), + ), + ) + git = Git(ctx) + + e = ChangeExtractor(ctx, git) + + changes = e.extract() + + assert changes == [ + Change( + "Features and Improvements", + "Detail about 2", + short_hash=hashes[5][:7], + commit_hash=hashes[5], + commit_type="feat", + footers=[ + Footer("PR", ": ", "#2"), + Footer("Authors", ": ", "@tom, @edgy"), + Footer("closes", " ", "#2"), + ], + extractions={"author": ["tom", "edgy"]}, + ), + Change( + "Bug fixes", + "Detail about 1", + breaking=True, + short_hash=hashes[3][:7], + commit_hash=hashes[3], + commit_type="fix", + footers=[ + Footer("Refs", ": ", "#1"), + ], + extractions={"issue_ref": ["1"]}, + ), + Change( + "Features and Improvements", + "Detail about 3", + breaking=True, + scope="docs", + short_hash=hashes[2][:7], + commit_hash=hashes[2], + commit_type="feat", + footers=[ + Footer("Fixes", " ", "#3"), + ], + extractions={"issue_ref": ["3"]}, + ), + Change( + "Bug fixes", + "Detail about 4", + scope="config", + short_hash=hashes[0][:7], + commit_hash=hashes[0], + commit_type="fix", + footers=[ + Footer("Refs", ": ", "#4"), + ], + extractions={"issue_ref": ["4"]}, + ), + ] + + def test_git_commit_extraction_handles_random_tags(conventional_commits, multiversion_repo): hashes = conventional_commits multiversion_repo.api.create_tag("a-random-tag") @@ -411,13 +471,12 @@ def test_git_commit_extraction_handles_random_tags(conventional_commits, multive ), Change( "Features and Improvements", - "Detail about 2", + "Detail about 2 (#2)", short_hash=hashes[5][:7], commit_hash=hashes[5], commit_type="feat", footers=[ Footer("Authors", ": ", "@tom, @edgy"), - Footer("closes", " ", "#2"), ], ), Change( @@ -439,9 +498,7 @@ def test_git_commit_extraction_handles_random_tags(conventional_commits, multive short_hash=hashes[2][:7], commit_hash=hashes[2], commit_type="feat", - footers=[ - Footer("Fixes", " ", "#3"), - ], + footers=[], ), Change( "Bug fixes", @@ -544,7 +601,7 @@ def test_git_commit_extraction_picks_up_additional_allowed_characted(multiversio multiversion_repo.api.index.commit(msg) hashes.append(str(multiversion_repo.api.head.commit)) - ctx = Context(Config(current_version="0.0.2")) + ctx = Context(Config(current_version="0.0.2", github=GithubConfig(strip_pr_from_description=True))) git = Git(ctx) e = ChangeExtractor(ctx, git) @@ -560,7 +617,6 @@ def test_git_commit_extraction_picks_up_additional_allowed_characted(multiversio commit_hash=hashes[0], commit_type="fix", footers=[ - Footer("PR", ": ", "#20"), Footer("Refs", ": ", "#1"), ], ),