diff --git a/.github/actions/install_python_and_poetry/action.yml b/.github/actions/install_python_and_poetry/action.yml index 3fa8b91..c2752c6 100644 --- a/.github/actions/install_python_and_poetry/action.yml +++ b/.github/actions/install_python_and_poetry/action.yml @@ -43,7 +43,7 @@ runs: run: "pipx install --python='${{ steps.python.outputs.python-path }}' poetry==${{ inputs.poetry-version }}" - name: "Cache venv" - uses: "actions/cache@v3.0.10" + uses: "actions/cache@v3.0.11" with: path: "./.venv/" key: "venv-${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ hashFiles('poetry.lock') }}${{ inputs.cache-key-suffix }}" diff --git a/.github/actions/run_pre_commit/action.yml b/.github/actions/run_pre_commit/action.yml index bb66960..61d8cd3 100644 --- a/.github/actions/run_pre_commit/action.yml +++ b/.github/actions/run_pre_commit/action.yml @@ -14,7 +14,7 @@ runs: shell: "bash" - name: "Cache mypy" - uses: "actions/cache@v3.0.10" + uses: "actions/cache@v3.0.11" with: path: "./.mypy_cache/" key: "mypy-${{ runner.os }}-${{ inputs.python-version }}" diff --git a/.github/actions/run_tox/action.yml b/.github/actions/run_tox/action.yml index 7c4f65a..35bb830 100644 --- a/.github/actions/run_tox/action.yml +++ b/.github/actions/run_tox/action.yml @@ -36,7 +36,7 @@ runs: shell: "bash" - name: "Cache tox" - uses: "actions/cache@v3.0.10" + uses: "actions/cache@v3.0.11" with: path: "./.tox/" key: "tox-${{ inputs.python-version }}" diff --git a/.github/workflows/release_pr.yml b/.github/workflows/release_pr.yml index 0974f89..0ab2342 100644 --- a/.github/workflows/release_pr.yml +++ b/.github/workflows/release_pr.yml @@ -46,7 +46,7 @@ jobs: run: "poetry run python3 -m badabump --ci ${{ github.event.inputs.args }}" - id: "token" - uses: "tibdex/github-app-token@v1.7" + uses: "tibdex/github-app-token@v1.7.0" with: app_id: "${{ secrets.BADABUMP_APP_ID }}" private_key: "${{ secrets.BADABUMP_APP_PRIVATE_KEY }}" diff --git a/.github/workflows/release_tag.yml b/.github/workflows/release_tag.yml index 9852580..c98fb75 100644 --- a/.github/workflows/release_tag.yml +++ b/.github/workflows/release_tag.yml @@ -20,7 +20,7 @@ jobs: steps: - id: "token" - uses: "tibdex/github-app-token@v1.7" + uses: "tibdex/github-app-token@v1.7.0" with: app_id: "${{ secrets.BADABUMP_APP_ID }}" private_key: "${{ secrets.BADABUMP_APP_PRIVATE_KEY }}" diff --git a/src/badabump/changelog.py b/src/badabump/changelog.py index e6b4fcd..1e6c7ea 100644 --- a/src/badabump/changelog.py +++ b/src/badabump/changelog.py @@ -29,6 +29,10 @@ r"^(Closes|Fixes|Issue|Ref|Relates): (?P.+)$", re.M ) +FORMATTED_COMMIT_WITH_PR_RE = re.compile( + r"^(?P.*) \(\#(?P\d+)\)$" +) + logger = logging.getLogger(__name__) @@ -202,16 +206,12 @@ def format_block( return "\n\n".join((header, items)) def format_commits(commits: Iterator[ConventionalCommit]) -> str: - formatted_commits = [] - for commit in commits: - formatted_commit = commit.format( - format_type, ignore_footer_urls=ignore_footer_urls + return "\n".join( + ul_li(item) + for item in prepare_formatted_commits( + commits, format_type, ignore_footer_urls=ignore_footer_urls ) - if formatted_commit in formatted_commits: - continue - formatted_commits.append(formatted_commit) - - return "\n".join(ul_li(item) for item in formatted_commits) + ) features = format_block("Features:", self.feature_commits) fixes = format_block("Fixes:", self.fix_commits) @@ -279,6 +279,55 @@ def markdown_header(value: str, *, level: int) -> str: return f'{"#" * level} {value}' +def prepare_formatted_commits( + commits: Iterator[ConventionalCommit], + format_type: FormatTypeEnum, + *, + ignore_footer_urls: bool, +) -> List[str]: + storage = [] + + for commit in commits: + # First, format commit as asked + formatted_commit = commit.format( + format_type, ignore_footer_urls=ignore_footer_urls + ) + + # Next, check whether it is already added to the storage. If previously + # added - do nothing + if formatted_commit in storage: + continue + + # If not yet added, check if commit match regex "with PR" + with_pr_matched = FORMATTED_COMMIT_WITH_PR_RE.match(formatted_commit) + # If not matched - just append formatted commit to the storage + if not with_pr_matched: + storage.append(formatted_commit) + continue + + # Now, attempt to match other commit in storage + with_pr_data = with_pr_matched.groupdict() + with_pr_prefix = with_pr_data["formatted_commit"] + other_idx = -1 + + for idx, other_commit in enumerate(storage): + if other_commit.startswith(with_pr_prefix): + other_idx = idx + break + + # If such commit does not exist - just append formatted commit to the + # storage + if other_idx == -1: + storage.append(formatted_commit) + continue + + # Otherwise, merge commit message of other commit and current commit + pr_number = with_pr_data["pr_number"] + storage[other_idx] = f"{storage[other_idx][:-1]}, #{pr_number})" + + return storage + + def rst_h1(value: str) -> str: return rst_header(value, symbol="=") diff --git a/tests/test_changelog.py b/tests/test_changelog.py index b8c7254..b7a942e 100644 --- a/tests/test_changelog.py +++ b/tests/test_changelog.py @@ -44,6 +44,18 @@ Fixes: DEV-1010 """ +DEFAULT_GIT_COMMITS = [ + FEATURE_COMMIT, + FIX_COMMIT, + CI_BREAKING_COMMIT, + REFACTOR_COMMIT, + DOCS_SCOPE_COMMIT, + REFACTOR_COMMIT, + CI_BREAKING_COMMIT, + REFACTOR_COMMIT, +] + + CHANGELOG_EMPTY = "No changes since last pre-release" CHANGELOG_FILE_MD = """## Features: @@ -121,10 +133,6 @@ UTCNOW = datetime.datetime.utcnow() -def test_changelog_duplicate_commits_with_prs(): - ... - - @pytest.mark.parametrize( "changelog_type, format_type, expected", ( @@ -162,18 +170,7 @@ def test_changelog_empty(changelog_type, format_type, expected): ), ) def test_changelog_format_file(format_type, is_pre_release, expected): - changelog = ChangeLog.from_git_commits( - [ - FEATURE_COMMIT, - FIX_COMMIT, - CI_BREAKING_COMMIT, - REFACTOR_COMMIT, - DOCS_SCOPE_COMMIT, - REFACTOR_COMMIT, - CI_BREAKING_COMMIT, - REFACTOR_COMMIT, - ] - ) + changelog = ChangeLog.from_git_commits(DEFAULT_GIT_COMMITS) content = changelog.format( ChangeLogTypeEnum.changelog_file, format_type, @@ -192,15 +189,7 @@ def test_changelog_format_file(format_type, is_pre_release, expected): ), ) def test_changelog_format_git(format_type, is_pre_release, expected): - changelog = ChangeLog.from_git_commits( - [ - FEATURE_COMMIT, - FIX_COMMIT, - CI_BREAKING_COMMIT, - DOCS_SCOPE_COMMIT, - REFACTOR_COMMIT, - ] - ) + changelog = ChangeLog.from_git_commits(DEFAULT_GIT_COMMITS) content = changelog.format( ChangeLogTypeEnum.git_commit, format_type, @@ -247,6 +236,29 @@ def test_changelog_invalid_commit_non_strict_mode(): assert changelog.has_micro_change is True +def test_changelog_merge_similar_commits(): + changelog = ChangeLog.from_git_commits( + [ + "fix: Does not matter (#9999)", + f"{FIX_COMMIT} (#9000)", + f"{FIX_COMMIT} (#69)", + f"{FIX_COMMIT} (#42)", + ] + ) + content = changelog.format( + ChangeLogTypeEnum.changelog_file, + FormatTypeEnum.markdown, + is_pre_release=False, + ) + assert ( + content + == f"""## Fixes: + +- {FIX_COMMIT[5:]} (#42, #69, #9000) +- Does not matter (#9999)""" + ) + + @pytest.mark.parametrize( "fix_commit", (