From 40a4ee2574d0608f330b8e4df2c09c93ef588c0e Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Wed, 25 Sep 2024 09:36:32 +0200 Subject: [PATCH 01/51] #55 - Chapter line formatting - authors - Add limitation to avoid of usage black to tests. - Removed workflow copying Release notes from PR to Issue as no more supported. - Updated README.md to show example of row formatting as build-in feature and provide list of supported keywords. --- .../release_notes_comments_migration.yml | 110 ------------------ README.md | 15 ++- action.yml | 4 +- pyproject.toml | 1 + release_notes_generator/action_inputs.py | 2 +- release_notes_generator/model/record.py | 2 +- .../test_release_notes_builder.py | 6 +- 7 files changed, 22 insertions(+), 118 deletions(-) delete mode 100644 .github/workflows/release_notes_comments_migration.yml diff --git a/.github/workflows/release_notes_comments_migration.yml b/.github/workflows/release_notes_comments_migration.yml deleted file mode 100644 index 61103777..00000000 --- a/.github/workflows/release_notes_comments_migration.yml +++ /dev/null @@ -1,110 +0,0 @@ -# -# Copyright 2023 ABSA Group Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -name: Copy Release Notes to Related Issues - -on: - pull_request: - types: [closed] - branches: [ master ] - -jobs: - copy_release_notes: - if: github.event.pull_request.merged == true - runs-on: ubuntu-latest - steps: - - name: Fetch PR Comments - id: get-comments - uses: actions/github-script@v7 - with: - github-token: ${{secrets.GITHUB_TOKEN}} - script: | - const prNumber = context.payload.pull_request.number; - const repoName = context.repo.repo; - const repoOwner = context.repo.owner; - const releaseNotesRegex = /release notes/i; - - const comments = await github.rest.issues.listComments({ - owner: repoOwner, - repo: repoName, - issue_number: prNumber, - }); - - const releaseNoteComment = comments.data.find(comment => releaseNotesRegex.test(comment.body)); - const releaseNoteBody = releaseNoteComment ? releaseNoteComment.body : ''; - console.log(`Release Note Body: ${releaseNoteBody}`); - core.setOutput('releaseNoteBody', releaseNoteBody); - - - name: Print Extracted releaseNoteBody - run: | - echo "Extracted Release Note Body:" - echo "${{ steps.get-comments.outputs.releaseNoteBody }}" - echo "RELEASE_NOTE_BODY<> $GITHUB_ENV - echo "${{ steps.get-comments.outputs.releaseNoteBody }}" >> $GITHUB_ENV - echo "EOF" >> $GITHUB_ENV - - - name: Parse PR Description for Related Issues - id: find-issues - uses: actions/github-script@v7 - with: - github-token: ${{secrets.GITHUB_TOKEN}} - script: | - const description = context.payload.pull_request.body; - const issueNumbers = []; - const regexPattern = /([Cc]los(e|es|ed)|[Ff]ix(es|ed)?|[Rr]esolv(e|es|ed))\s*#\s*([0-9]+)/g; - - let match; - while ((match = regexPattern.exec(description)) !== null) { - // This is necessary to avoid infinite loops with zero-width matches - if (match.index === regexPattern.lastIndex) { - regexPattern.lastIndex++; - } - - // The actual issue number is in the last group of the match - const issueNumber = match[match.length - 1]; - if (issueNumber) { - issueNumbers.push(issueNumber); - } - } - - core.setOutput('issueNumbers', issueNumbers.join(', ')); - - - name: Print Extracted Issue Numbers - run: | - echo "Extracted Issue Numbers: ${{ steps.find-issues.outputs.issueNumbers }}" - echo "ISSUE_NUMBERS=${{ steps.find-issues.outputs.issueNumbers }}" >> $GITHUB_ENV - - - name: Post Comment to Issues - if: ${{ steps.get-comments.outputs.releaseNoteBody }} && ${{ steps.find-issues.outputs.issueNumbers }} - uses: actions/github-script@v7 - with: - github-token: ${{secrets.GITHUB_TOKEN}} - script: | - const issueNumbers = process.env.ISSUE_NUMBERS; - const commentBody = process.env.RELEASE_NOTE_BODY; - const repoName = context.repo.repo; - const repoOwner = context.repo.owner; - - for (const issueNumber of issueNumbers.split(', ')) { - if (issueNumber && commentBody) { - await github.rest.issues.createComment({ - owner: repoOwner, - repo: repoName, - issue_number: issueNumber, - body: commentBody - }); - } - } diff --git a/README.md b/README.md index 8f943f89..964c2e71 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ Generate Release Notes action is dedicated to enhance the quality and organizati ### `row-format-issue` - **Description**: The format of the row for the issue in the release notes. The format can contain placeholders for the issue `number`, `title`, and issues `pull-requests`. The placeholders are case-sensitive. - **Required**: No -- **Default**: `#{number} _{title}_ in {pull-requests}"` +- **Default**: `#{number} _{title}_ {pull-requests}"` ### `row-format-pr` - **Description**: The format of the row for the PR in the release notes. The format can contain placeholders for the PR `number`, `title`, and PR `pull-requests`. The placeholders are case-sensitive. @@ -166,6 +166,9 @@ Add the following step to your GitHub workflow (in example are used non-default warnings: false print-empty-chapters: false chapters-to-pr-without-issue: false + row-format-issue: '#{number} _{title}_ {pull-requests}"' + row-format-pr: '#{number} _{title}_"' + row-format-link-pr: true ``` ## Features @@ -197,6 +200,16 @@ If an issue is linked to multiple PRs, the action fetches and aggregates contrib #### No Release Notes Found If no valid "Release Notes" comment is found in an issue, it will be marked accordingly. This helps maintainers quickly identify which issues need attention for documentation. +#### Row formatting +Format of the row for the issue and PR in the release notes can be customized. The placeholders are case-sensitive. + +**Supported row format keywords:** +- `{number}`: Issue or PR number. +- `{title}`: Issue or PR title. +- `{pull-requests}`: List of PRs linked to the issue. Adds a list of PRs linked to the issue in the row with `in` prefix: + - `#{number} _{title}_ {pull-requests}` => "[#43]() _title_ in [#PR1](), [#PR2](), [#PR3]()" + - Not used in PR row format. See default value. + ### Select start date for closed issues and PRs By set **published-at** to true the action will use the `published-at` timestamp of the latest release as the reference point for searching closed issues and PRs, instead of the `created-at` date. If first release, repository creation date is used. diff --git a/action.yml b/action.yml index 45a06a21..310abd6e 100644 --- a/action.yml +++ b/action.yml @@ -58,11 +58,11 @@ inputs: row-format-issue: description: 'Format of the issue row in the release notes. Available placeholders: {link}, {title}, {pull-requests}. Placeholders are case-insensitive.' required: false - default: '#{number} _{title}_ in {pull-requests}' + default: '#{number} _{title}_ {pull-requests} {authors}' row-format-pr: description: 'Format of the pr row in the release notes. Available placeholders: {link}, {title}, {pull-requests}. Placeholders are case-insensitive.' required: false - default: '#{number} _{title}_' + default: '#{number} _{title}_ {authors}' row-format-link-pr: description: 'Add prefix "PR:" before link to PR when not linked an Issue.' required: false diff --git a/pyproject.toml b/pyproject.toml index 474d0aff..68eb39c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,4 @@ [tool.black] line-length = 120 target-version = ['py311'] +force-exclude = '''test''' diff --git a/release_notes_generator/action_inputs.py b/release_notes_generator/action_inputs.py index 519b689d..9868561d 100644 --- a/release_notes_generator/action_inputs.py +++ b/release_notes_generator/action_inputs.py @@ -167,7 +167,7 @@ def get_row_format_issue() -> str: """ Get the issue row format for the release notes. """ - return get_action_input(ROW_FORMAT_ISSUE, "#{number} _{title}_ in {pull-requests}").strip() + return get_action_input(ROW_FORMAT_ISSUE, "#{number} _{title}_ {pull-requests}").strip() @staticmethod def get_row_format_pr() -> str: diff --git a/release_notes_generator/model/record.py b/release_notes_generator/model/record.py index c14f4b90..9a50e6f4 100644 --- a/release_notes_generator/model/record.py +++ b/release_notes_generator/model/record.py @@ -287,7 +287,7 @@ def to_chapter_row(self) -> str: else: format_values["number"] = self.__gh_issue.number format_values["title"] = self.__gh_issue.title - format_values["pull-requests"] = self.pr_links if len(self.__pulls) > 0 else "" + format_values["pull-requests"] = f"in {self.pr_links}" if len(self.__pulls) > 0 else "" format_values["authors"] = self.authors if self.authors is not None else "" format_values["contributors"] = self.contributors if self.contributors is not None else "" diff --git a/tests/release_notes/test_release_notes_builder.py b/tests/release_notes/test_release_notes_builder.py index 9002b719..599617d6 100644 --- a/tests/release_notes/test_release_notes_builder.py +++ b/tests/release_notes/test_release_notes_builder.py @@ -173,10 +173,10 @@ def __init__(self, name): """ RELEASE_NOTES_DATA_SERVICE_CHAPTERS_CLOSED_ISSUE_NO_PR_NO_USER_LABELS = """### Closed Issues without Pull Request ⚠️ -- #121 _Fix the bug_ in +- #121 _Fix the bug_ ### Closed Issues without User Defined Labels ⚠️ -- 🔔 #121 _Fix the bug_ in +- 🔔 #121 _Fix the bug_ #### Full Changelog http://example.com/changelog @@ -224,7 +224,7 @@ def __init__(self, name): """ RELEASE_NOTES_DATA_CLOSED_ISSUE_NO_PR_WITH_USER_LABELS = """### Closed Issues without Pull Request ⚠️ -- #121 _Fix the bug_ in +- #121 _Fix the bug_ #### Full Changelog http://example.com/changelog From ec9eb91ade6168904ac3badceb2748b681080329 Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Mon, 30 Sep 2024 16:18:38 +0200 Subject: [PATCH 02/51] - Introduced logic and example adding prefix strings as part of keyword. - Fixed structure of test folder. --- README.md | 32 ++++++++---- action.yml | 8 +-- release_notes_generator/action_inputs.py | 28 +++++----- release_notes_generator/model/record.py | 52 ++++++++++++------- release_notes_generator/utils/constants.py | 2 +- .../utils/github_rate_limiter.py | 2 + .../__init__.py | 0 .../model/__init__.py | 0 .../model/test_base_chapters.py | 0 .../model/test_chapter.py | 0 .../model/test_custom_chapters.py | 0 .../model/test_record.py | 2 +- .../model/test_service_chapters.py | 0 .../record/__init__.py | 0 .../record}/test_record_factory.py | 0 .../test_action_inputs.py | 0 .../test_release_notes_builder.py | 0 .../test_release_notes_generator.py | 0 .../utils/__init__.py | 0 .../utils/test_decorators.py | 4 +- .../utils/test_gh_action.py | 0 .../utils/test_github_rate_limiter.py | 0 .../utils/test_logging_config.py | 0 .../utils/test_pull_reuqest_utils.py | 0 .../utils/test_utils.py | 0 25 files changed, 79 insertions(+), 51 deletions(-) rename tests/{release_notes => release_notes_generator}/__init__.py (100%) rename tests/{release_notes => release_notes_generator}/model/__init__.py (100%) rename tests/{release_notes => release_notes_generator}/model/test_base_chapters.py (100%) rename tests/{release_notes => release_notes_generator}/model/test_chapter.py (100%) rename tests/{release_notes => release_notes_generator}/model/test_custom_chapters.py (100%) rename tests/{release_notes => release_notes_generator}/model/test_record.py (99%) rename tests/{release_notes => release_notes_generator}/model/test_service_chapters.py (100%) create mode 100644 tests/release_notes_generator/record/__init__.py rename tests/{release_notes => release_notes_generator/record}/test_record_factory.py (100%) rename tests/{ => release_notes_generator}/test_action_inputs.py (100%) rename tests/{release_notes => release_notes_generator}/test_release_notes_builder.py (100%) rename tests/{ => release_notes_generator}/test_release_notes_generator.py (100%) rename tests/{ => release_notes_generator}/utils/__init__.py (100%) rename tests/{ => release_notes_generator}/utils/test_decorators.py (88%) rename tests/{ => release_notes_generator}/utils/test_gh_action.py (100%) rename tests/{ => release_notes_generator}/utils/test_github_rate_limiter.py (100%) rename tests/{ => release_notes_generator}/utils/test_logging_config.py (100%) rename tests/{ => release_notes_generator}/utils/test_pull_reuqest_utils.py (100%) rename tests/{ => release_notes_generator}/utils/test_utils.py (100%) diff --git a/README.md b/README.md index 964c2e71..360d6620 100644 --- a/README.md +++ b/README.md @@ -54,12 +54,12 @@ Generate Release Notes action is dedicated to enhance the quality and organizati ### `row-format-issue` - **Description**: The format of the row for the issue in the release notes. The format can contain placeholders for the issue `number`, `title`, and issues `pull-requests`. The placeholders are case-sensitive. - **Required**: No -- **Default**: `#{number} _{title}_ {pull-requests}"` +- **Default**: `#{number} _{title}_ {pull-requests} {assignee} {implemented-by} {contributed-by}"` ### `row-format-pr` - **Description**: The format of the row for the PR in the release notes. The format can contain placeholders for the PR `number`, `title`, and PR `pull-requests`. The placeholders are case-sensitive. - **Required**: No -- **Default**: `#{number} _{title}_"` +- **Default**: `#{number} _{title}_ {assignee} {implemented-by} {contributed-by}` ### `row-format-link-pr` - **Description**: If defined `true`, the PR row will begin with a `"PR: "` string. Otherwise, no prefix will be added. @@ -166,8 +166,8 @@ Add the following step to your GitHub workflow (in example are used non-default warnings: false print-empty-chapters: false chapters-to-pr-without-issue: false - row-format-issue: '#{number} _{title}_ {pull-requests}"' - row-format-pr: '#{number} _{title}_"' + row-format-issue: '#{number} _{title}_ {pull-requests} {assignee} {developed-by} {co-authored-by}' + row-format-pr: '#{number} _{title}_ {assignee} {developed-by} {co-authored-by}' row-format-link-pr: true ``` @@ -181,8 +181,8 @@ This action requires that your GitHub issues include comments with specific rele - The action scans through comments on each closed issue since the last release. It identifies comments that follow the specified format and extracts the content as part of the release notes. - The time considered for the previous release is based on its creation time. This means that the action will look for issues closed after the creation time of the most recent release to ensure that all relevant updates since that release are included. -**Comment Format** -- For an issue's contributions to be included in the release notes, it must contain a comment starting with "Release Notes" followed by the note content. This comment is typically added by the contributors. +**Release Notes Comment Format** +- It must contain a comment starting with "Release Notes" followed by the note content. This comment is typically added by the contributors. - Here is an example of the content for a 'Release Notes' string, which is not case-sensitive: ``` Release Notes @@ -191,16 +191,13 @@ Release Notes - Using `-` as a bullet point for each note is the best practice. The Markdown parser will automatically convert it to a list. - These comments are not required for action functionality. If an issue does not contain a "Release Notes" comment, it will be marked accordingly in the release notes. This helps maintainers quickly identify which issues need attention for documentation. -#### Contributors Mention -Along with the release note content, the action also gathers a list of contributors for each issue. This includes issue assignees and authors of linked pull requests' commits, providing acknowledgment for their contributions in the release notes. - #### Handling Multiple PRs If an issue is linked to multiple PRs, the action fetches and aggregates contributions from all linked PRs. #### No Release Notes Found If no valid "Release Notes" comment is found in an issue, it will be marked accordingly. This helps maintainers quickly identify which issues need attention for documentation. -#### Row formatting +#### Issue or PR Row formatting Format of the row for the issue and PR in the release notes can be customized. The placeholders are case-sensitive. **Supported row format keywords:** @@ -208,7 +205,20 @@ Format of the row for the issue and PR in the release notes can be customized. T - `{title}`: Issue or PR title. - `{pull-requests}`: List of PRs linked to the issue. Adds a list of PRs linked to the issue in the row with `in` prefix: - `#{number} _{title}_ {pull-requests}` => "[#43]() _title_ in [#PR1](), [#PR2](), [#PR3]()" - - Not used in PR row format. See default value. + - Not used in PR row format. See default value. +- `{assingee}`: Issue or PR assignee. Adds a login of assignees in the row with `assigned to` prefix: + - `#{number} _{title}_ {assignee}` => "[#43]() _title_ implemented by @login1" + - TODO - mention problem with public and private email +- `{assignees}`: Issue or PR assignees. Adds a list of assignees logins in the row with `assigned to` prefix: + - `#{number} _{title}_ {assignees}` => "[#43]() _title_ implemented by @login1, @login2" + - This is alternative representation of multiple assignees provided by GitHub. +- `{developed-by}`: List of PR developer(s) login(s). Adds a login of commit authors for PR(s) in the row with `developed by` prefix: + - `#{number} _{title}_ {developed-by}` => "[#43]() _title_ developed by @login1" + - TODO - mention problem with public and private email +- `{co-authored-by}`: List of PR contributors. Adds a login of contributors in the row with `co-authored by` prefix: + - `#{number} _{title}_ {co-authored-by}` => "[#43]() _title_ co-authored by @login1 @login2" + - Contribution is detected in PR commit messages by detection of GitHub supported trailer `Co-authored-by`. + - TODO - mention problem with public and private email ### Select start date for closed issues and PRs By set **published-at** to true the action will use the `published-at` timestamp of the latest release as the reference point for searching closed issues and PRs, instead of the `created-at` date. If first release, repository creation date is used. diff --git a/action.yml b/action.yml index aa2b193b..22b0d6e0 100644 --- a/action.yml +++ b/action.yml @@ -56,13 +56,13 @@ inputs: required: false default: 'false' row-format-issue: - description: 'Format of the issue row in the release notes. Available placeholders: {link}, {title}, {pull-requests}. Placeholders are case-insensitive.' + description: 'Format of the issue row in the release notes. Available placeholders: {link}, {title}, {pull-requests}, {assignee}, {assignees}, {developed-by}, {co-authored-by}. Placeholders are case-insensitive.' required: false - default: '#{number} _{title}_ {pull-requests} {authors}' + default: '#{number} _{title}_ {pull-requests} {assignee} {developed-by} {co-authored-by}' row-format-pr: - description: 'Format of the pr row in the release notes. Available placeholders: {link}, {title}, {pull-requests}. Placeholders are case-insensitive.' + description: 'Format of the pr row in the release notes. Available placeholders: {link}, {title}, {pull-requests}, {assignee}, {assignees}, {developed-by}, {co-authored-by}. Placeholders are case-insensitive.' required: false - default: '#{number} _{title}_ {authors}' + default: '#{number} _{title}_ {assignee} {developed-by} {co-authored-by}' row-format-link-pr: description: 'Add prefix "PR:" before link to PR when not linked an Issue.' required: false diff --git a/release_notes_generator/action_inputs.py b/release_notes_generator/action_inputs.py index 9868561d..040e773f 100644 --- a/release_notes_generator/action_inputs.py +++ b/release_notes_generator/action_inputs.py @@ -167,14 +167,14 @@ def get_row_format_issue() -> str: """ Get the issue row format for the release notes. """ - return get_action_input(ROW_FORMAT_ISSUE, "#{number} _{title}_ {pull-requests}").strip() + return get_action_input(ROW_FORMAT_ISSUE, "#{number} _{title}_ {pull-requests} {assignee} {developed-by} {contributed-by}").strip() @staticmethod def get_row_format_pr() -> str: """ Get the pr row format for the release notes. """ - return get_action_input(ROW_FORMAT_PR, "#{number} _{title}_").strip() + return get_action_input(ROW_FORMAT_PR, "#{number} _{title}_ {assignee} {developed-by} {contributed-by}").strip() @staticmethod def get_row_format_link_pr() -> bool: @@ -227,18 +227,6 @@ def validate_inputs(): verbose = ActionInputs.get_verbose() ActionInputs.validate_input(verbose, bool, "Verbose logging must be a boolean.", errors) - row_format_issue = ActionInputs.get_row_format_issue() - if not isinstance(row_format_issue, str) or not row_format_issue.strip(): - errors.append("Issue row format must be a non-empty string.") - - errors.extend(detect_row_format_invalid_keywords(row_format_issue)) - - row_format_pr = ActionInputs.get_row_format_pr() - if not isinstance(row_format_pr, str) or not row_format_pr.strip(): - errors.append("PR Row format must be a non-empty string.") - - errors.extend(detect_row_format_invalid_keywords(row_format_pr, row_type="PR")) - row_format_link_pr = ActionInputs.get_row_format_link_pr() ActionInputs.validate_input(row_format_link_pr, bool, "'row-format-link-pr' value must be a boolean.", errors) @@ -251,6 +239,18 @@ def validate_inputs(): chapters_to_pr_without_issue, bool, "Chapters to PR without issue must be a boolean.", errors ) + row_format_issue = ActionInputs.get_row_format_issue() + if not isinstance(row_format_issue, str) or not row_format_issue.strip(): + errors.append("Issue row format must be a non-empty string.") + + errors.extend(detect_row_format_invalid_keywords(row_format_issue)) + + row_format_pr = ActionInputs.get_row_format_pr() + if not isinstance(row_format_pr, str) or not row_format_pr.strip(): + errors.append("PR Row format must be a non-empty string.") + + errors.extend(detect_row_format_invalid_keywords(row_format_pr, row_type="PR")) + # Log errors if any if errors: for error in errors: diff --git a/release_notes_generator/model/record.py b/release_notes_generator/model/record.py index 9a50e6f4..622ccc93 100644 --- a/release_notes_generator/model/record.py +++ b/release_notes_generator/model/record.py @@ -25,6 +25,7 @@ from github.PullRequest import PullRequest from github.Repository import Repository from github.Commit import Commit +from torch.nn.functional import selu_ from release_notes_generator.action_inputs import ActionInputs from release_notes_generator.utils.constants import ( @@ -179,26 +180,42 @@ def pr_contains_issue_mentions(self) -> bool: """Checks if the pull request contains issue mentions.""" return len(extract_issue_numbers_from_body(self.__pulls[0])) > 0 + # Note: assignee & assignees are related to this GitHub Docs - https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/assigning-issues-and-pull-requests-to-other-github-users#about-issue-and-pull-request-assignees @property - def authors(self) -> Optional[str]: + def assignee(self) -> Optional[str]: """Getter for the authors of the record.""" + if self.__gh_issue is None: + return self.pulls[0].assignee.login if self.pulls[0].assignee is not None else None + + else: + return self.issue.assignee.login if self.issue.assignee is not None else None + + @property + def assignees(self) -> Optional[str]: + """Getter for the assignees of the record.""" + if self.__gh_issue is None: + logins = [a.login for a in self.pulls[0].assignees] + return ", ".join(logins) if len(logins) > 0 else None + + else: + logins = [a.login for a in self.issue.assignees] + return ", ".join(logins) if len(logins) > 0 else None + + @property + def developers(self) -> Optional[str]: + """Getter for the developers of the record.""" + # TODO - cycle across record commits and collect PR "authors" + # Commit.author - by mel byt ten, kdo ten PR pripravil, commiter je ten kdo s nim naposledy pracoval + return None - # TODO in Issue named 'Chapter line formatting - authors' - # authors: list[str] = [] - # - # for pull in self.__pulls: - # if pull.author is not None: - # authors.append(f"@{pull.author}") - # - # if len(authors) > 0: - # return None - # - # res = ", ".join(authors) - # return res @property def contributors(self) -> Optional[str]: """Getter for the contributors of the record.""" + # TODO - cycle across record commits and check if contribution string is present in commit message + # + # Co-authored-by + # check if extra API calls are here - 100% jsou tam - udelat test na generovani s spoptrebe API + varovani do README return None @property @@ -272,14 +289,15 @@ def to_chapter_row(self) -> str: """ self.increment_present_in_chapters() row_prefix = f"{ActionInputs.get_duplicity_icon()} " if self.present_in_chapters() > 1 else "" - format_values = {} + format_values = {"assignee": f"assigned to @{self.assignee}" if self.assignee is not None else "", + "assignees": f"assigned to @{self.assignees}" if self.assignees is not None else "", + "developers": f"developed by {self.developers}" if self.developers is not None else "", + "contributors": f"co-authored by {self.contributors}" if self.contributors is not None else ""} if self.__gh_issue is None: p = self.__pulls[0] format_values["number"] = p.number format_values["title"] = p.title - format_values["authors"] = self.authors if self.authors is not None else "" - format_values["contributors"] = self.contributors if self.contributors is not None else "" pr_prefix = "PR: " if ActionInputs.get_row_format_link_pr() else "" row = f"{row_prefix}{pr_prefix}" + ActionInputs.get_row_format_pr().format(**format_values) @@ -288,8 +306,6 @@ def to_chapter_row(self) -> str: format_values["number"] = self.__gh_issue.number format_values["title"] = self.__gh_issue.title format_values["pull-requests"] = f"in {self.pr_links}" if len(self.__pulls) > 0 else "" - format_values["authors"] = self.authors if self.authors is not None else "" - format_values["contributors"] = self.contributors if self.contributors is not None else "" row = f"{row_prefix}" + ActionInputs.get_row_format_issue().format(**format_values) diff --git a/release_notes_generator/utils/constants.py b/release_notes_generator/utils/constants.py index 5d2bcdef..6c8b8455 100644 --- a/release_notes_generator/utils/constants.py +++ b/release_notes_generator/utils/constants.py @@ -32,7 +32,7 @@ ROW_FORMAT_ISSUE = "row-format-issue" ROW_FORMAT_PR = "row-format-pr" ROW_FORMAT_LINK_PR = "row-format-link-pr" -SUPPORTED_ROW_FORMAT_KEYS = ["number", "title", "pull-requests"] +SUPPORTED_ROW_FORMAT_KEYS = ["number", "title", "pull-requests", "assignee", "assignees", "developed-by", "contributed-by"] # Features WARNINGS = "warnings" diff --git a/release_notes_generator/utils/github_rate_limiter.py b/release_notes_generator/utils/github_rate_limiter.py index 9d7dc67b..c0b07c7b 100644 --- a/release_notes_generator/utils/github_rate_limiter.py +++ b/release_notes_generator/utils/github_rate_limiter.py @@ -49,6 +49,8 @@ def wrapped_method(*args, **kwargs) -> Optional: remaining_calls = self.github_client.get_rate_limit().core.remaining reset_time = self.github_client.get_rate_limit().core.reset.timestamp() + logger.debug("Remaining calls: %s", remaining_calls) + if remaining_calls < 5: logger.info("Rate limit almost reached. Sleeping until reset time.") sleep_time = reset_time - (now := time.time()) diff --git a/tests/release_notes/__init__.py b/tests/release_notes_generator/__init__.py similarity index 100% rename from tests/release_notes/__init__.py rename to tests/release_notes_generator/__init__.py diff --git a/tests/release_notes/model/__init__.py b/tests/release_notes_generator/model/__init__.py similarity index 100% rename from tests/release_notes/model/__init__.py rename to tests/release_notes_generator/model/__init__.py diff --git a/tests/release_notes/model/test_base_chapters.py b/tests/release_notes_generator/model/test_base_chapters.py similarity index 100% rename from tests/release_notes/model/test_base_chapters.py rename to tests/release_notes_generator/model/test_base_chapters.py diff --git a/tests/release_notes/model/test_chapter.py b/tests/release_notes_generator/model/test_chapter.py similarity index 100% rename from tests/release_notes/model/test_chapter.py rename to tests/release_notes_generator/model/test_chapter.py diff --git a/tests/release_notes/model/test_custom_chapters.py b/tests/release_notes_generator/model/test_custom_chapters.py similarity index 100% rename from tests/release_notes/model/test_custom_chapters.py rename to tests/release_notes_generator/model/test_custom_chapters.py diff --git a/tests/release_notes/model/test_record.py b/tests/release_notes_generator/model/test_record.py similarity index 99% rename from tests/release_notes/model/test_record.py rename to tests/release_notes_generator/model/test_record.py index 7c46fcc3..a3bad912 100644 --- a/tests/release_notes/model/test_record.py +++ b/tests/release_notes_generator/model/test_record.py @@ -58,7 +58,7 @@ def test_record_properties_with_pull(mock_pull_closed, record_with_no_issue_one_ # authors & contributors - not supported now by code def test_record_properties_authors_contributors(record_with_no_issue_one_pull_closed): - assert record_with_no_issue_one_pull_closed.authors is None + assert record_with_no_issue_one_pull_closed.assignee is None assert record_with_no_issue_one_pull_closed.contributors is None diff --git a/tests/release_notes/model/test_service_chapters.py b/tests/release_notes_generator/model/test_service_chapters.py similarity index 100% rename from tests/release_notes/model/test_service_chapters.py rename to tests/release_notes_generator/model/test_service_chapters.py diff --git a/tests/release_notes_generator/record/__init__.py b/tests/release_notes_generator/record/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/release_notes/test_record_factory.py b/tests/release_notes_generator/record/test_record_factory.py similarity index 100% rename from tests/release_notes/test_record_factory.py rename to tests/release_notes_generator/record/test_record_factory.py diff --git a/tests/test_action_inputs.py b/tests/release_notes_generator/test_action_inputs.py similarity index 100% rename from tests/test_action_inputs.py rename to tests/release_notes_generator/test_action_inputs.py diff --git a/tests/release_notes/test_release_notes_builder.py b/tests/release_notes_generator/test_release_notes_builder.py similarity index 100% rename from tests/release_notes/test_release_notes_builder.py rename to tests/release_notes_generator/test_release_notes_builder.py diff --git a/tests/test_release_notes_generator.py b/tests/release_notes_generator/test_release_notes_generator.py similarity index 100% rename from tests/test_release_notes_generator.py rename to tests/release_notes_generator/test_release_notes_generator.py diff --git a/tests/utils/__init__.py b/tests/release_notes_generator/utils/__init__.py similarity index 100% rename from tests/utils/__init__.py rename to tests/release_notes_generator/utils/__init__.py diff --git a/tests/utils/test_decorators.py b/tests/release_notes_generator/utils/test_decorators.py similarity index 88% rename from tests/utils/test_decorators.py rename to tests/release_notes_generator/utils/test_decorators.py index a2f124e1..0e6f60c7 100644 --- a/tests/utils/test_decorators.py +++ b/tests/release_notes_generator/utils/test_decorators.py @@ -27,7 +27,7 @@ def sample_function(x, y): def test_debug_log_decorator(mocker): # Mock logging - mock_log_debug = mocker.patch("release_notes_generator.utils.decorators.logger.debug") + mock_log_debug = mocker.patch("release_notes_generator.release_notes_generator.utils.decorators.logger.debug") decorated_function = debug_log_decorator(sample_function) expected_call = [ @@ -54,7 +54,7 @@ def sample_method(x, y): def test_safe_call_decorator_exception(rate_limiter, mocker): - mock_log_error = mocker.patch("release_notes_generator.utils.decorators.logger.error") + mock_log_error = mocker.patch("release_notes_generator.release_notes_generator.utils.decorators.logger.error") @safe_call_decorator(rate_limiter) def sample_method(x, y): diff --git a/tests/utils/test_gh_action.py b/tests/release_notes_generator/utils/test_gh_action.py similarity index 100% rename from tests/utils/test_gh_action.py rename to tests/release_notes_generator/utils/test_gh_action.py diff --git a/tests/utils/test_github_rate_limiter.py b/tests/release_notes_generator/utils/test_github_rate_limiter.py similarity index 100% rename from tests/utils/test_github_rate_limiter.py rename to tests/release_notes_generator/utils/test_github_rate_limiter.py diff --git a/tests/utils/test_logging_config.py b/tests/release_notes_generator/utils/test_logging_config.py similarity index 100% rename from tests/utils/test_logging_config.py rename to tests/release_notes_generator/utils/test_logging_config.py diff --git a/tests/utils/test_pull_reuqest_utils.py b/tests/release_notes_generator/utils/test_pull_reuqest_utils.py similarity index 100% rename from tests/utils/test_pull_reuqest_utils.py rename to tests/release_notes_generator/utils/test_pull_reuqest_utils.py diff --git a/tests/utils/test_utils.py b/tests/release_notes_generator/utils/test_utils.py similarity index 100% rename from tests/utils/test_utils.py rename to tests/release_notes_generator/utils/test_utils.py From 17bfaf78ba4b44fda29ede005d5122eef613f091 Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 1 Oct 2024 08:51:23 +0200 Subject: [PATCH 03/51] - Fixed wrong keywords. Debug. --- release_notes_generator/action_inputs.py | 4 +-- release_notes_generator/model/record.py | 29 +++++++++++++++++++--- release_notes_generator/utils/constants.py | 2 +- release_notes_generator/utils/utils.py | 2 +- 4 files changed, 29 insertions(+), 8 deletions(-) diff --git a/release_notes_generator/action_inputs.py b/release_notes_generator/action_inputs.py index 040e773f..af349834 100644 --- a/release_notes_generator/action_inputs.py +++ b/release_notes_generator/action_inputs.py @@ -167,14 +167,14 @@ def get_row_format_issue() -> str: """ Get the issue row format for the release notes. """ - return get_action_input(ROW_FORMAT_ISSUE, "#{number} _{title}_ {pull-requests} {assignee} {developed-by} {contributed-by}").strip() + return get_action_input(ROW_FORMAT_ISSUE, "#{number} _{title}_ {pull-requests} {assignee} {developed-by} {co-authored-by}").strip() @staticmethod def get_row_format_pr() -> str: """ Get the pr row format for the release notes. """ - return get_action_input(ROW_FORMAT_PR, "#{number} _{title}_ {assignee} {developed-by} {contributed-by}").strip() + return get_action_input(ROW_FORMAT_PR, "#{number} _{title}_ {assignee} {developed-by} {co-authored-by}").strip() @staticmethod def get_row_format_link_pr() -> bool: diff --git a/release_notes_generator/model/record.py b/release_notes_generator/model/record.py index 622ccc93..d0b71cdb 100644 --- a/release_notes_generator/model/record.py +++ b/release_notes_generator/model/record.py @@ -289,15 +289,13 @@ def to_chapter_row(self) -> str: """ self.increment_present_in_chapters() row_prefix = f"{ActionInputs.get_duplicity_icon()} " if self.present_in_chapters() > 1 else "" - format_values = {"assignee": f"assigned to @{self.assignee}" if self.assignee is not None else "", - "assignees": f"assigned to @{self.assignees}" if self.assignees is not None else "", - "developers": f"developed by {self.developers}" if self.developers is not None else "", - "contributors": f"co-authored by {self.contributors}" if self.contributors is not None else ""} + format_values = {} if self.__gh_issue is None: p = self.__pulls[0] format_values["number"] = p.number format_values["title"] = p.title + format_values.update(self.__get_row_format_values(ActionInputs.get_row_format_pr())) pr_prefix = "PR: " if ActionInputs.get_row_format_link_pr() else "" row = f"{row_prefix}{pr_prefix}" + ActionInputs.get_row_format_pr().format(**format_values) @@ -306,6 +304,7 @@ def to_chapter_row(self) -> str: format_values["number"] = self.__gh_issue.number format_values["title"] = self.__gh_issue.title format_values["pull-requests"] = f"in {self.pr_links}" if len(self.__pulls) > 0 else "" + format_values.update(self.__get_row_format_values(ActionInputs.get_row_format_issue())) row = f"{row_prefix}" + ActionInputs.get_row_format_issue().format(**format_values) @@ -314,6 +313,28 @@ def to_chapter_row(self) -> str: return row + def __get_row_format_values(self, row_format: str) -> dict: + """ + Create dictionary and fill by user row format defined values. + NoteL some values are API call intensive. + + @param row_format: User defined row format. + @return: The dictionary with supported values required by user row format. + """ + format_values = {} + + if "{assignee}" in row_format: + format_values["assignee"] = f"assigned to @{self.assignee}" if self.assignee is not None else "" + if "{assignees}" in row_format: + format_values["assignees"] = f"assigned to @{self.assignees}" if self.assignees is not None else "" + if "{developers}" in row_format: + format_values["developers"] = f"developed by {self.developers}" if self.developers is not None else "" + if "{contributors}" in row_format: + format_values["contributors"] = f"co-authored by {self.contributors}" if self.contributors is not None else "" + + return format_values + + def contains_min_one_label(self, labels: list[str]) -> bool: """ Check if the record contains at least one of the specified labels. diff --git a/release_notes_generator/utils/constants.py b/release_notes_generator/utils/constants.py index 6c8b8455..0824188a 100644 --- a/release_notes_generator/utils/constants.py +++ b/release_notes_generator/utils/constants.py @@ -32,7 +32,7 @@ ROW_FORMAT_ISSUE = "row-format-issue" ROW_FORMAT_PR = "row-format-pr" ROW_FORMAT_LINK_PR = "row-format-link-pr" -SUPPORTED_ROW_FORMAT_KEYS = ["number", "title", "pull-requests", "assignee", "assignees", "developed-by", "contributed-by"] +SUPPORTED_ROW_FORMAT_KEYS = ["number", "title", "pull-requests", "assignee", "assignees", "developed-by", "co-authored-by"] # Features WARNINGS = "warnings" diff --git a/release_notes_generator/utils/utils.py b/release_notes_generator/utils/utils.py index f1a38695..fe10dc9d 100644 --- a/release_notes_generator/utils/utils.py +++ b/release_notes_generator/utils/utils.py @@ -71,5 +71,5 @@ def detect_row_format_invalid_keywords(row_format: str, row_type: str = "Issue") keywords_in_braces = re.findall(r"\{(.*?)\}", row_format) invalid_keywords = [keyword for keyword in keywords_in_braces if keyword not in SUPPORTED_ROW_FORMAT_KEYS] if invalid_keywords: - errors.append(f"Invalid {row_type} row format keyword(s) found: {', '.join(invalid_keywords)}") + errors.append(f"Invalid {row_type} row format '{row_format}'. Invalid keyword(s) found: {', '.join(invalid_keywords)}") return errors From 78850406e83e51fdee28491aba182fe4ce8bf2b1 Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 1 Oct 2024 08:53:41 +0200 Subject: [PATCH 04/51] - Remove unexpected import. --- release_notes_generator/model/record.py | 1 - 1 file changed, 1 deletion(-) diff --git a/release_notes_generator/model/record.py b/release_notes_generator/model/record.py index d0b71cdb..cbfcde53 100644 --- a/release_notes_generator/model/record.py +++ b/release_notes_generator/model/record.py @@ -25,7 +25,6 @@ from github.PullRequest import PullRequest from github.Repository import Repository from github.Commit import Commit -from torch.nn.functional import selu_ from release_notes_generator.action_inputs import ActionInputs from release_notes_generator.utils.constants import ( From f8e90f8f09d8680db351a19b3f823314e6078316 Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 1 Oct 2024 08:58:51 +0200 Subject: [PATCH 05/51] - Fixes from manual test. --- release_notes_generator/model/record.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/release_notes_generator/model/record.py b/release_notes_generator/model/record.py index cbfcde53..cf8dedba 100644 --- a/release_notes_generator/model/record.py +++ b/release_notes_generator/model/record.py @@ -326,10 +326,10 @@ def __get_row_format_values(self, row_format: str) -> dict: format_values["assignee"] = f"assigned to @{self.assignee}" if self.assignee is not None else "" if "{assignees}" in row_format: format_values["assignees"] = f"assigned to @{self.assignees}" if self.assignees is not None else "" - if "{developers}" in row_format: - format_values["developers"] = f"developed by {self.developers}" if self.developers is not None else "" - if "{contributors}" in row_format: - format_values["contributors"] = f"co-authored by {self.contributors}" if self.contributors is not None else "" + if "{developed-by}" in row_format: + format_values["developed-by"] = f"developed by {self.developers}" if self.developers is not None else "" + if "{co-authored-by}" in row_format: + format_values["co-authored-by"] = f"co-authored by {self.contributors}" if self.contributors is not None else "" return format_values From 9be86e6279925b64d6d46f525aaeba36d759262c Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 1 Oct 2024 09:12:23 +0200 Subject: [PATCH 06/51] - Fixes from manual test. --- release_notes_generator/record/record_factory.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/release_notes_generator/record/record_factory.py b/release_notes_generator/record/record_factory.py index 92e6b0d9..f5345400 100644 --- a/release_notes_generator/record/record_factory.py +++ b/release_notes_generator/record/record_factory.py @@ -99,6 +99,8 @@ def register_commit_to_record(commit: Commit) -> bool: if record.is_commit_sha_present(commit.sha): record.register_commit(commit) return True + + logger.warning(f"Commit '{commit.url}' not registered to record.") return False rate_limiter = GithubRateLimiter(github) From 946368dbb624ee120978ffffa55463ca313fee69 Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 1 Oct 2024 09:19:12 +0200 Subject: [PATCH 07/51] - Fixes from manual test. --- release_notes_generator/generator.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/release_notes_generator/generator.py b/release_notes_generator/generator.py index c59abb2a..0ea39305 100644 --- a/release_notes_generator/generator.py +++ b/release_notes_generator/generator.py @@ -89,15 +89,17 @@ def generate(self) -> Optional[str]: pulls = pulls_all = self._safe_call(repo.get_pulls)(state="closed") commits = commits_all = list(self._safe_call(repo.get_commits)()) + logger.info("Count of issues: %d", len(list(issues))) if rls is not None: - logger.info("Count of issues: %d", len(list(issues))) - # filter out merged PRs and commits before the since date pulls = list(filter(lambda pull: pull.merged_at is not None and pull.merged_at > since, list(pulls_all))) logger.debug("Count of pulls reduced from %d to %d", len(list(pulls_all)), len(pulls)) commits = list(filter(lambda commit: commit.commit.author.date > since, list(commits_all))) logger.debug("Count of commits reduced from %d to %d", len(list(commits_all)), len(commits)) + else: + logger.info("Count of pulls: %d", len(list(pulls))) + logger.info("Count of commits: %d", len(list(commits))) changelog_url = get_change_url(tag_name=ActionInputs.get_tag_name(), repository=repo, git_release=rls) From eb325a57b46608996a4c46a1c3dbb16f13d3258a Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 1 Oct 2024 09:33:43 +0200 Subject: [PATCH 08/51] - Fixes from manual test. --- release_notes_generator/model/record.py | 12 +++++++++--- release_notes_generator/record/record_factory.py | 5 ++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/release_notes_generator/model/record.py b/release_notes_generator/model/record.py index cf8dedba..08975206 100644 --- a/release_notes_generator/model/record.py +++ b/release_notes_generator/model/record.py @@ -264,21 +264,27 @@ def register_pull_request(self, pull) -> None: """ self.__pulls.append(pull) - def register_commit(self, commit: Commit) -> None: + def register_commit(self, commit: Commit) -> bool: """ Registers a commit with the record. @param commit: The Commit object to register. @return: None """ + if self.is_commit_sha_present(commit.sha): + logger.debug("Record does not contains commit sha. Skipping for commit registration check.") + return False + for pull in self.__pulls: if commit.sha == pull.merge_commit_sha: if self.__pull_commits.get(pull.number) is None: self.__pull_commits[pull.number] = [] self.__pull_commits[pull.number].append(commit) - return + logger.debug("Commit %s registered in PR %s of record %s", commit.sha, pull.number, self.number) + return True - logger.error("Commit %s not registered in any PR of record %s", commit.sha, self.number) + logger.error("Commit %s registered in any PR of record %s", commit.sha, self.number) + return False def to_chapter_row(self) -> str: """ diff --git a/release_notes_generator/record/record_factory.py b/release_notes_generator/record/record_factory.py index f5345400..5d3be44f 100644 --- a/release_notes_generator/record/record_factory.py +++ b/release_notes_generator/record/record_factory.py @@ -96,11 +96,10 @@ def register_commit_to_record(commit: Commit) -> bool: @return: True if the commit was registered to a record, False otherwise """ for record in records.values(): - if record.is_commit_sha_present(commit.sha): - record.register_commit(commit) + if record.register_commit(commit): return True - logger.warning(f"Commit '{commit.url}' not registered to record.") + logger.warning(f"Commit '{commit.url}' registered to any record.") return False rate_limiter = GithubRateLimiter(github) From 91569968a4dd9d31b53ecf60ee70c64f8ff252e2 Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 1 Oct 2024 10:05:28 +0200 Subject: [PATCH 09/51] - Fixes from manual test. --- release_notes_generator/model/record.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/release_notes_generator/model/record.py b/release_notes_generator/model/record.py index 08975206..65dcbef0 100644 --- a/release_notes_generator/model/record.py +++ b/release_notes_generator/model/record.py @@ -19,6 +19,7 @@ """ import logging +import re from typing import Optional from github.Issue import Issue @@ -272,18 +273,31 @@ def register_commit(self, commit: Commit) -> bool: @return: None """ if self.is_commit_sha_present(commit.sha): - logger.debug("Record does not contains commit sha. Skipping for commit registration check.") + logger.debug("Record '%s' does not contains commit sha. Skipping for commit registration check.", self.number) return False for pull in self.__pulls: - if commit.sha == pull.merge_commit_sha: + sha = commit.sha + if sha == pull.merge_commit_sha or sha == pull.head.sha: if self.__pull_commits.get(pull.number) is None: self.__pull_commits[pull.number] = [] self.__pull_commits[pull.number].append(commit) - logger.debug("Commit %s registered in PR %s of record %s", commit.sha, pull.number, self.number) + logger.debug("Commit %s registered using sha in PR %s of record %s", commit.sha, pull.number, self.number) return True - logger.error("Commit %s registered in any PR of record %s", commit.sha, self.number) + # Parse commit message for PR numbers + message = commit.commit.message + match = re.search(r"Merge pull request #(\d+)", message) + if not match: + match = re.search(r"\(#(\d+)\)", message) + if match: + pr_number = int(match.group(1)) + if self.__pull_commits.get(pr_number) is None: + self.__pull_commits[pr_number] = [] + self.__pull_commits[pr_number].append(commit) + logger.debug("Commit %s registered using message in PR %s of record %s", commit.sha, pr_number, self.number) + return True + return False def to_chapter_row(self) -> str: From 3c6bce8bafd5c9efaf29d189f3138b08e9dad7b5 Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 1 Oct 2024 10:18:49 +0200 Subject: [PATCH 10/51] - Fixes from manual test. --- release_notes_generator/model/record.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/release_notes_generator/model/record.py b/release_notes_generator/model/record.py index 65dcbef0..32df9cf3 100644 --- a/release_notes_generator/model/record.py +++ b/release_notes_generator/model/record.py @@ -272,9 +272,9 @@ def register_commit(self, commit: Commit) -> bool: @param commit: The Commit object to register. @return: None """ - if self.is_commit_sha_present(commit.sha): - logger.debug("Record '%s' does not contains commit sha. Skipping for commit registration check.", self.number) - return False + # if self.is_commit_sha_present(commit.sha): + # logger.debug("Record '%s' does not contain commit sha. Skipping for commit registration check.", self.number) + # return False for pull in self.__pulls: sha = commit.sha From 8585cf6dbe8437a55649e0ceab75a7d8ddbbaea3 Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 1 Oct 2024 10:35:37 +0200 Subject: [PATCH 11/51] - Fixes from manual test. --- release_notes_generator/model/record.py | 17 ----------------- .../record/record_factory.py | 6 +++++- 2 files changed, 5 insertions(+), 18 deletions(-) diff --git a/release_notes_generator/model/record.py b/release_notes_generator/model/record.py index 32df9cf3..23617989 100644 --- a/release_notes_generator/model/record.py +++ b/release_notes_generator/model/record.py @@ -272,10 +272,6 @@ def register_commit(self, commit: Commit) -> bool: @param commit: The Commit object to register. @return: None """ - # if self.is_commit_sha_present(commit.sha): - # logger.debug("Record '%s' does not contain commit sha. Skipping for commit registration check.", self.number) - # return False - for pull in self.__pulls: sha = commit.sha if sha == pull.merge_commit_sha or sha == pull.head.sha: @@ -397,19 +393,6 @@ def present_in_chapters(self) -> int: """ return self.__present_in_chapters - def is_commit_sha_present(self, sha: str) -> bool: - """ - Checks if the specified commit SHA is present in the record. - - @param sha: The commit SHA to check for. - @return: A boolean indicating whether the specified commit SHA is present in the record. - """ - for pull in self.__pulls: - if pull.merge_commit_sha == sha: - return True - - return False - @staticmethod def is_pull_request_merged(pull: PullRequest) -> bool: """ diff --git a/release_notes_generator/record/record_factory.py b/release_notes_generator/record/record_factory.py index 5d3be44f..2632c822 100644 --- a/release_notes_generator/record/record_factory.py +++ b/release_notes_generator/record/record_factory.py @@ -25,6 +25,7 @@ from github.PullRequest import PullRequest from github.Repository import Repository from github.Commit import Commit +from jsonschema.benchmarks.issue232 import issue232 from release_notes_generator.model.record import Record @@ -105,9 +106,12 @@ def register_commit_to_record(commit: Commit) -> bool: rate_limiter = GithubRateLimiter(github) safe_call = safe_call_decorator(rate_limiter) + real_issue_counts = len(issues) # issues could contain PRs too - known behaviour from API for issue in issues: if issue.number not in pull_numbers: create_record_for_issue(repo, issue) + else: + real_issue_counts -= 1 for pull in pulls: if not extract_issue_numbers_from_body(pull): @@ -122,7 +126,7 @@ def register_commit_to_record(commit: Commit) -> bool: logger.info( "Generated %d records from %d issues and %d PRs, with %d commits detected.", len(records), - len(issues), + real_issue_counts, len(pulls), detected_prs_count, ) From ffc4fc68cf59b9768ae4f6b383fad1d48b6097b0 Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 1 Oct 2024 10:44:04 +0200 Subject: [PATCH 12/51] - Fixes from manual test. --- release_notes_generator/generator.py | 2 +- release_notes_generator/record/record_factory.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/release_notes_generator/generator.py b/release_notes_generator/generator.py index 0ea39305..caf32783 100644 --- a/release_notes_generator/generator.py +++ b/release_notes_generator/generator.py @@ -89,7 +89,7 @@ def generate(self) -> Optional[str]: pulls = pulls_all = self._safe_call(repo.get_pulls)(state="closed") commits = commits_all = list(self._safe_call(repo.get_commits)()) - logger.info("Count of issues: %d", len(list(issues))) + logger.info("Count of issues: %d. (Note: Mixture of Issues and PRs from API :-(.)", len(list(issues))) if rls is not None: # filter out merged PRs and commits before the since date pulls = list(filter(lambda pull: pull.merged_at is not None and pull.merged_at > since, list(pulls_all))) diff --git a/release_notes_generator/record/record_factory.py b/release_notes_generator/record/record_factory.py index 2632c822..a030b71a 100644 --- a/release_notes_generator/record/record_factory.py +++ b/release_notes_generator/record/record_factory.py @@ -26,6 +26,7 @@ from github.Repository import Repository from github.Commit import Commit from jsonschema.benchmarks.issue232 import issue232 +from sympy.strategies.branch import debug from release_notes_generator.model.record import Record @@ -111,7 +112,9 @@ def register_commit_to_record(commit: Commit) -> bool: if issue.number not in pull_numbers: create_record_for_issue(repo, issue) else: + logger.debug("Detected issue number among pulls one %s", issue.number) real_issue_counts -= 1 + logger.debug("New count of issues is %s", real_issue_counts) for pull in pulls: if not extract_issue_numbers_from_body(pull): From c0a283e4b608676c47fd07b90deeab170aa00512 Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 1 Oct 2024 11:00:18 +0200 Subject: [PATCH 13/51] - Fixes from manual test. --- release_notes_generator/record/record_factory.py | 1 + 1 file changed, 1 insertion(+) diff --git a/release_notes_generator/record/record_factory.py b/release_notes_generator/record/record_factory.py index a030b71a..3b57d1f3 100644 --- a/release_notes_generator/record/record_factory.py +++ b/release_notes_generator/record/record_factory.py @@ -110,6 +110,7 @@ def register_commit_to_record(commit: Commit) -> bool: real_issue_counts = len(issues) # issues could contain PRs too - known behaviour from API for issue in issues: if issue.number not in pull_numbers: + logger.debug("Calling create issue for number %s", issue.number) create_record_for_issue(repo, issue) else: logger.debug("Detected issue number among pulls one %s", issue.number) From 1e8b6bce818a5c45fb67df879a0caa45e18ee088 Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 1 Oct 2024 11:03:48 +0200 Subject: [PATCH 14/51] - Fixes from manual test. --- release_notes_generator/record/record_factory.py | 1 + 1 file changed, 1 insertion(+) diff --git a/release_notes_generator/record/record_factory.py b/release_notes_generator/record/record_factory.py index 3b57d1f3..934b9c51 100644 --- a/release_notes_generator/record/record_factory.py +++ b/release_notes_generator/record/record_factory.py @@ -109,6 +109,7 @@ def register_commit_to_record(commit: Commit) -> bool: real_issue_counts = len(issues) # issues could contain PRs too - known behaviour from API for issue in issues: + logger.debug("Hello for issue %s", issue) if issue.number not in pull_numbers: logger.debug("Calling create issue for number %s", issue.number) create_record_for_issue(repo, issue) From 80952d861aa34d2e24709d6ac6881a5dbbaca539 Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 1 Oct 2024 11:05:38 +0200 Subject: [PATCH 15/51] - Fixes from manual test. --- release_notes_generator/record/record_factory.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/release_notes_generator/record/record_factory.py b/release_notes_generator/record/record_factory.py index 934b9c51..9372c56c 100644 --- a/release_notes_generator/record/record_factory.py +++ b/release_notes_generator/record/record_factory.py @@ -25,8 +25,6 @@ from github.PullRequest import PullRequest from github.Repository import Repository from github.Commit import Commit -from jsonschema.benchmarks.issue232 import issue232 -from sympy.strategies.branch import debug from release_notes_generator.model.record import Record From ad799447a035c1674bb9e0a3ab4c5473ee62eb41 Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 1 Oct 2024 11:22:52 +0200 Subject: [PATCH 16/51] - Fixes from manual test. --- .../record/record_factory.py | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/release_notes_generator/record/record_factory.py b/release_notes_generator/record/record_factory.py index 9372c56c..606c0629 100644 --- a/release_notes_generator/record/record_factory.py +++ b/release_notes_generator/record/record_factory.py @@ -19,6 +19,7 @@ """ import logging +import sys from github import Github from github.Issue import Issue @@ -55,7 +56,7 @@ def generate( @param commits: The list of commits. @return: A dictionary of records. """ - records = {} + records = {sys.maxsize: Record(repo)} pull_numbers = [pull.number for pull in pulls] def create_record_for_issue(r: Repository, i: Issue): @@ -88,19 +89,18 @@ def register_pull_request(pull: PullRequest): parent_issue_number, ) - def register_commit_to_record(commit: Commit) -> bool: + def register_commit_to_record(commit: Commit) -> None: """ Register a commit to a record if the commit is linked to an issue or a PR. @param commit: The commit to register. - @return: True if the commit was registered to a record, False otherwise + @return: None """ for record in records.values(): if record.register_commit(commit): - return True + return - logger.warning(f"Commit '{commit.url}' registered to any record.") - return False + records[sys.maxsize].register_commit(commit) rate_limiter = GithubRateLimiter(github) safe_call = safe_call_decorator(rate_limiter) @@ -124,13 +124,15 @@ def register_commit_to_record(commit: Commit) -> bool: else: register_pull_request(pull) - detected_prs_count = sum(register_commit_to_record(commit) for commit in commits) + for commit in commits: + register_commit_to_record(commit) logger.info( - "Generated %d records from %d issues and %d PRs, with %d commits detected.", + "Generated %d records from %d issues and %d PRs, with %d commits detected. %d of commits are isolated", len(records), real_issue_counts, len(pulls), - detected_prs_count, + len(commits), + len(records[sys.maxsize].commits) ) return records From 67f19616aabcbada48580e208f605929e0369902 Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 1 Oct 2024 12:11:55 +0200 Subject: [PATCH 17/51] - Fixes from manual test. --- release_notes_generator/generator.py | 2 +- .../model/custom_chapters.py | 3 + .../model/isolated_commits_record.py | 148 ++++++++++++++++++ release_notes_generator/model/record.py | 1 + .../model/service_chapters.py | 68 ++++---- .../record/record_factory.py | 18 ++- release_notes_generator/utils/constants.py | 2 + 7 files changed, 205 insertions(+), 37 deletions(-) create mode 100644 release_notes_generator/model/isolated_commits_record.py diff --git a/release_notes_generator/generator.py b/release_notes_generator/generator.py index caf32783..bd1dc993 100644 --- a/release_notes_generator/generator.py +++ b/release_notes_generator/generator.py @@ -103,7 +103,7 @@ def generate(self) -> Optional[str]: changelog_url = get_change_url(tag_name=ActionInputs.get_tag_name(), repository=repo, git_release=rls) - rls_notes_records: dict[int, Record] = RecordFactory.generate( + rls_notes_records: dict[int|str, Record] = RecordFactory.generate( github=self.github_instance, repo=repo, issues=list(issues), # PaginatedList --> list diff --git a/release_notes_generator/model/custom_chapters.py b/release_notes_generator/model/custom_chapters.py index a2582141..f35fcf03 100644 --- a/release_notes_generator/model/custom_chapters.py +++ b/release_notes_generator/model/custom_chapters.py @@ -41,6 +41,9 @@ def populate(self, records: dict[int, Record]) -> None: @return: None """ for nr in records: # iterate all records + if isinstance(nr, str): + continue + for ch in self.chapters.values(): # iterate all chapters if nr in self.populated_record_numbers_list and ActionInputs.get_duplicity_scope() not in ( DuplicityScopeEnum.CUSTOM, diff --git a/release_notes_generator/model/isolated_commits_record.py b/release_notes_generator/model/isolated_commits_record.py new file mode 100644 index 00000000..e23c2e50 --- /dev/null +++ b/release_notes_generator/model/isolated_commits_record.py @@ -0,0 +1,148 @@ +# +# Copyright 2023 ABSA Group Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +""" +This module contains the BaseChapters class which is responsible for representing the base chapters. +""" + +import logging +import re +import sys +from typing import Optional + +from github.Issue import Issue +from github.PullRequest import PullRequest +from github.Repository import Repository +from github.Commit import Commit + +from release_notes_generator.action_inputs import ActionInputs +from release_notes_generator.model.record import Record +from release_notes_generator.utils.constants import ( + RELEASE_NOTE_DETECTION_PATTERN, + RELEASE_NOTE_LINE_MARK, +) + +logger = logging.getLogger(__name__) + + +class IsolatedCommitsRecord(Record): + """ + A class used to represent a record in the release notes. + The record holds isolated commit without link to Issue or Pull request. + """ + + def __init__(self, repo: Repository): + super().__init__(repo) + + @property + def number(self) -> int: + """Getter for the number of the record.""" + return sys.maxsize + + @property + def is_pr(self) -> bool: + """Check if the record is a pull request.""" + return False + + @property + def is_issue(self) -> bool: + """Check if the record is an issue.""" + return False + + @property + def is_closed(self) -> bool: + return False + + @property + def is_merged_pr(self) -> bool: + return False + + @property + def labels(self) -> list[str]: + return [] + + def get_rls_notes(self, detection_pattern=RELEASE_NOTE_DETECTION_PATTERN, line_mark=RELEASE_NOTE_LINE_MARK) -> str: + return "" + + @property + def contains_release_notes(self) -> bool: + return False + + @property + def pr_contains_issue_mentions(self) -> bool: + return False + + @property + def assignee(self) -> Optional[str]: + return None + + @property + def assignees(self) -> Optional[str]: + return None + + @property + def developers(self) -> Optional[str]: + return None + + @property + def contributors(self) -> Optional[str]: + return None + + @property + def pr_links(self) -> Optional[str]: + return None + + def pull_request_commit_count(self, pull_number: int = 0) -> int: + return len(self.commits) + + def pull_request(self, index: int = 0) -> Optional[PullRequest]: + return None + + def register_pull_request(self, pull) -> None: + pass + + def register_commit(self, commit: Commit) -> bool: + """ + Registers a commit with the record. + + @param commit: The Commit object to register. + @return: Always return True. + """ + self.__pull_commits[0] = commit + return True + + def to_chapter_row(self) -> str: + """ + Converts the record to a string row in a chapter. + + @return: The record as a row string. + """ + self.increment_present_in_chapters() + row_prefix = f"{ActionInputs.get_duplicity_icon()} " if self.present_in_chapters() > 1 else "" + return f"{row_prefix}Commit: {self.commits[0].sha}" + + def __get_row_format_values(self, row_format: str) -> dict: + return {} + + def contains_min_one_label(self, labels: list[str]) -> bool: + return False + + def contain_all_labels(self, labels: list[str]) -> bool: + return False + + @staticmethod + def is_pull_request_merged(pull: PullRequest) -> bool: + return False diff --git a/release_notes_generator/model/record.py b/release_notes_generator/model/record.py index 23617989..a3b62e08 100644 --- a/release_notes_generator/model/record.py +++ b/release_notes_generator/model/record.py @@ -20,6 +20,7 @@ import logging import re +import sys from typing import Optional from github.Issue import Issue diff --git a/release_notes_generator/model/service_chapters.py b/release_notes_generator/model/service_chapters.py index 7eaf3d09..ef7f41be 100644 --- a/release_notes_generator/model/service_chapters.py +++ b/release_notes_generator/model/service_chapters.py @@ -18,6 +18,8 @@ This module contains the ServiceChapters class which is responsible for representing the service chapters in the release notes. """ +from typing import Union + from release_notes_generator.action_inputs import ActionInputs from release_notes_generator.model.base_chapters import BaseChapters from release_notes_generator.model.chapter import Chapter @@ -28,7 +30,7 @@ MERGED_PRS_WITHOUT_ISSUE_AND_USER_DEFINED_LABELS, CLOSED_PRS_WITHOUT_ISSUE_AND_USER_DEFINED_LABELS, MERGED_PRS_LINKED_TO_NOT_CLOSED_ISSUES, - OTHERS_NO_TOPIC, + OTHERS_NO_TOPIC, ISOLATED_COMMITS, ) from release_notes_generator.utils.enums import DuplicityScopeEnum @@ -44,7 +46,7 @@ def __init__( sort_ascending: bool = True, print_empty_chapters: bool = True, user_defined_labels: list[str] = None, - used_record_numbers: list[int] = None, + used_record_numbers: list[Union[int, str]] = None, ): super().__init__(sort_ascending, print_empty_chapters) @@ -52,11 +54,14 @@ def __init__( self.sort_ascending = sort_ascending if used_record_numbers is None: - self.used_record_numbers = [] + self.used_record_numbers: list[Union[int, str]] = [] else: self.used_record_numbers = used_record_numbers self.chapters = { + ISOLATED_COMMITS: Chapter( + title=ISOLATED_COMMITS, empty_message="All commits are linked to an Issues or a Pull Request." + ), CLOSED_ISSUES_WITHOUT_PULL_REQUESTS: Chapter( title=CLOSED_ISSUES_WITHOUT_PULL_REQUESTS, empty_message="All closed issues linked to a Pull Request." ), @@ -87,7 +92,7 @@ def __init__( self.show_chapter_merged_prs_linked_to_open_issues = True - def populate(self, records: dict[int, Record]) -> None: + def populate(self, records: dict[int|str, Record]) -> None: """ Populates the service chapters with records. @@ -100,7 +105,10 @@ def populate(self, records: dict[int, Record]) -> None: if self.__is_row_present(nr) and not self.duplicity_allowed(): continue - if records[nr].is_closed_issue: + if isinstance(nr, str): + self.__populate_isolated_commits(records[nr], nr) + + elif records[nr].is_closed_issue: self.__populate_closed_issues(records[nr], nr) elif records[nr].is_pr: @@ -116,32 +124,36 @@ def populate(self, records: dict[int, Record]) -> None: self.chapters[OTHERS_NO_TOPIC].add_row(nr, records[nr].to_chapter_row()) self.used_record_numbers.append(nr) - def __populate_closed_issues(self, record: Record, nr: int) -> None: + def __populate_isolated_commits(self, r: Record, nr: str) -> None: + self.chapters[ISOLATED_COMMITS].add_row(nr, r.to_chapter_row()) + self.used_record_numbers.append(nr) + + def __populate_closed_issues(self, r: Record, nr: int) -> None: """ Populates the service chapters with closed issues. - @param record: The Record object representing the closed issue. + @param r: The Record object representing the closed issue. @param nr: The number of the record. @return: None """ # check record properties if it fits to a chapter: CLOSED_ISSUES_WITHOUT_PULL_REQUESTS populated = False - if record.pulls_count == 0: - self.chapters[CLOSED_ISSUES_WITHOUT_PULL_REQUESTS].add_row(nr, record.to_chapter_row()) + if r.pulls_count == 0: + self.chapters[CLOSED_ISSUES_WITHOUT_PULL_REQUESTS].add_row(nr, r.to_chapter_row()) self.used_record_numbers.append(nr) populated = True # check record properties if it fits to a chapter: CLOSED_ISSUES_WITHOUT_USER_DEFINED_LABELS - if not record.contains_min_one_label(self.user_defined_labels): + if not r.contains_min_one_label(self.user_defined_labels): # check if the record is already present among the chapters if self.__is_row_present(nr) and not self.duplicity_allowed(): return - self.chapters[CLOSED_ISSUES_WITHOUT_USER_DEFINED_LABELS].add_row(nr, record.to_chapter_row()) + self.chapters[CLOSED_ISSUES_WITHOUT_USER_DEFINED_LABELS].add_row(nr, r.to_chapter_row()) self.used_record_numbers.append(nr) populated = True - if record.pulls_count > 0: + if r.pulls_count > 0: # the record looks to be valid closed issue with 1+ pull requests return @@ -149,51 +161,51 @@ def __populate_closed_issues(self, record: Record, nr: int) -> None: if self.__is_row_present(nr) and not self.duplicity_allowed(): return - self.chapters[OTHERS_NO_TOPIC].add_row(nr, record.to_chapter_row()) + self.chapters[OTHERS_NO_TOPIC].add_row(nr, r.to_chapter_row()) self.used_record_numbers.append(nr) - def __populate_pr(self, record: Record, nr: int) -> None: + def __populate_pr(self, r: Record, nr: int) -> None: """ Populates the service chapters with pull requests. - @param record: The Record object representing the pull request. + @param r: The Record object representing the pull request. @param nr: The number of the record. @return: None """ - if record.is_merged_pr: + if r.is_merged_pr: # check record properties if it fits to a chapter: MERGED_PRS_WITHOUT_ISSUE - if not record.pr_contains_issue_mentions and not record.contains_min_one_label(self.user_defined_labels): + if not r.pr_contains_issue_mentions and not r.contains_min_one_label(self.user_defined_labels): if self.__is_row_present(nr) and not self.duplicity_allowed(): return - self.chapters[MERGED_PRS_WITHOUT_ISSUE_AND_USER_DEFINED_LABELS].add_row(nr, record.to_chapter_row()) + self.chapters[MERGED_PRS_WITHOUT_ISSUE_AND_USER_DEFINED_LABELS].add_row(nr, r.to_chapter_row()) self.used_record_numbers.append(nr) # check record properties if it fits to a chapter: MERGED_PRS_LINKED_TO_NOT_CLOSED_ISSUES - if record.pr_contains_issue_mentions: + if r.pr_contains_issue_mentions: if self.__is_row_present(nr) and not self.duplicity_allowed(): return - self.chapters[MERGED_PRS_LINKED_TO_NOT_CLOSED_ISSUES].add_row(nr, record.to_chapter_row()) + self.chapters[MERGED_PRS_LINKED_TO_NOT_CLOSED_ISSUES].add_row(nr, r.to_chapter_row()) self.used_record_numbers.append(nr) - if not record.is_present_in_chapters: + if not r.is_present_in_chapters: if self.__is_row_present(nr) and not self.duplicity_allowed(): return - self.chapters[OTHERS_NO_TOPIC].add_row(nr, record.to_chapter_row()) + self.chapters[OTHERS_NO_TOPIC].add_row(nr, r.to_chapter_row()) self.used_record_numbers.append(nr) # check record properties if it fits to a chapter: CLOSED_PRS_WITHOUT_ISSUE elif ( - record.is_closed - and not record.pr_contains_issue_mentions - and not record.contains_min_one_label(self.user_defined_labels) + r.is_closed + and not r.pr_contains_issue_mentions + and not r.contains_min_one_label(self.user_defined_labels) ): if self.__is_row_present(nr) and not self.duplicity_allowed(): return - self.chapters[CLOSED_PRS_WITHOUT_ISSUE_AND_USER_DEFINED_LABELS].add_row(nr, record.to_chapter_row()) + self.chapters[CLOSED_PRS_WITHOUT_ISSUE_AND_USER_DEFINED_LABELS].add_row(nr, r.to_chapter_row()) self.used_record_numbers.append(nr) else: @@ -201,10 +213,10 @@ def __populate_pr(self, record: Record, nr: int) -> None: return # not record.is_present_in_chapters: - self.chapters[OTHERS_NO_TOPIC].add_row(nr, record.to_chapter_row()) + self.chapters[OTHERS_NO_TOPIC].add_row(nr, r.to_chapter_row()) self.used_record_numbers.append(nr) - def __is_row_present(self, nr: int) -> bool: + def __is_row_present(self, nr: int|str) -> bool: return nr in self.used_record_numbers @staticmethod diff --git a/release_notes_generator/record/record_factory.py b/release_notes_generator/record/record_factory.py index 606c0629..3ad3725d 100644 --- a/release_notes_generator/record/record_factory.py +++ b/release_notes_generator/record/record_factory.py @@ -21,12 +21,14 @@ import logging import sys +from astroid.brain.brain_unittest import IsolatedAsyncioTestCaseImport from github import Github from github.Issue import Issue from github.PullRequest import PullRequest from github.Repository import Repository from github.Commit import Commit +from release_notes_generator.model.isolated_commits_record import IsolatedCommitsRecord from release_notes_generator.model.record import Record from release_notes_generator.utils.decorators import safe_call_decorator @@ -45,7 +47,7 @@ class RecordFactory: @staticmethod def generate( github: Github, repo: Repository, issues: list[Issue], pulls: list[PullRequest], commits: list[Commit] - ) -> dict[int, Record]: + ) -> dict[int|str, Record]: """ Generate records for release notes. @@ -56,7 +58,7 @@ def generate( @param commits: The list of commits. @return: A dictionary of records. """ - records = {sys.maxsize: Record(repo)} + records: dict[int|str, Record] = {} pull_numbers = [pull.number for pull in pulls] def create_record_for_issue(r: Repository, i: Issue): @@ -89,18 +91,19 @@ def register_pull_request(pull: PullRequest): parent_issue_number, ) - def register_commit_to_record(commit: Commit) -> None: + def register_commit_to_record(c: Commit) -> None: """ Register a commit to a record if the commit is linked to an issue or a PR. - @param commit: The commit to register. + @param c: The commit to register. @return: None """ for record in records.values(): - if record.register_commit(commit): + if record.register_commit(c): return - records[sys.maxsize].register_commit(commit) + records[c.sha] = IsolatedCommitsRecord(repo) + records[c.sha].register_commit(c) rate_limiter = GithubRateLimiter(github) safe_call = safe_call_decorator(rate_limiter) @@ -112,9 +115,8 @@ def register_commit_to_record(commit: Commit) -> None: logger.debug("Calling create issue for number %s", issue.number) create_record_for_issue(repo, issue) else: - logger.debug("Detected issue number among pulls one %s", issue.number) + logger.debug("Detected pr number %s among issues", issue.number) real_issue_counts -= 1 - logger.debug("New count of issues is %s", real_issue_counts) for pull in pulls: if not extract_issue_numbers_from_body(pull): diff --git a/release_notes_generator/utils/constants.py b/release_notes_generator/utils/constants.py index 0824188a..69086f27 100644 --- a/release_notes_generator/utils/constants.py +++ b/release_notes_generator/utils/constants.py @@ -53,6 +53,8 @@ RELEASE_NOTE_LINE_MARK = "-" # Service chapters titles +ISOLATED_COMMITS: str = "Isolated commits without Issue ot PR ⚠️" + CLOSED_ISSUES_WITHOUT_PULL_REQUESTS: str = "Closed Issues without Pull Request ⚠️" CLOSED_ISSUES_WITHOUT_USER_DEFINED_LABELS: str = "Closed Issues without User Defined Labels ⚠️" From 291dd9ca482753c7d16313e7bc207aa5909ab383 Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 1 Oct 2024 12:20:13 +0200 Subject: [PATCH 18/51] - Fixes from manual test. --- release_notes_generator/model/isolated_commits_record.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/release_notes_generator/model/isolated_commits_record.py b/release_notes_generator/model/isolated_commits_record.py index e23c2e50..ba49ece1 100644 --- a/release_notes_generator/model/isolated_commits_record.py +++ b/release_notes_generator/model/isolated_commits_record.py @@ -19,11 +19,9 @@ """ import logging -import re import sys from typing import Optional -from github.Issue import Issue from github.PullRequest import PullRequest from github.Repository import Repository from github.Commit import Commit @@ -46,6 +44,7 @@ class IsolatedCommitsRecord(Record): def __init__(self, repo: Repository): super().__init__(repo) + self.__commits: dict = {} @property def number(self) -> int: @@ -121,7 +120,7 @@ def register_commit(self, commit: Commit) -> bool: @param commit: The Commit object to register. @return: Always return True. """ - self.__pull_commits[0] = commit + self.__commits[0] = commit return True def to_chapter_row(self) -> str: From daa4b67aeb6089d55a37be9f414bd6d7e93a2986 Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 1 Oct 2024 12:24:47 +0200 Subject: [PATCH 19/51] - Fixes from manual test. --- release_notes_generator/record/record_factory.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/release_notes_generator/record/record_factory.py b/release_notes_generator/record/record_factory.py index 3ad3725d..ec5efc90 100644 --- a/release_notes_generator/record/record_factory.py +++ b/release_notes_generator/record/record_factory.py @@ -27,6 +27,7 @@ from github.PullRequest import PullRequest from github.Repository import Repository from github.Commit import Commit +from numpy.core.records import record from release_notes_generator.model.isolated_commits_record import IsolatedCommitsRecord from release_notes_generator.model.record import Record @@ -135,6 +136,6 @@ def register_commit_to_record(c: Commit) -> None: real_issue_counts, len(pulls), len(commits), - len(records[sys.maxsize].commits) + sum(isinstance(nr, str) for nr in records.keys()) ) return records From f6e76472da70b94ccacc433ca04353b3b150f882 Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 1 Oct 2024 12:28:21 +0200 Subject: [PATCH 20/51] - Fixes from manual test. --- release_notes_generator/record/record_factory.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/release_notes_generator/record/record_factory.py b/release_notes_generator/record/record_factory.py index ec5efc90..17809488 100644 --- a/release_notes_generator/record/record_factory.py +++ b/release_notes_generator/record/record_factory.py @@ -19,15 +19,12 @@ """ import logging -import sys -from astroid.brain.brain_unittest import IsolatedAsyncioTestCaseImport from github import Github from github.Issue import Issue from github.PullRequest import PullRequest from github.Repository import Repository from github.Commit import Commit -from numpy.core.records import record from release_notes_generator.model.isolated_commits_record import IsolatedCommitsRecord from release_notes_generator.model.record import Record From f83b490ebc293d69dbd60c93007f6291c70763cc Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 1 Oct 2024 12:32:49 +0200 Subject: [PATCH 21/51] - Fixes from manual test. --- release_notes_generator/model/isolated_commits_record.py | 1 + 1 file changed, 1 insertion(+) diff --git a/release_notes_generator/model/isolated_commits_record.py b/release_notes_generator/model/isolated_commits_record.py index ba49ece1..7538c8f4 100644 --- a/release_notes_generator/model/isolated_commits_record.py +++ b/release_notes_generator/model/isolated_commits_record.py @@ -121,6 +121,7 @@ def register_commit(self, commit: Commit) -> bool: @return: Always return True. """ self.__commits[0] = commit + logger.debug("Commit 'type: Isolated' %s registered", commit.sha) return True def to_chapter_row(self) -> str: From 4e877b28bbfecb55b00162bc1e4847ec5815e609 Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 1 Oct 2024 12:40:06 +0200 Subject: [PATCH 22/51] - Fixes from manual test. --- release_notes_generator/model/isolated_commits_record.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/release_notes_generator/model/isolated_commits_record.py b/release_notes_generator/model/isolated_commits_record.py index 7538c8f4..20dc8f43 100644 --- a/release_notes_generator/model/isolated_commits_record.py +++ b/release_notes_generator/model/isolated_commits_record.py @@ -51,6 +51,10 @@ def number(self) -> int: """Getter for the number of the record.""" return sys.maxsize + @property + def commits(self) -> dict: + return self.__commits + @property def is_pr(self) -> bool: """Check if the record is a pull request.""" From d16adb08484d023c0e88b491a1b7c0d627986599 Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 1 Oct 2024 12:43:30 +0200 Subject: [PATCH 23/51] - Fixes from manual test. --- release_notes_generator/record/record_factory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release_notes_generator/record/record_factory.py b/release_notes_generator/record/record_factory.py index 17809488..74db12c4 100644 --- a/release_notes_generator/record/record_factory.py +++ b/release_notes_generator/record/record_factory.py @@ -133,6 +133,6 @@ def register_commit_to_record(c: Commit) -> None: real_issue_counts, len(pulls), len(commits), - sum(isinstance(nr, str) for nr in records.keys()) + sum(isinstance(r, IsolatedCommitsRecord) for r in records) ) return records From 5cc9c12fa48e9b974d4e52480b440c64a2095cc3 Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 1 Oct 2024 12:54:50 +0200 Subject: [PATCH 24/51] - Fixes from manual test. --- .../record/record_factory.py | 103 +++++++++--------- 1 file changed, 53 insertions(+), 50 deletions(-) diff --git a/release_notes_generator/record/record_factory.py b/release_notes_generator/record/record_factory.py index 74db12c4..732021c5 100644 --- a/release_notes_generator/record/record_factory.py +++ b/release_notes_generator/record/record_factory.py @@ -59,59 +59,12 @@ def generate( records: dict[int|str, Record] = {} pull_numbers = [pull.number for pull in pulls] - def create_record_for_issue(r: Repository, i: Issue): - records[i.number] = Record(r, i) - logger.debug("Created record for issue %d: %s", i.number, i.title) - - def register_pull_request(pull: PullRequest): - for parent_issue_number in extract_issue_numbers_from_body(pull): - if parent_issue_number not in records: - logger.warning( - "Detected PR %d linked to issue %d which is not in the list of received issues. " - "Fetching ...", - pull.number, - parent_issue_number, - ) - parent_issue = safe_call(repo.get_issue)(parent_issue_number) - if parent_issue is not None: - create_record_for_issue(repo, parent_issue) - - if parent_issue_number in records: - records[parent_issue_number].register_pull_request(pull) - logger.debug("Registering PR %d: %s to Issue %d", pull.number, pull.title, parent_issue_number) - else: - records[pull.number] = Record(repo) - records[pull.number].register_pull_request(pull) - logger.debug( - "Registering stand-alone PR %d: %s as mentioned Issue %d not found.", - pull.number, - pull.title, - parent_issue_number, - ) - - def register_commit_to_record(c: Commit) -> None: - """ - Register a commit to a record if the commit is linked to an issue or a PR. - - @param c: The commit to register. - @return: None - """ - for record in records.values(): - if record.register_commit(c): - return - - records[c.sha] = IsolatedCommitsRecord(repo) - records[c.sha].register_commit(c) - - rate_limiter = GithubRateLimiter(github) - safe_call = safe_call_decorator(rate_limiter) - real_issue_counts = len(issues) # issues could contain PRs too - known behaviour from API for issue in issues: logger.debug("Hello for issue %s", issue) if issue.number not in pull_numbers: logger.debug("Calling create issue for number %s", issue.number) - create_record_for_issue(repo, issue) + RecordFactory.__create_record_for_issue(records, repo, issue) else: logger.debug("Detected pr number %s among issues", issue.number) real_issue_counts -= 1 @@ -122,10 +75,10 @@ def register_commit_to_record(c: Commit) -> None: records[pull.number].register_pull_request(pull) logger.debug("Created record for PR %d: %s", pull.number, pull.title) else: - register_pull_request(pull) + RecordFactory.__register_pull_request(github, records, repo, pull) for commit in commits: - register_commit_to_record(commit) + RecordFactory.__register_commit_to_record(records, repo, commit) logger.info( "Generated %d records from %d issues and %d PRs, with %d commits detected. %d of commits are isolated", @@ -136,3 +89,53 @@ def register_commit_to_record(c: Commit) -> None: sum(isinstance(r, IsolatedCommitsRecord) for r in records) ) return records + + @staticmethod + def __create_record_for_issue(records: dict[int|str, Record], r: Repository, i: Issue): + records[i.number] = Record(r, i) + logger.debug("Created record for issue %d: %s", i.number, i.title) + + @staticmethod + def __register_pull_request(github: Github, records: dict[int|str, Record], repo: Repository, pull: PullRequest): + rate_limiter = GithubRateLimiter(github) + safe_call = safe_call_decorator(rate_limiter) + + for parent_issue_number in extract_issue_numbers_from_body(pull): + if parent_issue_number not in records: + logger.warning( + "Detected PR %d linked to issue %d which is not in the list of received issues. " + "Fetching ...", + pull.number, + parent_issue_number, + ) + parent_issue = safe_call(repo.get_issue)(parent_issue_number) + if parent_issue is not None: + RecordFactory.__create_record_for_issue(records, repo, parent_issue) + + if parent_issue_number in records: + records[parent_issue_number].register_pull_request(pull) + logger.debug("Registering PR %d: %s to Issue %d", pull.number, pull.title, parent_issue_number) + else: + records[pull.number] = Record(repo) + records[pull.number].register_pull_request(pull) + logger.debug( + "Registering stand-alone PR %d: %s as mentioned Issue %d not found.", + pull.number, + pull.title, + parent_issue_number, + ) + + @staticmethod + def __register_commit_to_record(records: dict[int|str, Record], repo: Repository, c: Commit) -> None: + """ + Register a commit to a record if the commit is linked to an issue or a PR. + + @param c: The commit to register. + @return: None + """ + for record in records.values(): + if record.register_commit(c): + return + + records[c.sha] = IsolatedCommitsRecord(repo) + records[c.sha].register_commit(c) From 81dfaec0d482c1cd2ab31b5cce1f63c1516a84fd Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 1 Oct 2024 13:02:14 +0200 Subject: [PATCH 25/51] - Fixes from manual test. --- release_notes_generator/model/isolated_commits_record.py | 2 +- release_notes_generator/record/record_factory.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/release_notes_generator/model/isolated_commits_record.py b/release_notes_generator/model/isolated_commits_record.py index 20dc8f43..4b15c489 100644 --- a/release_notes_generator/model/isolated_commits_record.py +++ b/release_notes_generator/model/isolated_commits_record.py @@ -125,7 +125,7 @@ def register_commit(self, commit: Commit) -> bool: @return: Always return True. """ self.__commits[0] = commit - logger.debug("Commit 'type: Isolated' %s registered", commit.sha) + logger.debug("Registering commit 'type: Isolated' %s registered", commit.sha) return True def to_chapter_row(self) -> str: diff --git a/release_notes_generator/record/record_factory.py b/release_notes_generator/record/record_factory.py index 732021c5..6e858b00 100644 --- a/release_notes_generator/record/record_factory.py +++ b/release_notes_generator/record/record_factory.py @@ -78,7 +78,9 @@ def generate( RecordFactory.__register_pull_request(github, records, repo, pull) for commit in commits: + logger.debug("DEBUG count of records is '%s'", len(records)) RecordFactory.__register_commit_to_record(records, repo, commit) + logger.debug("DEBUG count of records is '%s'", len(records)) logger.info( "Generated %d records from %d issues and %d PRs, with %d commits detected. %d of commits are isolated", @@ -137,5 +139,6 @@ def __register_commit_to_record(records: dict[int|str, Record], repo: Repository if record.register_commit(c): return - records[c.sha] = IsolatedCommitsRecord(repo) - records[c.sha].register_commit(c) + iso_record = IsolatedCommitsRecord(repo) + iso_record.register_commit(c) + records[c.sha] = iso_record From d42079adcc7f6f96dfbc8de683cd972e4906905e Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 1 Oct 2024 13:55:25 +0200 Subject: [PATCH 26/51] - Fixes from manual test. --- release_notes_generator/record/record_factory.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/release_notes_generator/record/record_factory.py b/release_notes_generator/record/record_factory.py index 6e858b00..51f6b453 100644 --- a/release_notes_generator/record/record_factory.py +++ b/release_notes_generator/record/record_factory.py @@ -79,7 +79,7 @@ def generate( for commit in commits: logger.debug("DEBUG count of records is '%s'", len(records)) - RecordFactory.__register_commit_to_record(records, repo, commit) + records = RecordFactory.__register_commit_to_record(records, repo, commit) logger.debug("DEBUG count of records is '%s'", len(records)) logger.info( @@ -128,7 +128,7 @@ def __register_pull_request(github: Github, records: dict[int|str, Record], repo ) @staticmethod - def __register_commit_to_record(records: dict[int|str, Record], repo: Repository, c: Commit) -> None: + def __register_commit_to_record(records: dict[int|str, Record], repo: Repository, c: Commit) -> dict[int|str, Record]: """ Register a commit to a record if the commit is linked to an issue or a PR. @@ -137,8 +137,9 @@ def __register_commit_to_record(records: dict[int|str, Record], repo: Repository """ for record in records.values(): if record.register_commit(c): - return + return records - iso_record = IsolatedCommitsRecord(repo) - iso_record.register_commit(c) - records[c.sha] = iso_record + records[c.sha] = IsolatedCommitsRecord(repo) + records[c.sha].register_commit(c) + + return records From 5e5d595a92f75fd446ba91de913fd997e0e75ecc Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 1 Oct 2024 14:03:57 +0200 Subject: [PATCH 27/51] - Fixes from manual test. --- release_notes_generator/record/record_factory.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/release_notes_generator/record/record_factory.py b/release_notes_generator/record/record_factory.py index 51f6b453..2b3accbc 100644 --- a/release_notes_generator/record/record_factory.py +++ b/release_notes_generator/record/record_factory.py @@ -133,12 +133,15 @@ def __register_commit_to_record(records: dict[int|str, Record], repo: Repository Register a commit to a record if the commit is linked to an issue or a PR. @param c: The commit to register. - @return: None + @return: TODO """ for record in records.values(): if record.register_commit(c): return records + if c.sha in records.keys(): + logger.debug("DEBUG - do we have a problem?") + records[c.sha] = IsolatedCommitsRecord(repo) records[c.sha].register_commit(c) From 55c728b1999a3875b02bf30023b08d484ebe1861 Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 1 Oct 2024 14:16:30 +0200 Subject: [PATCH 28/51] - Fixes from manual test. --- .../model/isolated_commits_record.py | 2 +- .../record/record_factory.py | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/release_notes_generator/model/isolated_commits_record.py b/release_notes_generator/model/isolated_commits_record.py index 4b15c489..29160201 100644 --- a/release_notes_generator/model/isolated_commits_record.py +++ b/release_notes_generator/model/isolated_commits_record.py @@ -125,7 +125,7 @@ def register_commit(self, commit: Commit) -> bool: @return: Always return True. """ self.__commits[0] = commit - logger.debug("Registering commit 'type: Isolated' %s registered", commit.sha) + logger.debug("Registering commit 'type: Isolated' sha: %s", commit.sha) return True def to_chapter_row(self) -> str: diff --git a/release_notes_generator/record/record_factory.py b/release_notes_generator/record/record_factory.py index 2b3accbc..ccc4f2cf 100644 --- a/release_notes_generator/record/record_factory.py +++ b/release_notes_generator/record/record_factory.py @@ -20,6 +20,8 @@ import logging +from typing import Optional + from github import Github from github.Issue import Issue from github.PullRequest import PullRequest @@ -79,7 +81,9 @@ def generate( for commit in commits: logger.debug("DEBUG count of records is '%s'", len(records)) - records = RecordFactory.__register_commit_to_record(records, repo, commit) + isolated_r = RecordFactory.__register_commit_to_record(records, repo, commit) + if isolated_r is not None: + records[commit.sha] = isolated_r logger.debug("DEBUG count of records is '%s'", len(records)) logger.info( @@ -128,7 +132,7 @@ def __register_pull_request(github: Github, records: dict[int|str, Record], repo ) @staticmethod - def __register_commit_to_record(records: dict[int|str, Record], repo: Repository, c: Commit) -> dict[int|str, Record]: + def __register_commit_to_record(records: dict[int|str, Record], repo: Repository, c: Commit) -> Optional[IsolatedCommitsRecord]: """ Register a commit to a record if the commit is linked to an issue or a PR. @@ -137,12 +141,9 @@ def __register_commit_to_record(records: dict[int|str, Record], repo: Repository """ for record in records.values(): if record.register_commit(c): - return records - - if c.sha in records.keys(): - logger.debug("DEBUG - do we have a problem?") + return None - records[c.sha] = IsolatedCommitsRecord(repo) - records[c.sha].register_commit(c) + iso_record = IsolatedCommitsRecord(repo) + iso_record.register_commit(c) - return records + return iso_record From ea98398df6ffb41c85d1667a59a849d149456f4a Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 1 Oct 2024 14:21:24 +0200 Subject: [PATCH 29/51] - Fixes from manual test. --- release_notes_generator/record/record_factory.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/release_notes_generator/record/record_factory.py b/release_notes_generator/record/record_factory.py index ccc4f2cf..30da9856 100644 --- a/release_notes_generator/record/record_factory.py +++ b/release_notes_generator/record/record_factory.py @@ -80,11 +80,12 @@ def generate( RecordFactory.__register_pull_request(github, records, repo, pull) for commit in commits: - logger.debug("DEBUG count of records is '%s'", len(records)) + logger.debug("DEBUG count of records is '%s', keys %s", len(records), records.keys()) + isolated_r = RecordFactory.__register_commit_to_record(records, repo, commit) if isolated_r is not None: records[commit.sha] = isolated_r - logger.debug("DEBUG count of records is '%s'", len(records)) + logger.debug("DEBUG count of records is '%s', keys %s", len(records), records.keys()) logger.info( "Generated %d records from %d issues and %d PRs, with %d commits detected. %d of commits are isolated", From 37bd927c802fa31d850b589750552a8e57b3aae8 Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 1 Oct 2024 14:24:37 +0200 Subject: [PATCH 30/51] - Fixes from manual test. --- release_notes_generator/record/record_factory.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/release_notes_generator/record/record_factory.py b/release_notes_generator/record/record_factory.py index 30da9856..f605fb27 100644 --- a/release_notes_generator/record/record_factory.py +++ b/release_notes_generator/record/record_factory.py @@ -81,10 +81,12 @@ def generate( for commit in commits: logger.debug("DEBUG count of records is '%s', keys %s", len(records), records.keys()) + logger.debug("DEBUG - checking commit with sha: %s", commit.sha) isolated_r = RecordFactory.__register_commit_to_record(records, repo, commit) if isolated_r is not None: records[commit.sha] = isolated_r + logger.debug("DEBUG count of records is '%s', keys %s", len(records), records.keys()) logger.info( From 3d3c8037260338da5676ddfef9ca628d2ddd254c Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 1 Oct 2024 14:30:00 +0200 Subject: [PATCH 31/51] - Fixes from manual test. --- release_notes_generator/record/record_factory.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/release_notes_generator/record/record_factory.py b/release_notes_generator/record/record_factory.py index f605fb27..02f0852e 100644 --- a/release_notes_generator/record/record_factory.py +++ b/release_notes_generator/record/record_factory.py @@ -85,8 +85,14 @@ def generate( isolated_r = RecordFactory.__register_commit_to_record(records, repo, commit) if isolated_r is not None: + logger.debug("DEBUG - Adding new isolated record to records dict") records[commit.sha] = isolated_r + if commit.sha in records.keys(): + logger.debug("DEBUG - found in keys") + else: + logger.debug("DEBUG - not found in keys") + logger.debug("DEBUG count of records is '%s', keys %s", len(records), records.keys()) logger.info( From ba570b3fed9995bdec534b45f2d321addaf632e9 Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 1 Oct 2024 14:54:09 +0200 Subject: [PATCH 32/51] - Fixes from manual test. --- release_notes_generator/record/record_factory.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/release_notes_generator/record/record_factory.py b/release_notes_generator/record/record_factory.py index 02f0852e..185a355f 100644 --- a/release_notes_generator/record/record_factory.py +++ b/release_notes_generator/record/record_factory.py @@ -92,6 +92,8 @@ def generate( logger.debug("DEBUG - found in keys") else: logger.debug("DEBUG - not found in keys") + else: + logger.debug("DEBUG - Adding normal record to records dict") logger.debug("DEBUG count of records is '%s', keys %s", len(records), records.keys()) From 105fffd99a4a2f5760c2a76059e3359a5d45a1b2 Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 1 Oct 2024 15:08:42 +0200 Subject: [PATCH 33/51] - Fixes from manual test. --- release_notes_generator/record/record_factory.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/release_notes_generator/record/record_factory.py b/release_notes_generator/record/record_factory.py index 185a355f..cc89ad75 100644 --- a/release_notes_generator/record/record_factory.py +++ b/release_notes_generator/record/record_factory.py @@ -80,10 +80,11 @@ def generate( RecordFactory.__register_pull_request(github, records, repo, pull) for commit in commits: - logger.debug("DEBUG count of records is '%s', keys %s", len(records), records.keys()) + logger.debug("DEBUG - Before - count of records is '%s', keys %s", len(records), records.keys()) logger.debug("DEBUG - checking commit with sha: %s", commit.sha) isolated_r = RecordFactory.__register_commit_to_record(records, repo, commit) + logger.debug("DEBUG - isolated record is '%s'", isolated_r) if isolated_r is not None: logger.debug("DEBUG - Adding new isolated record to records dict") records[commit.sha] = isolated_r @@ -95,7 +96,7 @@ def generate( else: logger.debug("DEBUG - Adding normal record to records dict") - logger.debug("DEBUG count of records is '%s', keys %s", len(records), records.keys()) + logger.debug("DEBUG - After - count of records is '%s', keys %s", len(records), records.keys()) logger.info( "Generated %d records from %d issues and %d PRs, with %d commits detected. %d of commits are isolated", From 72609cca174179c3796b5654bbb319126cf08e6b Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 1 Oct 2024 15:21:21 +0200 Subject: [PATCH 34/51] - Fixes from manual test. --- release_notes_generator/record/record_factory.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/release_notes_generator/record/record_factory.py b/release_notes_generator/record/record_factory.py index cc89ad75..f4ea1ca4 100644 --- a/release_notes_generator/record/record_factory.py +++ b/release_notes_generator/record/record_factory.py @@ -151,11 +151,13 @@ def __register_commit_to_record(records: dict[int|str, Record], repo: Repository @param c: The commit to register. @return: TODO """ + # try to register normal commit to existing records for record in records.values(): if record.register_commit(c): return None + # if not registered, create a new isolated record iso_record = IsolatedCommitsRecord(repo) iso_record.register_commit(c) - + logger.debug("DEBUG - Returning isolated record %s", iso_record) return iso_record From d1e4d9a975477bb02a1d0d6887cbb6301272bc1c Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 1 Oct 2024 15:29:07 +0200 Subject: [PATCH 35/51] - Fixes from manual test. --- release_notes_generator/record/record_factory.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/release_notes_generator/record/record_factory.py b/release_notes_generator/record/record_factory.py index f4ea1ca4..6c90436a 100644 --- a/release_notes_generator/record/record_factory.py +++ b/release_notes_generator/record/record_factory.py @@ -158,6 +158,10 @@ def __register_commit_to_record(records: dict[int|str, Record], repo: Repository # if not registered, create a new isolated record iso_record = IsolatedCommitsRecord(repo) - iso_record.register_commit(c) + try: + iso_record.register_commit(c) + except Exception as e: + logger.error("Failed to add record for commit %s: %s", c.sha, str(e)) + logger.debug("DEBUG - Returning isolated record %s", iso_record) return iso_record From 7ced5ad713b333cbbb2d51dce73c61d8b020ae3a Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 1 Oct 2024 15:36:09 +0200 Subject: [PATCH 36/51] - Fixes from manual test. --- .../record/record_factory.py | 119 +++++++++--------- 1 file changed, 58 insertions(+), 61 deletions(-) diff --git a/release_notes_generator/record/record_factory.py b/release_notes_generator/record/record_factory.py index 6c90436a..1420a459 100644 --- a/release_notes_generator/record/record_factory.py +++ b/release_notes_generator/record/record_factory.py @@ -58,6 +58,61 @@ def generate( @param commits: The list of commits. @return: A dictionary of records. """ + def create_record_for_issue(i: Issue): + records[i.number] = Record(repo, i) + logger.debug("Created record for issue %d: %s", i.number, i.title) + + def register_pull_request(pull: PullRequest): + rate_limiter = GithubRateLimiter(github) + safe_call = safe_call_decorator(rate_limiter) + + for parent_issue_number in extract_issue_numbers_from_body(pull): + if parent_issue_number not in records: + logger.warning( + "Detected PR %d linked to issue %d which is not in the list of received issues. " + "Fetching ...", + pull.number, + parent_issue_number, + ) + parent_issue = safe_call(repo.get_issue)(parent_issue_number) + if parent_issue is not None: + create_record_for_issue(parent_issue) + + if parent_issue_number in records: + records[parent_issue_number].register_pull_request(pull) + logger.debug("Registering PR %d: %s to Issue %d", pull.number, pull.title, parent_issue_number) + else: + records[pull.number] = Record(repo) + records[pull.number].register_pull_request(pull) + logger.debug( + "Registering stand-alone PR %d: %s as mentioned Issue %d not found.", + pull.number, + pull.title, + parent_issue_number, + ) + + def register_commit_to_record(c: Commit) -> Optional[IsolatedCommitsRecord]: + """ + Register a commit to a record if the commit is linked to an issue or a PR. + + @param c: The commit to register. + @return: TODO + """ + # try to register normal commit to existing records + for record in records.values(): + if record.register_commit(c): + return None + + # if not registered, create a new isolated record + iso_record = IsolatedCommitsRecord(repo) + try: + iso_record.register_commit(c) + except Exception as e: + logger.error("Failed to add record for commit %s: %s", c.sha, str(e)) + + logger.debug("DEBUG - Returning isolated record %s", iso_record) + return iso_record + records: dict[int|str, Record] = {} pull_numbers = [pull.number for pull in pulls] @@ -66,7 +121,7 @@ def generate( logger.debug("Hello for issue %s", issue) if issue.number not in pull_numbers: logger.debug("Calling create issue for number %s", issue.number) - RecordFactory.__create_record_for_issue(records, repo, issue) + create_record_for_issue(issue) else: logger.debug("Detected pr number %s among issues", issue.number) real_issue_counts -= 1 @@ -77,13 +132,13 @@ def generate( records[pull.number].register_pull_request(pull) logger.debug("Created record for PR %d: %s", pull.number, pull.title) else: - RecordFactory.__register_pull_request(github, records, repo, pull) + register_pull_request(pull) for commit in commits: logger.debug("DEBUG - Before - count of records is '%s', keys %s", len(records), records.keys()) logger.debug("DEBUG - checking commit with sha: %s", commit.sha) - isolated_r = RecordFactory.__register_commit_to_record(records, repo, commit) + isolated_r = register_commit_to_record(commit) logger.debug("DEBUG - isolated record is '%s'", isolated_r) if isolated_r is not None: logger.debug("DEBUG - Adding new isolated record to records dict") @@ -107,61 +162,3 @@ def generate( sum(isinstance(r, IsolatedCommitsRecord) for r in records) ) return records - - @staticmethod - def __create_record_for_issue(records: dict[int|str, Record], r: Repository, i: Issue): - records[i.number] = Record(r, i) - logger.debug("Created record for issue %d: %s", i.number, i.title) - - @staticmethod - def __register_pull_request(github: Github, records: dict[int|str, Record], repo: Repository, pull: PullRequest): - rate_limiter = GithubRateLimiter(github) - safe_call = safe_call_decorator(rate_limiter) - - for parent_issue_number in extract_issue_numbers_from_body(pull): - if parent_issue_number not in records: - logger.warning( - "Detected PR %d linked to issue %d which is not in the list of received issues. " - "Fetching ...", - pull.number, - parent_issue_number, - ) - parent_issue = safe_call(repo.get_issue)(parent_issue_number) - if parent_issue is not None: - RecordFactory.__create_record_for_issue(records, repo, parent_issue) - - if parent_issue_number in records: - records[parent_issue_number].register_pull_request(pull) - logger.debug("Registering PR %d: %s to Issue %d", pull.number, pull.title, parent_issue_number) - else: - records[pull.number] = Record(repo) - records[pull.number].register_pull_request(pull) - logger.debug( - "Registering stand-alone PR %d: %s as mentioned Issue %d not found.", - pull.number, - pull.title, - parent_issue_number, - ) - - @staticmethod - def __register_commit_to_record(records: dict[int|str, Record], repo: Repository, c: Commit) -> Optional[IsolatedCommitsRecord]: - """ - Register a commit to a record if the commit is linked to an issue or a PR. - - @param c: The commit to register. - @return: TODO - """ - # try to register normal commit to existing records - for record in records.values(): - if record.register_commit(c): - return None - - # if not registered, create a new isolated record - iso_record = IsolatedCommitsRecord(repo) - try: - iso_record.register_commit(c) - except Exception as e: - logger.error("Failed to add record for commit %s: %s", c.sha, str(e)) - - logger.debug("DEBUG - Returning isolated record %s", iso_record) - return iso_record From 52ceae9f2e6cc34491e66528725a4fad44ae345f Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 1 Oct 2024 15:42:16 +0200 Subject: [PATCH 37/51] - Fixes from manual test. --- release_notes_generator/record/record_factory.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/release_notes_generator/record/record_factory.py b/release_notes_generator/record/record_factory.py index 1420a459..b2b5f457 100644 --- a/release_notes_generator/record/record_factory.py +++ b/release_notes_generator/record/record_factory.py @@ -106,7 +106,9 @@ def register_commit_to_record(c: Commit) -> Optional[IsolatedCommitsRecord]: # if not registered, create a new isolated record iso_record = IsolatedCommitsRecord(repo) try: + logger.debug("Before call iso_record.register_commit(c)") iso_record.register_commit(c) + logger.debug("After call iso_record.register_commit(c)") except Exception as e: logger.error("Failed to add record for commit %s: %s", c.sha, str(e)) From cd361e7773f162ee48f6e0a926a60e1edba3c616 Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 1 Oct 2024 15:51:10 +0200 Subject: [PATCH 38/51] - Fixes from manual test. --- .../record/record_factory.py | 36 ++++++------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/release_notes_generator/record/record_factory.py b/release_notes_generator/record/record_factory.py index b2b5f457..115b8253 100644 --- a/release_notes_generator/record/record_factory.py +++ b/release_notes_generator/record/record_factory.py @@ -91,30 +91,6 @@ def register_pull_request(pull: PullRequest): parent_issue_number, ) - def register_commit_to_record(c: Commit) -> Optional[IsolatedCommitsRecord]: - """ - Register a commit to a record if the commit is linked to an issue or a PR. - - @param c: The commit to register. - @return: TODO - """ - # try to register normal commit to existing records - for record in records.values(): - if record.register_commit(c): - return None - - # if not registered, create a new isolated record - iso_record = IsolatedCommitsRecord(repo) - try: - logger.debug("Before call iso_record.register_commit(c)") - iso_record.register_commit(c) - logger.debug("After call iso_record.register_commit(c)") - except Exception as e: - logger.error("Failed to add record for commit %s: %s", c.sha, str(e)) - - logger.debug("DEBUG - Returning isolated record %s", iso_record) - return iso_record - records: dict[int|str, Record] = {} pull_numbers = [pull.number for pull in pulls] @@ -140,7 +116,17 @@ def register_commit_to_record(c: Commit) -> Optional[IsolatedCommitsRecord]: logger.debug("DEBUG - Before - count of records is '%s', keys %s", len(records), records.keys()) logger.debug("DEBUG - checking commit with sha: %s", commit.sha) - isolated_r = register_commit_to_record(commit) + normal_record_detected = False + for record in records.values(): + if record.register_commit(commit): + normal_record_detected = True + break + + isolated_r = None + if not normal_record_detected: + isolated_r = IsolatedCommitsRecord(repo) + isolated_r.register_commit(commit) + logger.debug("DEBUG - isolated record is '%s'", isolated_r) if isolated_r is not None: logger.debug("DEBUG - Adding new isolated record to records dict") From d32f425940372fb2ef1a6e45a9590e81db81a0dc Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 1 Oct 2024 16:04:08 +0200 Subject: [PATCH 39/51] - Fixes from manual test. --- .../record/record_factory.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/release_notes_generator/record/record_factory.py b/release_notes_generator/record/record_factory.py index 115b8253..e8f16666 100644 --- a/release_notes_generator/record/record_factory.py +++ b/release_notes_generator/record/record_factory.py @@ -113,8 +113,8 @@ def register_pull_request(pull: PullRequest): register_pull_request(pull) for commit in commits: - logger.debug("DEBUG - Before - count of records is '%s', keys %s", len(records), records.keys()) - logger.debug("DEBUG - checking commit with sha: %s", commit.sha) + logger.debug("DEBUG 1 - Before - count of records is '%s', keys %s", len(records), records.keys()) + logger.debug("DEBUG 2 - checking commit with sha: %s", commit.sha) normal_record_detected = False for record in records.values(): @@ -124,22 +124,25 @@ def register_pull_request(pull: PullRequest): isolated_r = None if not normal_record_detected: + logger.debug("DEBUG 3") isolated_r = IsolatedCommitsRecord(repo) + logger.debug("DEBUG 4") isolated_r.register_commit(commit) + logger.debug("DEBUG 5") - logger.debug("DEBUG - isolated record is '%s'", isolated_r) + logger.debug("DEBUG 6 - isolated record is '%s'", isolated_r) if isolated_r is not None: - logger.debug("DEBUG - Adding new isolated record to records dict") + logger.debug("DEBUG 7.1 - Adding new isolated record to records dict") records[commit.sha] = isolated_r if commit.sha in records.keys(): - logger.debug("DEBUG - found in keys") + logger.debug("DEBUG 8.1 - found in keys") else: - logger.debug("DEBUG - not found in keys") + logger.debug("DEBUG 8.2 - not found in keys") else: - logger.debug("DEBUG - Adding normal record to records dict") + logger.debug("DEBUG 7.2 - Adding normal record to records dict") - logger.debug("DEBUG - After - count of records is '%s', keys %s", len(records), records.keys()) + logger.debug("DEBUG 9 - After - count of records is '%s', keys %s", len(records), records.keys()) logger.info( "Generated %d records from %d issues and %d PRs, with %d commits detected. %d of commits are isolated", From 9ea0f4844189e418b0ff6c5b63c34a2e245c185b Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 1 Oct 2024 16:10:30 +0200 Subject: [PATCH 40/51] - Fixes from manual test. --- release_notes_generator/record/record_factory.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/release_notes_generator/record/record_factory.py b/release_notes_generator/record/record_factory.py index e8f16666..cefc84aa 100644 --- a/release_notes_generator/record/record_factory.py +++ b/release_notes_generator/record/record_factory.py @@ -92,6 +92,7 @@ def register_pull_request(pull: PullRequest): ) records: dict[int|str, Record] = {} + records_for_isolated_commits: dict[int|str, Record] = {} pull_numbers = [pull.number for pull in pulls] real_issue_counts = len(issues) # issues could contain PRs too - known behaviour from API @@ -129,11 +130,13 @@ def register_pull_request(pull: PullRequest): logger.debug("DEBUG 4") isolated_r.register_commit(commit) logger.debug("DEBUG 5") + else: + logger.debug("DEBUG 5.5") logger.debug("DEBUG 6 - isolated record is '%s'", isolated_r) if isolated_r is not None: logger.debug("DEBUG 7.1 - Adding new isolated record to records dict") - records[commit.sha] = isolated_r + records_for_isolated_commits[commit.sha] = isolated_r if commit.sha in records.keys(): logger.debug("DEBUG 8.1 - found in keys") @@ -144,6 +147,7 @@ def register_pull_request(pull: PullRequest): logger.debug("DEBUG 9 - After - count of records is '%s', keys %s", len(records), records.keys()) + records.update(records_for_isolated_commits) logger.info( "Generated %d records from %d issues and %d PRs, with %d commits detected. %d of commits are isolated", len(records), From 7bc3eb6c6e1fd23f78bce0023cd7e8059518f3fb Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 1 Oct 2024 16:22:14 +0200 Subject: [PATCH 41/51] - Fixes from manual test. --- .../model/service_chapters.py | 6 ++--- .../record/record_factory.py | 24 ++----------------- release_notes_generator/utils/constants.py | 2 +- 3 files changed, 6 insertions(+), 26 deletions(-) diff --git a/release_notes_generator/model/service_chapters.py b/release_notes_generator/model/service_chapters.py index ef7f41be..66b01587 100644 --- a/release_notes_generator/model/service_chapters.py +++ b/release_notes_generator/model/service_chapters.py @@ -59,9 +59,6 @@ def __init__( self.used_record_numbers = used_record_numbers self.chapters = { - ISOLATED_COMMITS: Chapter( - title=ISOLATED_COMMITS, empty_message="All commits are linked to an Issues or a Pull Request." - ), CLOSED_ISSUES_WITHOUT_PULL_REQUESTS: Chapter( title=CLOSED_ISSUES_WITHOUT_PULL_REQUESTS, empty_message="All closed issues linked to a Pull Request." ), @@ -81,6 +78,9 @@ def __init__( title=MERGED_PRS_LINKED_TO_NOT_CLOSED_ISSUES, empty_message="All merged PRs are linked to Closed issues.", ), + ISOLATED_COMMITS: Chapter( + title=ISOLATED_COMMITS, empty_message="All commits are linked to an Issues or a Pull Request." + ), OTHERS_NO_TOPIC: Chapter( title=OTHERS_NO_TOPIC, empty_message="Previous filters caught all Issues or Pull Requests." ), diff --git a/release_notes_generator/record/record_factory.py b/release_notes_generator/record/record_factory.py index cefc84aa..d517b1a0 100644 --- a/release_notes_generator/record/record_factory.py +++ b/release_notes_generator/record/record_factory.py @@ -97,7 +97,6 @@ def register_pull_request(pull: PullRequest): real_issue_counts = len(issues) # issues could contain PRs too - known behaviour from API for issue in issues: - logger.debug("Hello for issue %s", issue) if issue.number not in pull_numbers: logger.debug("Calling create issue for number %s", issue.number) create_record_for_issue(issue) @@ -114,9 +113,6 @@ def register_pull_request(pull: PullRequest): register_pull_request(pull) for commit in commits: - logger.debug("DEBUG 1 - Before - count of records is '%s', keys %s", len(records), records.keys()) - logger.debug("DEBUG 2 - checking commit with sha: %s", commit.sha) - normal_record_detected = False for record in records.values(): if record.register_commit(commit): @@ -125,35 +121,19 @@ def register_pull_request(pull: PullRequest): isolated_r = None if not normal_record_detected: - logger.debug("DEBUG 3") isolated_r = IsolatedCommitsRecord(repo) - logger.debug("DEBUG 4") isolated_r.register_commit(commit) - logger.debug("DEBUG 5") - else: - logger.debug("DEBUG 5.5") - logger.debug("DEBUG 6 - isolated record is '%s'", isolated_r) if isolated_r is not None: - logger.debug("DEBUG 7.1 - Adding new isolated record to records dict") records_for_isolated_commits[commit.sha] = isolated_r - if commit.sha in records.keys(): - logger.debug("DEBUG 8.1 - found in keys") - else: - logger.debug("DEBUG 8.2 - not found in keys") - else: - logger.debug("DEBUG 7.2 - Adding normal record to records dict") - - logger.debug("DEBUG 9 - After - count of records is '%s', keys %s", len(records), records.keys()) - records.update(records_for_isolated_commits) logger.info( - "Generated %d records from %d issues and %d PRs, with %d commits detected. %d of commits are isolated", + "Generated %d records from %d issues and %d PRs, with %d commits detected. %d of commits are isolated.", len(records), real_issue_counts, len(pulls), len(commits), - sum(isinstance(r, IsolatedCommitsRecord) for r in records) + len(records_for_isolated_commits) ) return records diff --git a/release_notes_generator/utils/constants.py b/release_notes_generator/utils/constants.py index 69086f27..11391342 100644 --- a/release_notes_generator/utils/constants.py +++ b/release_notes_generator/utils/constants.py @@ -53,7 +53,7 @@ RELEASE_NOTE_LINE_MARK = "-" # Service chapters titles -ISOLATED_COMMITS: str = "Isolated commits without Issue ot PR ⚠️" +ISOLATED_COMMITS: str = "Isolated commits without Issue or PR ⚠️" CLOSED_ISSUES_WITHOUT_PULL_REQUESTS: str = "Closed Issues without Pull Request ⚠️" CLOSED_ISSUES_WITHOUT_USER_DEFINED_LABELS: str = "Closed Issues without User Defined Labels ⚠️" From bdc18729af1342475d1e802d82c6f1dbd7ba2a70 Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Fri, 4 Oct 2024 10:53:14 +0200 Subject: [PATCH 42/51] - Implemented support for developers and contributors. --- .../model/isolated_commits_record.py | 38 +++++-- release_notes_generator/model/record.py | 100 ++++++++++++------ .../record/record_factory.py | 53 ++++++---- 3 files changed, 126 insertions(+), 65 deletions(-) diff --git a/release_notes_generator/model/isolated_commits_record.py b/release_notes_generator/model/isolated_commits_record.py index 29160201..382e520a 100644 --- a/release_notes_generator/model/isolated_commits_record.py +++ b/release_notes_generator/model/isolated_commits_record.py @@ -42,9 +42,9 @@ class IsolatedCommitsRecord(Record): The record holds isolated commit without link to Issue or Pull request. """ - def __init__(self, repo: Repository): - super().__init__(repo) - self.__commits: dict = {} + def __init__(self, repo: Repository, safe_call): + super().__init__(repo, safe_call) + self.__commit: Optional[Commit] = None @property def number(self) -> int: @@ -52,8 +52,8 @@ def number(self) -> int: return sys.maxsize @property - def commits(self) -> dict: - return self.__commits + def commit(self) -> Commit: + return self.__commit @property def is_pr(self) -> bool: @@ -98,11 +98,19 @@ def assignees(self) -> Optional[str]: @property def developers(self) -> Optional[str]: - return None + return f"{self.commit.author.login}" @property def contributors(self) -> Optional[str]: - return None + logins = set() + commit_comments = list(self._safe_call(self.commit.get_comments)()) + + for commit_comment in commit_comments: + for line in commit_comment.body.split("\n"): + if "Co-authored-by:" in line: + logins.add(commit_comment.split("Co-authored-by:")[1].strip()) + + return ", ".join(logins) if len(logins) > 0 else None @property def pr_links(self) -> Optional[str]: @@ -124,19 +132,26 @@ def register_commit(self, commit: Commit) -> bool: @param commit: The Commit object to register. @return: Always return True. """ - self.__commits[0] = commit + self.__commit = commit logger.debug("Registering commit 'type: Isolated' sha: %s", commit.sha) return True def to_chapter_row(self) -> str: """ - Converts the record to a string row in a chapter. + Converts the record to a string row in a chapter. @return: The record as a row string. """ self.increment_present_in_chapters() row_prefix = f"{ActionInputs.get_duplicity_icon()} " if self.present_in_chapters() > 1 else "" - return f"{row_prefix}Commit: {self.commits[0].sha}" + + row = f"{row_prefix}Commit: {self.commit.sha} developed by {self.developers}" + + contributors = self.contributors + if contributors: + row += f" co-authored by {contributors}." + + return row.replace(" ", " ") def __get_row_format_values(self, row_format: str) -> dict: return {} @@ -150,3 +165,6 @@ def contain_all_labels(self, labels: list[str]) -> bool: @staticmethod def is_pull_request_merged(pull: PullRequest) -> bool: return False + + def get_commits(self): + pass diff --git a/release_notes_generator/model/record.py b/release_notes_generator/model/record.py index a3b62e08..5cc50aed 100644 --- a/release_notes_generator/model/record.py +++ b/release_notes_generator/model/record.py @@ -19,8 +19,6 @@ """ import logging -import re -import sys from typing import Optional from github.Issue import Issue @@ -48,15 +46,17 @@ class Record: A class used to represent a record in the release notes. """ - def __init__(self, repo: Repository, issue: Optional[Issue] = None): + def __init__(self, repo: Repository, safe_call, issue: Optional[Issue] = None): self.__repo: Repository = repo self.__gh_issue: Issue = issue self.__pulls: list[PullRequest] = [] - self.__pull_commits: dict = {} + self.__pull_commits: dict[int, list[Commit]] = {} self.__is_release_note_detected: bool = False self.__present_in_chapters = 0 + self._safe_call = safe_call + @property def number(self) -> int: """Getter for the number of the record.""" @@ -75,7 +75,7 @@ def pulls(self) -> list[PullRequest]: return self.__pulls @property - def commits(self) -> dict: + def commits(self) -> dict[int, list[Commit]]: """Getter for the commits of the record.""" return self.__pull_commits @@ -205,19 +205,47 @@ def assignees(self) -> Optional[str]: @property def developers(self) -> Optional[str]: """Getter for the developers of the record.""" - # TODO - cycle across record commits and collect PR "authors" - # Commit.author - by mel byt ten, kdo ten PR pripravil, commiter je ten kdo s nim naposledy pracoval + if self.is_issue: + # is issue - go for its pulls + logins = set() + for commits in self.__pull_commits.values(): + for commit in commits: + if commit.author is not None: + logins.add(f"@{commit.author.login}") + + if not logins: + logger.warning("Found issue record %s with %d pull requests and no commits", self.number, len(self.__pulls)) + return ", ".join(logins) if len(logins) > 0 else None - return None + elif self.is_pr: + # is pr - go for its one pull only + logins = {f"@{c.author.login}" for c in self.__pull_commits[self.pulls[0].number]} + if not logins: + logger.warning("Found pull request record '%s' with no commits", self.pulls[0].number) + return ", ".join(logins) if len(logins) > 0 else None + + else: + logger.warning("Record '%s' is not issue nor PR. Developers cannot be determined.", self.number) + return None @property def contributors(self) -> Optional[str]: """Getter for the contributors of the record.""" - # TODO - cycle across record commits and check if contribution string is present in commit message - # - # Co-authored-by - # check if extra API calls are here - 100% jsou tam - udelat test na generovani s spoptrebe API + varovani do README - return None + + if not self.is_issue and not self.is_pr: + logger.warning("Record '%s' is not issue nor PR. Contributors cannot be determined.", self.number) + return None + + logins = [] + for commits in self.commits.values(): + for c in commits: + for line in c.commit.message.split("\n"): + if "Co-authored-by:" in line: + name = line.split("Co-authored-by:")[1].strip() + if name not in logins: + logins.append(name) + + return ", ".join(logins) if len(logins) > 0 else None @property def pr_links(self) -> Optional[str]: @@ -273,8 +301,8 @@ def register_commit(self, commit: Commit) -> bool: @param commit: The Commit object to register. @return: None """ + sha = commit.sha for pull in self.__pulls: - sha = commit.sha if sha == pull.merge_commit_sha or sha == pull.head.sha: if self.__pull_commits.get(pull.number) is None: self.__pull_commits[pull.number] = [] @@ -282,19 +310,6 @@ def register_commit(self, commit: Commit) -> bool: logger.debug("Commit %s registered using sha in PR %s of record %s", commit.sha, pull.number, self.number) return True - # Parse commit message for PR numbers - message = commit.commit.message - match = re.search(r"Merge pull request #(\d+)", message) - if not match: - match = re.search(r"\(#(\d+)\)", message) - if match: - pr_number = int(match.group(1)) - if self.__pull_commits.get(pr_number) is None: - self.__pull_commits[pr_number] = [] - self.__pull_commits[pr_number].append(commit) - logger.debug("Commit %s registered using message in PR %s of record %s", commit.sha, pr_number, self.number) - return True - return False def to_chapter_row(self) -> str: @@ -327,7 +342,7 @@ def to_chapter_row(self) -> str: if self.contains_release_notes: row = f"{row}\n{self.get_rls_notes()}" - return row + return row.replace(" ", " ") def __get_row_format_values(self, row_format: str) -> dict: """ @@ -340,13 +355,17 @@ def __get_row_format_values(self, row_format: str) -> dict: format_values = {} if "{assignee}" in row_format: - format_values["assignee"] = f"assigned to @{self.assignee}" if self.assignee is not None else "" + assignee = self.assignees + format_values["assignee"] = f"assigned to @{assignee}" if assignee is not None else "" if "{assignees}" in row_format: - format_values["assignees"] = f"assigned to @{self.assignees}" if self.assignees is not None else "" + assignees = self.assignees + format_values["assignees"] = f"assigned to @{assignees}" if assignees is not None else "" if "{developed-by}" in row_format: - format_values["developed-by"] = f"developed by {self.developers}" if self.developers is not None else "" + developers = self.developers + format_values["developed-by"] = f"developed by {developers}" if developers is not None else "" if "{co-authored-by}" in row_format: - format_values["co-authored-by"] = f"co-authored by {self.contributors}" if self.contributors is not None else "" + contributors = self.contributors + format_values["co-authored-by"] = f"co-authored by {contributors}" if contributors is not None else "" return format_values @@ -403,3 +422,20 @@ def is_pull_request_merged(pull: PullRequest) -> bool: @return: A boolean indicating whether the pull request is merged. """ return pull.state == PR_STATE_CLOSED and pull.merged_at is not None and pull.closed_at is not None + + def get_commits(self) -> None: + for pull in self.__pulls: + self.__pull_commits[pull.number] = list(self._safe_call(pull.get_commits)()) + + def get_commits_shas(self) -> set[str]: + set_of_commit_shas = set() + + for commits in self.__pull_commits.values(): + for c in commits: + set_of_commit_shas.add(c.sha) + set_of_commit_shas.add(c.commit.sha) + + for pull in self.__pulls: + set_of_commit_shas.add(pull.merge_commit_sha) + + return set_of_commit_shas diff --git a/release_notes_generator/record/record_factory.py b/release_notes_generator/record/record_factory.py index d517b1a0..8d42a4bb 100644 --- a/release_notes_generator/record/record_factory.py +++ b/release_notes_generator/record/record_factory.py @@ -20,8 +20,6 @@ import logging -from typing import Optional - from github import Github from github.Issue import Issue from github.PullRequest import PullRequest @@ -37,6 +35,7 @@ logger = logging.getLogger(__name__) +# TODO - make record types a classes # pylint: disable=too-few-public-methods class RecordFactory: @@ -55,17 +54,17 @@ def generate( @param repo: The repository. @param issues: The list of issues. @param pulls: The list of pull requests. - @param commits: The list of commits. + @param commits: The list of commits with no links to PRs! @return: A dictionary of records. """ + rate_limiter = GithubRateLimiter(github) + safe_call = safe_call_decorator(rate_limiter) + def create_record_for_issue(i: Issue): - records[i.number] = Record(repo, i) + records[i.number] = Record(repo, safe_call, i) logger.debug("Created record for issue %d: %s", i.number, i.title) def register_pull_request(pull: PullRequest): - rate_limiter = GithubRateLimiter(github) - safe_call = safe_call_decorator(rate_limiter) - for parent_issue_number in extract_issue_numbers_from_body(pull): if parent_issue_number not in records: logger.warning( @@ -82,7 +81,7 @@ def register_pull_request(pull: PullRequest): records[parent_issue_number].register_pull_request(pull) logger.debug("Registering PR %d: %s to Issue %d", pull.number, pull.title, parent_issue_number) else: - records[pull.number] = Record(repo) + records[pull.number] = Record(repo, safe_call) records[pull.number].register_pull_request(pull) logger.debug( "Registering stand-alone PR %d: %s as mentioned Issue %d not found.", @@ -95,6 +94,7 @@ def register_pull_request(pull: PullRequest): records_for_isolated_commits: dict[int|str, Record] = {} pull_numbers = [pull.number for pull in pulls] + logger.debug("Creating records from issue.") real_issue_counts = len(issues) # issues could contain PRs too - known behaviour from API for issue in issues: if issue.number not in pull_numbers: @@ -104,28 +104,35 @@ def register_pull_request(pull: PullRequest): logger.debug("Detected pr number %s among issues", issue.number) real_issue_counts -= 1 + logger.debug("Creating records from Pull Requests.") for pull in pulls: if not extract_issue_numbers_from_body(pull): - records[pull.number] = Record(repo) + records[pull.number] = Record(repo, safe_call) records[pull.number].register_pull_request(pull) logger.debug("Created record for PR %d: %s", pull.number, pull.title) else: register_pull_request(pull) - for commit in commits: - normal_record_detected = False - for record in records.values(): - if record.register_commit(commit): - normal_record_detected = True - break - - isolated_r = None - if not normal_record_detected: - isolated_r = IsolatedCommitsRecord(repo) - isolated_r.register_commit(commit) - - if isolated_r is not None: - records_for_isolated_commits[commit.sha] = isolated_r + logger.debug("Registering commits to Pull Requests.") + """Why are commits needed: + - identify developers - as commit authors + - identify contributors - as co-authors in commit message + - identify direct commits (no PRs related) + """ + # cycle across all record's PRs & ask for their commits + for record in records.values(): + record.get_commits() + + # cycle across all record's PR's commits & identify direct commits + linked_commits_by_sha: set[str] = set() + for r in records.values(): + linked_commits_by_sha.update(r.get_commits_shas()) + + for c in commits: + if c.sha not in linked_commits_by_sha: + isolated_record = IsolatedCommitsRecord(repo, safe_call) + isolated_record.register_commit(c) + records_for_isolated_commits[c.sha] = isolated_record records.update(records_for_isolated_commits) logger.info( From 3285283aab9dc84ff63d69829e8506c1ac79176a Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Mon, 7 Oct 2024 10:01:46 +0200 Subject: [PATCH 43/51] - The Record class logic split to 4 files. --- README.md | 23 +- release_notes_generator/builder.py | 2 +- release_notes_generator/generator.py | 2 +- .../model/base_chapters.py | 2 +- release_notes_generator/model/base_record.py | 327 +++++++++++++ ...ted_commits_record.py => commit_record.py} | 120 ++--- .../model/custom_chapters.py | 4 +- release_notes_generator/model/issue_record.py | 197 ++++++++ .../model/pull_request_record.py | 177 +++++++ release_notes_generator/model/record.py | 441 ------------------ .../model/service_chapters.py | 22 +- .../record/record_factory.py | 20 +- tests/conftest.py | 30 +- 13 files changed, 803 insertions(+), 564 deletions(-) create mode 100644 release_notes_generator/model/base_record.py rename release_notes_generator/model/{isolated_commits_record.py => commit_record.py} (55%) create mode 100644 release_notes_generator/model/issue_record.py create mode 100644 release_notes_generator/model/pull_request_record.py delete mode 100644 release_notes_generator/model/record.py diff --git a/README.md b/README.md index 360d6620..52fc3766 100644 --- a/README.md +++ b/README.md @@ -173,23 +173,20 @@ Add the following step to your GitHub workflow (in example are used non-default ## Features ### Built-in -#### Release Notes Extraction Process +#### Release Notes Support +This action enables GitHub pull requests to include a dedicated section for release notes, making it easier for maintainers to track changes and updates. -This action requires that your GitHub issues include comments with specific release notes. Here's how it works: - -**Extraction Method**: -- The action scans through comments on each closed issue since the last release. It identifies comments that follow the specified format and extracts the content as part of the release notes. -- The time considered for the previous release is based on its creation time. This means that the action will look for issues closed after the creation time of the most recent release to ensure that all relevant updates since that release are included. - -**Release Notes Comment Format** -- It must contain a comment starting with "Release Notes" followed by the note content. This comment is typically added by the contributors. -- Here is an example of the content for a 'Release Notes' string, which is not case-sensitive: +- **Format:** The section must begin with the title `Release Notes:`, followed by the release notes in bullet points. +- **Example:** Here is an example of how to structure the release notes (case-sensitive): ``` -Release Notes +Release Notes: - This update introduces a new caching mechanism that improves performance by 20%. ``` -- Using `-` as a bullet point for each note is the best practice. The Markdown parser will automatically convert it to a list. -- These comments are not required for action functionality. If an issue does not contain a "Release Notes" comment, it will be marked accordingly in the release notes. This helps maintainers quickly identify which issues need attention for documentation. +- **Best Practice:** Use `-` for bullet points. The Markdown parser will automatically format them as a list. +- **Optional:** Including release notes is not mandatory for the action to function. If a pull request does not include a Release Notes: section, it will be flagged accordingly, helping maintainers identify which PRs require documentation updates. + +The action scans pull request descriptions for the `Release Notes:` section and extracts any content that follows the specified format. +Additionally, the action tracks issues closed after the most recent release, using the release creation time as a reference point. This ensures that all relevant updates since the last release are captured. #### Handling Multiple PRs If an issue is linked to multiple PRs, the action fetches and aggregates contributions from all linked PRs. diff --git a/release_notes_generator/builder.py b/release_notes_generator/builder.py index 37df7cad..e65fe62e 100644 --- a/release_notes_generator/builder.py +++ b/release_notes_generator/builder.py @@ -22,7 +22,7 @@ from itertools import chain from release_notes_generator.model.custom_chapters import CustomChapters -from release_notes_generator.model.record import Record +from release_notes_generator.model.base_record import Record from release_notes_generator.model.service_chapters import ServiceChapters from release_notes_generator.action_inputs import ActionInputs diff --git a/release_notes_generator/generator.py b/release_notes_generator/generator.py index bd1dc993..dc98d413 100644 --- a/release_notes_generator/generator.py +++ b/release_notes_generator/generator.py @@ -25,7 +25,7 @@ from github import Github from release_notes_generator.model.custom_chapters import CustomChapters -from release_notes_generator.model.record import Record +from release_notes_generator.model.base_record import Record from release_notes_generator.builder import ReleaseNotesBuilder from release_notes_generator.record.record_factory import RecordFactory from release_notes_generator.action_inputs import ActionInputs diff --git a/release_notes_generator/model/base_chapters.py b/release_notes_generator/model/base_chapters.py index fd3514a6..79007d0f 100644 --- a/release_notes_generator/model/base_chapters.py +++ b/release_notes_generator/model/base_chapters.py @@ -20,7 +20,7 @@ from abc import ABC, abstractmethod from release_notes_generator.model.chapter import Chapter -from release_notes_generator.model.record import Record +from release_notes_generator.model.base_record import Record class BaseChapters(ABC): diff --git a/release_notes_generator/model/base_record.py b/release_notes_generator/model/base_record.py new file mode 100644 index 00000000..234d3d4e --- /dev/null +++ b/release_notes_generator/model/base_record.py @@ -0,0 +1,327 @@ +# +# Copyright 2023 ABSA Group Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +""" +This module contains the BaseChapters class which is responsible for representing the base chapters. +""" + +import logging +from abc import ABC, abstractmethod +from typing import Optional + +from github.Issue import Issue +from github.PullRequest import PullRequest +from github.Repository import Repository +from github.Commit import Commit + +from release_notes_generator.utils.constants import ( + RELEASE_NOTE_DETECTION_PATTERN, + RELEASE_NOTE_LINE_MARK, PR_STATE_CLOSED, +) + +logger = logging.getLogger(__name__) + + +class Record(ABC): + """ + A class used to represent a record in the release notes. + """ + + LINK_TO_PR_TEMPLATE = "[#{number}](https://github.com/{full_name}/pull/{number})" + + def __init__(self, repo: Repository, safe_call): + self._repo: Repository = repo + self._safe_call = safe_call + + self.__is_release_note_detected: bool = False + self.__present_in_chapters = 0 + + @property + def is_present_in_chapters(self) -> bool: + """Check if the record is present in chapters.""" + return self.__present_in_chapters > 0 + + @property + def present_in_chapters(self) -> int: + """Gets the count of chapters in which the record is present.""" + return self.__present_in_chapters + + @property + @abstractmethod + def id(self) -> Optional[int | str]: + """Get the id of the record.""" + pass + + @property + @abstractmethod + def issue(self) -> Optional[Issue]: + """Get the record's issue.""" + pass + + @property + @abstractmethod + def pull_requests(self) -> list[PullRequest]: + """Get the record's pull requests.""" + pass + + @property + @abstractmethod + def commits(self) -> list[Commit]: + """Get the record's commits.""" + pass + + @property + @abstractmethod + def labels(self) -> list[str]: + """Get labels of the record.""" + pass + + # Note: assignee & assignees are related to this GitHub Docs - https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/assigning-issues-and-pull-requests-to-other-github-users#about-issue-and-pull-request-assignees + @property + @abstractmethod + def assignee(self) -> Optional[str]: + """Get record's assignee.""" + pass + + @property + @abstractmethod + def assignees(self) -> Optional[str]: + """Get record's assignees.""" + pass + + @property + @abstractmethod + def developers(self) -> Optional[str]: + """Get record's developers.""" + pass + + @property + @abstractmethod + def contributors(self) -> Optional[str]: + """Get record's contributors.""" + pass + + @abstractmethod + def pr_links(self) -> Optional[str]: + """Get links to record's pull requests.""" + pass + + @abstractmethod + def pr_contains_issue_mentions(self) -> bool: + """Checks if the record's pull requests contains issue mentions.""" + pass + + @abstractmethod + def is_state(self, state:str) -> bool: + """Check if the record's state is the specified state.""" + pass + + # TODO - review of all method doc strings - same format, return values + @abstractmethod + def register_pull_request(self, pr: PullRequest) -> None: + """ + Registers a pull request with the record. + + @param pr: The PullRequest object to register. + @return: None + """ + pass + + @abstractmethod + def register_commit(self, commit: Commit) -> bool: + """ + Registers a commit with the record. + + @param commit: The Commit object to register. + @return: True if record is registered and valid for one of record's pull requests, False otherwise. + """ + pass + + @abstractmethod + def to_chapter_row(self) -> str: + """ + Converts the record to a string row usable in a chapter. + + @return: The record as a row string. + """ + pass + + @abstractmethod + def fetch_pr_commits(self) -> None: + pass + + @abstractmethod + def get_sha_of_all_commits(self) -> set[str]: + pass + + # TODO - remove after fix of unit tests + # @abstractmethod + # def count_of_commits(self) -> int: + # """ + # Get count of commits in all record pull requests. + # + # @return: The count of commits in the records. + # """ + # pass + + # TODO - remove after fix of unit tests + # @abstractmethod + # def pull_request_by_number(self, pr_number: int) -> Optional[PullRequest]: + # """ + # Gets a pull request associated with the record. + # + # @param index: The index of the pull request. + # @return: The PullRequest instance. + # """ + # if index < 0 or index >= len(self.__pulls): + # return None + # return self.__pulls[index] + + @staticmethod + def get_contributors_for_commit(commit: Commit) -> list[str]: + """ + Gets Contributors from commit message. + + @param commit: The commit to get contributors from. + @return: A list of contributors. + """ + + logins = [] + for line in commit.commit.message.split("\n"): + if "Co-authored-by:" in line: + name = line.split("Co-authored-by:")[1].strip() + if name not in logins: + logins.append(name) + + return logins + + @staticmethod + def is_pull_request_merged(pull: PullRequest) -> bool: + """ + Checks if the pull request is merged. + + @param pull: The pull request to check. + @return: A boolean indicating whether the pull request is merged. + """ + return pull.state == PR_STATE_CLOSED and pull.merged_at is not None and pull.closed_at is not None + + def increment_present_in_chapters(self) -> None: + """ + Increments the count of chapters in which the record is present. + + @return: None + """ + self.__present_in_chapters += 1 + + # TODO in Issue named 'Configurable regex-based Release note detection in the PR body' + # - 'Release notest:' as detection pattern default - can be defined by user + # - '-' as leading line mark for each release note to be used + def get_rls_notes(self, detection_pattern=RELEASE_NOTE_DETECTION_PATTERN, line_mark=RELEASE_NOTE_LINE_MARK) -> str: + """ + Gets the release notes of the record. + + @param detection_pattern: The detection pattern to use. + @param line_mark: The line mark to use. + @return: The release notes of the record as a string. + """ + release_notes = "" + + # Iterate over all PRs + for pull in self.pull_requests: + body_lines = pull.body.split("\n") if pull.body is not None else [] + inside_release_notes = False + + for line in body_lines: + if detection_pattern in line: + logger.debug("Hello rls notes gen - pr.number %s, line: %s, detection_pattern: %s", pull.number, line, detection_pattern) + inside_release_notes = True + + if detection_pattern not in line and inside_release_notes: + if line.startswith(line_mark): + logger.debug("Hello rls notes gen - new line: %s", line) + release_notes += f" {line.strip()}\n" + else: + break + + # Return the concatenated release notes + return release_notes.rstrip() + + def contains_release_notes(self) -> bool: + """Checks if the record contains release notes.""" + if self.__is_release_note_detected: + return self.__is_release_note_detected + + rls_notes = self.get_rls_notes() + # if RELEASE_NOTE_LINE_MARK in self.get_rls_notes(): + if RELEASE_NOTE_LINE_MARK in rls_notes: + logger.debug("DEBUG: Detected rls notes in value: %s", rls_notes) + self.__is_release_note_detected = True + + return self.__is_release_note_detected + + def contains_min_one_label(self, input_labels: list[str]) -> bool: + """ + Check if the record contains at least one of the specified labels. + + @param input_labels: A list of labels to check for. + @return: A boolean indicating whether the record contains any of the specified labels. + """ + for lbl in self.labels: + if lbl in input_labels: + return True + + return False + + def contain_all_labels(self, input_labels: list[str]) -> bool: + """ + Check if the record contains all the specified labels. + + @param input_labels: A list of labels to check for. + @return: A boolean indicating whether the record contains all the specified + """ + if len(self.labels) != len(input_labels): + return False + + for lbl in self.labels: + if lbl not in input_labels: + return False + + return True + + def _get_row_format_values(self, row_format: str) -> dict: + """ + Create dictionary and fill by user row format defined values. + NoteL some values are API call intensive. + + @param row_format: User defined row format. + @return: The dictionary with supported values required by user row format. + """ + format_values = {} + + if "{assignee}" in row_format: + assignee = self.assignees + format_values["assignee"] = f"assigned to @{assignee}" if assignee is not None else "" + if "{assignees}" in row_format: + assignees = self.assignees + format_values["assignees"] = f"assigned to @{assignees}" if assignees is not None else "" + if "{developed-by}" in row_format: + developers = self.developers + format_values["developed-by"] = f"developed by {developers}" if developers is not None else "" + if "{co-authored-by}" in row_format: + contributors = self.contributors + format_values["co-authored-by"] = f"co-authored by {contributors}" if contributors is not None else "" + + return format_values diff --git a/release_notes_generator/model/isolated_commits_record.py b/release_notes_generator/model/commit_record.py similarity index 55% rename from release_notes_generator/model/isolated_commits_record.py rename to release_notes_generator/model/commit_record.py index 382e520a..b5bfb7ef 100644 --- a/release_notes_generator/model/isolated_commits_record.py +++ b/release_notes_generator/model/commit_record.py @@ -19,110 +19,97 @@ """ import logging -import sys from typing import Optional +from github.Issue import Issue from github.PullRequest import PullRequest from github.Repository import Repository from github.Commit import Commit from release_notes_generator.action_inputs import ActionInputs -from release_notes_generator.model.record import Record +from release_notes_generator.model.base_record import Record from release_notes_generator.utils.constants import ( - RELEASE_NOTE_DETECTION_PATTERN, - RELEASE_NOTE_LINE_MARK, + RELEASE_NOTE_DETECTION_PATTERN, RELEASE_NOTE_LINE_MARK, ) logger = logging.getLogger(__name__) -class IsolatedCommitsRecord(Record): +class CommitRecord(Record): """ A class used to represent a record in the release notes. - The record holds isolated commit without link to Issue or Pull request. + The record represent an isolated commit without link to Issue or Pull request. Direct commit to master. """ def __init__(self, repo: Repository, safe_call): super().__init__(repo, safe_call) - self.__commit: Optional[Commit] = None - - @property - def number(self) -> int: - """Getter for the number of the record.""" - return sys.maxsize - @property - def commit(self) -> Commit: - return self.__commit + self.__commit: Optional[Commit] = None @property - def is_pr(self) -> bool: - """Check if the record is a pull request.""" - return False + def issue(self) -> Optional[Issue]: + """Get the issue of the record.""" + return None @property - def is_issue(self) -> bool: - """Check if the record is an issue.""" - return False + def pull_requests(self) -> list[PullRequest]: + """Get the pull requests of the record.""" + return [] @property - def is_closed(self) -> bool: - return False + def commits(self) -> list[Commit]: + """Get the commits of the record.""" + return [self.__commit] if self.__commit is not None else [] @property - def is_merged_pr(self) -> bool: - return False + def id(self) -> Optional[int | str]: + """Get the id of the record.""" + return self.__commit.sha if self.__commit is not None else None @property def labels(self) -> list[str]: + """Get the labels of the record.""" return [] - def get_rls_notes(self, detection_pattern=RELEASE_NOTE_DETECTION_PATTERN, line_mark=RELEASE_NOTE_LINE_MARK) -> str: - return "" - - @property - def contains_release_notes(self) -> bool: - return False - - @property - def pr_contains_issue_mentions(self) -> bool: - return False - @property def assignee(self) -> Optional[str]: - return None + """Get the assignee of the record.""" + return f"{self.commits[0].author.login}" if self.commits else None @property def assignees(self) -> Optional[str]: - return None + """Get the assignees of the record.""" + return f"{self.commits[0].author.login}" if self.commits else None @property def developers(self) -> Optional[str]: - return f"{self.commit.author.login}" + """Get the developers of the record.""" + return f"{self.commits[0].author.login}" if self.commits else None @property def contributors(self) -> Optional[str]: - logins = set() - commit_comments = list(self._safe_call(self.commit.get_comments)()) - - for commit_comment in commit_comments: - for line in commit_comment.body.split("\n"): - if "Co-authored-by:" in line: - logins.add(commit_comment.split("Co-authored-by:")[1].strip()) - + """Get the contributors of the record.""" + logins = self.get_contributors_for_commit(self.__commit) return ", ".join(logins) if len(logins) > 0 else None - @property - def pr_links(self) -> Optional[str]: - return None + def is_state(self, state:str) -> bool: + """Check if the record is in the given state.""" + return False - def pull_request_commit_count(self, pull_number: int = 0) -> int: - return len(self.commits) + def get_rls_notes(self, detection_pattern=RELEASE_NOTE_DETECTION_PATTERN, line_mark=RELEASE_NOTE_LINE_MARK) -> str: + """Get the release notes of the record.""" + return "" - def pull_request(self, index: int = 0) -> Optional[PullRequest]: + def pr_contains_issue_mentions(self) -> bool: + """Check if the record's pull request contains issue mentions.""" + return False + + def pr_links(self) -> Optional[str]: + """Get the pull request links of the record.""" return None - def register_pull_request(self, pull) -> None: + def register_pull_request(self, pr: PullRequest) -> None: + """Register a pull request with the record.""" pass def register_commit(self, commit: Commit) -> bool: @@ -138,14 +125,15 @@ def register_commit(self, commit: Commit) -> bool: def to_chapter_row(self) -> str: """ - Converts the record to a string row in a chapter. + Converts the record to a string row usable in a chapter. @return: The record as a row string. """ self.increment_present_in_chapters() - row_prefix = f"{ActionInputs.get_duplicity_icon()} " if self.present_in_chapters() > 1 else "" + row_prefix = f"{ActionInputs.get_duplicity_icon()} " if self.present_in_chapters > 1 else "" - row = f"{row_prefix}Commit: {self.commit.sha} developed by {self.developers}" + commit_sha = self.__commit.sha if self.__commit is not None else None + row = f"{row_prefix}Commit: {commit_sha} developed by {self.developers}" contributors = self.contributors if contributors: @@ -153,18 +141,10 @@ def to_chapter_row(self) -> str: return row.replace(" ", " ") - def __get_row_format_values(self, row_format: str) -> dict: - return {} - - def contains_min_one_label(self, labels: list[str]) -> bool: - return False - - def contain_all_labels(self, labels: list[str]) -> bool: - return False - - @staticmethod - def is_pull_request_merged(pull: PullRequest) -> bool: - return False + def fetch_pr_commits(self) -> None: + """Fetch the pull request commits of the record.""" + pass - def get_commits(self): + def get_sha_of_all_commits(self) -> set[str]: + """Get the set of all commit shas of the record.""" pass diff --git a/release_notes_generator/model/custom_chapters.py b/release_notes_generator/model/custom_chapters.py index f35fcf03..5c008b51 100644 --- a/release_notes_generator/model/custom_chapters.py +++ b/release_notes_generator/model/custom_chapters.py @@ -24,7 +24,7 @@ from release_notes_generator.action_inputs import ActionInputs from release_notes_generator.model.base_chapters import BaseChapters from release_notes_generator.model.chapter import Chapter -from release_notes_generator.model.record import Record +from release_notes_generator.model.base_record import Record from release_notes_generator.utils.enums import DuplicityScopeEnum @@ -52,7 +52,7 @@ def populate(self, records: dict[int, Record]) -> None: continue for record_label in records[nr].labels: # iterate all labels of the record (issue, or 1st PR) - if record_label in ch.labels and records[nr].pulls_count > 0: + if record_label in ch.labels and len(records[nr].pull_requests) > 0: if not records[nr].is_present_in_chapters: ch.add_row(nr, records[nr].to_chapter_row()) self.populated_record_numbers_list.append(nr) diff --git a/release_notes_generator/model/issue_record.py b/release_notes_generator/model/issue_record.py new file mode 100644 index 00000000..0b5ab0e2 --- /dev/null +++ b/release_notes_generator/model/issue_record.py @@ -0,0 +1,197 @@ +# +# Copyright 2023 ABSA Group Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +""" +This module contains the BaseChapters class which is responsible for representing the base chapters. +""" + +import logging +from typing import Optional + +from github.Issue import Issue +from github.PullRequest import PullRequest +from github.Repository import Repository +from github.Commit import Commit + +from release_notes_generator.action_inputs import ActionInputs +from release_notes_generator.model.base_record import Record +from release_notes_generator.utils.pull_reuqest_utils import extract_issue_numbers_from_body + +logger = logging.getLogger(__name__) + + +class IssueRecord(Record): + """ + A class used to represent a record in the release notes. + The record represents an issue with its Pull requests and commits. + """ + + def __init__(self, repo: Repository, safe_call, issue: Optional[Issue] = None): + super().__init__(repo, safe_call) + + self.__issue: Issue = issue + self.__pulls_requests: list[PullRequest] = [] + self.__pr_commits: dict[int, list[Commit]] = {} + + @property + def issue(self) -> Optional[Issue]: + """Get the issue of the record.""" + return self.__issue + + @property + def pull_requests(self) -> list[PullRequest]: + """Get the pull requests of the record.""" + return self.__pulls_requests + + @property + def commits(self) -> list[Commit]: + """Get the commits of the record.""" + all_commits = [] + for commits in self.__pr_commits.values(): + all_commits.extend(commits) + return all_commits + + @property + def id(self) -> Optional[int | str]: + """Get the id of the record.""" + return self.__issue if self.__issue is not None else None + + @property + def labels(self) -> list[str]: + """Getter for the labels of the record.""" + return [label.name for label in self.__issue.labels] if self.__issue is not None else [] + + @property + def assignee(self) -> Optional[str]: + """Get record's assignee.""" + return self.issue.assignee.login if self.issue.assignee is not None else None + + @property + def assignees(self) -> Optional[str]: + """Get record's assignees.""" + logins = [a.login for a in self.issue.assignees] + return ", ".join(logins) if len(logins) > 0 else None + + @property + def developers(self) -> Optional[str]: + """Get record's developers.""" + logins = set() + for commits in self.__pr_commits.values(): + for commit in commits: + if commit.author is not None: + logins.add(f"@{commit.author.login}") + + if not logins: + logger.warning("Found issue record %s with %d pull requests and no commits", self.id, len(self.__pulls_requests)) + + return ", ".join(logins) if len(logins) > 0 else None + + @property + def contributors(self) -> Optional[str]: + """Get record's contributors.""" + logins = [] + for c in self.commits: + for line in c.commit.message.split("\n"): + if "Co-authored-by:" in line: + name = line.split("Co-authored-by:")[1].strip() + if name not in logins: + logins.append(name) + + return ", ".join(logins) if len(logins) > 0 else None + + def is_state(self, state:str) -> bool: + """Check if the record is in a specific state.""" + return self.__issue.state == state if self.__issue is not None else False + + def pr_contains_issue_mentions(self) -> bool: + """Checks if the record's pull request contains issue mentions.""" + return len(extract_issue_numbers_from_body(self.__pulls_requests[0])) > 0 + + def pr_links(self) -> Optional[str]: + """Get links to record's pull requests.""" + if len(self.__pulls_requests) == 0: + return None + + res = [self.LINK_TO_PR_TEMPLATE.format(number=pull.number, full_name=self._repo.full_name) for pull in self.__pulls_requests] + return ", ".join(res) + + def register_pull_request(self, pr: PullRequest) -> None: + """ + Registers a pull request with the record. + + @param pr: The PullRequest object to register. + @return: None + """ + self.__pulls_requests.append(pr) + + def register_commit(self, commit: Commit) -> bool: + """ + Registers a commit with the record. + + @param commit: The Commit object to register. + @return: True if record is registered and valid for one of record's pull requests, False otherwise. + """ + sha = commit.sha + for pull in self.__pulls_requests: + if sha == pull.merge_commit_sha or sha == pull.head.sha: + if self.__pr_commits.get(pull.number) is None: + self.__pr_commits[pull.number] = [] + self.__pr_commits[pull.number].append(commit) + logger.debug("Commit %s registered using sha in PR %s of record %s", commit.sha, pull.number, self.id) + return True + + return False + + def to_chapter_row(self) -> str: + """ + Converts the record to a string row usable in a chapter. + + @return: The record as a row string. + """ + self.increment_present_in_chapters() + row_prefix = f"{ActionInputs.get_duplicity_icon()} " if self.present_in_chapters > 1 else "" + format_values = { + "number": self.__issue.number, + "title": self.__issue.title, + "pull-requests": f"in {self.pr_links()}" if len(self.__pulls_requests) > 0 else "" + } + + format_values.update(self._get_row_format_values(ActionInputs.get_row_format_issue())) + + row = f"{row_prefix}" + ActionInputs.get_row_format_issue().format(**format_values) + if self.contains_release_notes(): + row = f"{row}\n{self.get_rls_notes()}" + + return row.replace(" ", " ") + + def fetch_pr_commits(self) -> None: + """Fetch commits of the record's pull requests.""" + for pull in self.__pulls_requests: + self.__pr_commits[pull.number] = list(self._safe_call(pull.get_commits)()) + + def get_sha_of_all_commits(self) -> set[str]: + """Get the set of all commit shas of the record.""" + set_of_commit_shas = set() + + for commits in self.__pr_commits.values(): + for c in commits: + set_of_commit_shas.add(c.sha) + set_of_commit_shas.add(c.commit.sha) + + for pull in self.__pulls_requests: + set_of_commit_shas.add(pull.merge_commit_sha) + + return set_of_commit_shas diff --git a/release_notes_generator/model/pull_request_record.py b/release_notes_generator/model/pull_request_record.py new file mode 100644 index 00000000..26f84634 --- /dev/null +++ b/release_notes_generator/model/pull_request_record.py @@ -0,0 +1,177 @@ +# +# Copyright 2023 ABSA Group Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +""" +This module contains the BaseChapters class which is responsible for representing the base chapters. +""" + +import logging +from typing import Optional + +from github.Issue import Issue +from github.PullRequest import PullRequest +from github.Repository import Repository +from github.Commit import Commit + +from release_notes_generator.action_inputs import ActionInputs +from release_notes_generator.model.base_record import Record +from release_notes_generator.utils.pull_reuqest_utils import extract_issue_numbers_from_body + +logger = logging.getLogger(__name__) + + +class PullRequestRecord(Record): + """ + A class used to represent a record in the release notes. + The record represents a pull request and its commits. + """ + + def __init__(self, repo: Repository, safe_call, pull_request: PullRequest): + super().__init__(repo, safe_call) + + self.__pull_request: PullRequest = pull_request + self.__commits: list[Commit] = [] + + @property + def issue(self) -> Optional[Issue]: + """Get the issue of the record.""" + return None + + @property + def pull_requests(self) -> list[PullRequest]: + """Get the pull requests of the record.""" + return [self.__pull_request] + + @property + def commits(self) -> list[Commit]: + """Get the commits of the record.""" + return self.__commits + + # TODO - remove this method after unit test fix + # def pull_request_by_number(self) -> PullRequest: + # """Getter for the pull request of the record.""" + # return self.__pull_request + + @property + def id(self) -> Optional[int | str]: + """Get the id of the record.""" + return self.__pull_request.number if self.__pull_request is not None else None + + @property + def labels(self) -> list[str]: + """Get the labels of the record.""" + return [label.name for label in self.__pull_request.labels] if self.__pull_request is not None else [] + + @property + def assignee(self) -> Optional[str]: + """Get record assignee.""" + return self.__pull_request.assignee.login if self.__pull_request.assignee is not None else None + + @property + def assignees(self) -> Optional[str]: + """Get record assignees.""" + logins = [a.login for a in self.__pull_request.assignees] + return ", ".join(logins) if len(logins) > 0 else None + + @property + def developers(self) -> Optional[str]: + """Get record developers.""" + logins = {f"@{c.author.login}" for c in self.__commits} + if not logins: + logger.warning("Found pull request record '%s' with no commits", self.__pull_request.number) + return ", ".join(logins) if len(logins) > 0 else None + + @property + def contributors(self) -> Optional[str]: + """Get record contributors.""" + logins = [] + for c in self.__commits: + logins.extend(self.get_contributors_for_commit(c)) + + return ", ".join(logins) if len(logins) > 0 else None + + def is_state(self, state:str) -> bool: + """Check if the record is in a specific state.""" + return self.__pull_request.state == state if self.__pull_request is not None else False + + def pr_contains_issue_mentions(self) -> bool: + """Checks if the record's pull request contains issue mentions.""" + return len(extract_issue_numbers_from_body(self.__pull_request)) > 0 + + def pr_links(self) -> Optional[str]: + """Get links to record's pull requests.""" + return self.LINK_TO_PR_TEMPLATE.format(number=self.__pull_request.number, full_name=self._repo.full_name) + + def register_pull_request(self, pr: PullRequest) -> None: + """ + Registers a pull request with the record. + + @param pr: The PullRequest object to register. + @return: None + """ + self.__pull_request = pr + + def register_commit(self, commit: Commit) -> bool: + """ + Registers a commit with the record. + + @param commit: The Commit object to register. + @return: None + """ + sha = commit.sha + if sha == self.__pull_request.merge_commit_sha or sha == self.__pull_request.head.sha: + self.__commits.append(commit) + logger.debug("Commit %s registered using sha in PR %s of record %s", commit.sha, self.__pull_request.number, self.id) + return True + + return False + + def to_chapter_row(self) -> str: + """ + Converts the record to a string row usable in a chapter. + + @return: The record as a row string. + """ + self.increment_present_in_chapters() + row_prefix = f"{ActionInputs.get_duplicity_icon()} " if self.present_in_chapters > 1 else "" + format_values = { + "number": self.__pull_request.number, + "title": self.__pull_request.title + } + + format_values.update(self._get_row_format_values(ActionInputs.get_row_format_pr())) + + pr_prefix = "PR: " if ActionInputs.get_row_format_link_pr() else "" + row = f"{row_prefix}{pr_prefix}" + ActionInputs.get_row_format_pr().format(**format_values) + if self.contains_release_notes(): + row = f"{row}\n{self.get_rls_notes()}" + + return row.replace(" ", " ") + + def fetch_pr_commits(self) -> None: + """Fetches the commits of the record.""" + self.__commits = list(self._safe_call(self.__pull_request.get_commits)()) + + def get_sha_of_all_commits(self) -> set[str]: + """Get the set of all commit shas of the record.""" + set_of_commit_shas = set() + + for c in self.__commits: + set_of_commit_shas.add(c.sha) + set_of_commit_shas.add(c.commit.sha) + + set_of_commit_shas.add(self.__pull_request.merge_commit_sha) + return set_of_commit_shas diff --git a/release_notes_generator/model/record.py b/release_notes_generator/model/record.py deleted file mode 100644 index 5cc50aed..00000000 --- a/release_notes_generator/model/record.py +++ /dev/null @@ -1,441 +0,0 @@ -# -# Copyright 2023 ABSA Group Limited -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -""" -This module contains the BaseChapters class which is responsible for representing the base chapters. -""" - -import logging -from typing import Optional - -from github.Issue import Issue -from github.PullRequest import PullRequest -from github.Repository import Repository -from github.Commit import Commit - -from release_notes_generator.action_inputs import ActionInputs -from release_notes_generator.utils.constants import ( - PR_STATE_CLOSED, - ISSUE_STATE_CLOSED, - ISSUE_STATE_OPEN, - RELEASE_NOTE_DETECTION_PATTERN, - RELEASE_NOTE_LINE_MARK, -) -from release_notes_generator.utils.pull_reuqest_utils import extract_issue_numbers_from_body - -logger = logging.getLogger(__name__) - - -# TODO - recheck the size of class, is there a way to reduce or split it? -# pylint: disable=too-many-instance-attributes, too-many-public-methods -class Record: - """ - A class used to represent a record in the release notes. - """ - - def __init__(self, repo: Repository, safe_call, issue: Optional[Issue] = None): - self.__repo: Repository = repo - self.__gh_issue: Issue = issue - self.__pulls: list[PullRequest] = [] - self.__pull_commits: dict[int, list[Commit]] = {} - - self.__is_release_note_detected: bool = False - self.__present_in_chapters = 0 - - self._safe_call = safe_call - - @property - def number(self) -> int: - """Getter for the number of the record.""" - if self.__gh_issue is None: - return self.__pulls[0].number - return self.__gh_issue.number - - @property - def issue(self) -> Optional[Issue]: - """Getter for the issue of the record.""" - return self.__gh_issue - - @property - def pulls(self) -> list[PullRequest]: - """Getter for the pull requests of the record.""" - return self.__pulls - - @property - def commits(self) -> dict[int, list[Commit]]: - """Getter for the commits of the record.""" - return self.__pull_commits - - @property - def is_present_in_chapters(self) -> bool: - """Check if the record is present in chapters.""" - return self.__present_in_chapters > 0 - - @property - def is_pr(self) -> bool: - """Check if the record is a pull request.""" - return self.__gh_issue is None and len(self.__pulls) == 1 - - @property - def is_issue(self) -> bool: - """Check if the record is an issue.""" - return self.__gh_issue is not None - - @property - def is_closed(self) -> bool: - """Check if the record is closed.""" - if self.__gh_issue is None: - # no issue ==> stand-alone PR - return self.__pulls[0].state == PR_STATE_CLOSED - - return self.__gh_issue.state == ISSUE_STATE_CLOSED - - @property - def is_closed_issue(self) -> bool: - """Check if the record is a closed issue.""" - return self.is_issue and self.__gh_issue.state == ISSUE_STATE_CLOSED - - @property - def is_open_issue(self) -> bool: - """Check if the record is an open issue.""" - return self.is_issue and self.__gh_issue.state == ISSUE_STATE_OPEN - - @property - def is_merged_pr(self) -> bool: - """Check if the record is a merged pull request.""" - if self.__gh_issue is None: - return self.is_pull_request_merged(self.__pulls[0]) - return False - - @property - def labels(self) -> list[str]: - """Getter for the labels of the record.""" - if self.__gh_issue is None: - return [label.name for label in self.__pulls[0].labels] - - return [label.name for label in self.__gh_issue.labels] - - # TODO in Issue named 'Configurable regex-based Release note detection in the PR body' - # - 'Release notest:' as detection pattern default - can be defined by user - # - '-' as leading line mark for each release note to be used - def get_rls_notes(self, detection_pattern=RELEASE_NOTE_DETECTION_PATTERN, line_mark=RELEASE_NOTE_LINE_MARK) -> str: - """ - Gets the release notes of the record. - - @param detection_pattern: The detection pattern to use. - @param line_mark: The line mark to use. - @return: The release notes of the record as a string. - """ - release_notes = "" - - # Iterate over all PRs - for pull in self.__pulls: - body_lines = pull.body.split("\n") if pull.body is not None else [] - inside_release_notes = False - - for line in body_lines: - if detection_pattern in line: - inside_release_notes = True - continue - - if inside_release_notes: - if line.startswith(line_mark): - release_notes += f" {line.strip()}\n" - else: - break - - # Return the concatenated release notes - return release_notes.rstrip() - - @property - def contains_release_notes(self) -> bool: - """Checks if the record contains release notes.""" - if self.__is_release_note_detected: - return self.__is_release_note_detected - - if RELEASE_NOTE_LINE_MARK in self.get_rls_notes(): - self.__is_release_note_detected = True - - return self.__is_release_note_detected - - @property - def pulls_count(self) -> int: - """Getter for the count of pull requests of the record.""" - return len(self.__pulls) - - @property - def pr_contains_issue_mentions(self) -> bool: - """Checks if the pull request contains issue mentions.""" - return len(extract_issue_numbers_from_body(self.__pulls[0])) > 0 - - # Note: assignee & assignees are related to this GitHub Docs - https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/assigning-issues-and-pull-requests-to-other-github-users#about-issue-and-pull-request-assignees - @property - def assignee(self) -> Optional[str]: - """Getter for the authors of the record.""" - if self.__gh_issue is None: - return self.pulls[0].assignee.login if self.pulls[0].assignee is not None else None - - else: - return self.issue.assignee.login if self.issue.assignee is not None else None - - @property - def assignees(self) -> Optional[str]: - """Getter for the assignees of the record.""" - if self.__gh_issue is None: - logins = [a.login for a in self.pulls[0].assignees] - return ", ".join(logins) if len(logins) > 0 else None - - else: - logins = [a.login for a in self.issue.assignees] - return ", ".join(logins) if len(logins) > 0 else None - - @property - def developers(self) -> Optional[str]: - """Getter for the developers of the record.""" - if self.is_issue: - # is issue - go for its pulls - logins = set() - for commits in self.__pull_commits.values(): - for commit in commits: - if commit.author is not None: - logins.add(f"@{commit.author.login}") - - if not logins: - logger.warning("Found issue record %s with %d pull requests and no commits", self.number, len(self.__pulls)) - return ", ".join(logins) if len(logins) > 0 else None - - elif self.is_pr: - # is pr - go for its one pull only - logins = {f"@{c.author.login}" for c in self.__pull_commits[self.pulls[0].number]} - if not logins: - logger.warning("Found pull request record '%s' with no commits", self.pulls[0].number) - return ", ".join(logins) if len(logins) > 0 else None - - else: - logger.warning("Record '%s' is not issue nor PR. Developers cannot be determined.", self.number) - return None - - @property - def contributors(self) -> Optional[str]: - """Getter for the contributors of the record.""" - - if not self.is_issue and not self.is_pr: - logger.warning("Record '%s' is not issue nor PR. Contributors cannot be determined.", self.number) - return None - - logins = [] - for commits in self.commits.values(): - for c in commits: - for line in c.commit.message.split("\n"): - if "Co-authored-by:" in line: - name = line.split("Co-authored-by:")[1].strip() - if name not in logins: - logins.append(name) - - return ", ".join(logins) if len(logins) > 0 else None - - @property - def pr_links(self) -> Optional[str]: - """Getter for the pull request links of the record.""" - if len(self.__pulls) == 0: - return None - - template = "[#{number}](https://github.com/{full_name}/pull/{number})" - res = [template.format(number=pull.number, full_name=self.__repo.full_name) for pull in self.__pulls] - - return ", ".join(res) - - def pull_request_commit_count(self, pull_number: int = 0) -> int: - """ - Get count of commits in all record pull requests. - - @param pull_number: The number of the pull request. - @return: The count of commits in the pull request. - """ - for pull in self.__pulls: - if pull.number == pull_number: - if pull.number in self.__pull_commits: - return len(self.__pull_commits.get(pull.number)) - - return 0 - - return 0 - - def pull_request(self, index: int = 0) -> Optional[PullRequest]: - """ - Gets a pull request associated with the record. - - @param index: The index of the pull request. - @return: The PullRequest instance. - """ - if index < 0 or index >= len(self.__pulls): - return None - return self.__pulls[index] - - def register_pull_request(self, pull) -> None: - """ - Registers a pull request with the record. - - @param pull: The PullRequest object to register. - @return: None - """ - self.__pulls.append(pull) - - def register_commit(self, commit: Commit) -> bool: - """ - Registers a commit with the record. - - @param commit: The Commit object to register. - @return: None - """ - sha = commit.sha - for pull in self.__pulls: - if sha == pull.merge_commit_sha or sha == pull.head.sha: - if self.__pull_commits.get(pull.number) is None: - self.__pull_commits[pull.number] = [] - self.__pull_commits[pull.number].append(commit) - logger.debug("Commit %s registered using sha in PR %s of record %s", commit.sha, pull.number, self.number) - return True - - return False - - def to_chapter_row(self) -> str: - """ - Converts the record to a string row in a chapter. - - @return: The record as a row string. - """ - self.increment_present_in_chapters() - row_prefix = f"{ActionInputs.get_duplicity_icon()} " if self.present_in_chapters() > 1 else "" - format_values = {} - - if self.__gh_issue is None: - p = self.__pulls[0] - format_values["number"] = p.number - format_values["title"] = p.title - format_values.update(self.__get_row_format_values(ActionInputs.get_row_format_pr())) - - pr_prefix = "PR: " if ActionInputs.get_row_format_link_pr() else "" - row = f"{row_prefix}{pr_prefix}" + ActionInputs.get_row_format_pr().format(**format_values) - - else: - format_values["number"] = self.__gh_issue.number - format_values["title"] = self.__gh_issue.title - format_values["pull-requests"] = f"in {self.pr_links}" if len(self.__pulls) > 0 else "" - format_values.update(self.__get_row_format_values(ActionInputs.get_row_format_issue())) - - row = f"{row_prefix}" + ActionInputs.get_row_format_issue().format(**format_values) - - if self.contains_release_notes: - row = f"{row}\n{self.get_rls_notes()}" - - return row.replace(" ", " ") - - def __get_row_format_values(self, row_format: str) -> dict: - """ - Create dictionary and fill by user row format defined values. - NoteL some values are API call intensive. - - @param row_format: User defined row format. - @return: The dictionary with supported values required by user row format. - """ - format_values = {} - - if "{assignee}" in row_format: - assignee = self.assignees - format_values["assignee"] = f"assigned to @{assignee}" if assignee is not None else "" - if "{assignees}" in row_format: - assignees = self.assignees - format_values["assignees"] = f"assigned to @{assignees}" if assignees is not None else "" - if "{developed-by}" in row_format: - developers = self.developers - format_values["developed-by"] = f"developed by {developers}" if developers is not None else "" - if "{co-authored-by}" in row_format: - contributors = self.contributors - format_values["co-authored-by"] = f"co-authored by {contributors}" if contributors is not None else "" - - return format_values - - - def contains_min_one_label(self, labels: list[str]) -> bool: - """ - Check if the record contains at least one of the specified labels. - - @param labels: A list of labels to check for. - @return: A boolean indicating whether the record contains any of the specified labels. - """ - for lbl in self.labels: - if lbl in labels: - return True - return False - - def contain_all_labels(self, labels: list[str]) -> bool: - """ - Check if the record contains all of the specified labels. - - @param labels: A list of labels to check for. - @return: A boolean indicating whether the record contains all of the specified - """ - if len(self.labels) != len(labels): - return False - - for lbl in self.labels: - if lbl not in labels: - return False - return True - - def increment_present_in_chapters(self) -> None: - """ - Increments the count of chapters in which the record is present. - - @return: None - """ - self.__present_in_chapters += 1 - - def present_in_chapters(self) -> int: - """ - Gets the count of chapters in which the record is present. - - @return: The count of chapters in which the record is present. - """ - return self.__present_in_chapters - - @staticmethod - def is_pull_request_merged(pull: PullRequest) -> bool: - """ - Checks if the pull request is merged. - - @param pull: The pull request to check. - @return: A boolean indicating whether the pull request is merged. - """ - return pull.state == PR_STATE_CLOSED and pull.merged_at is not None and pull.closed_at is not None - - def get_commits(self) -> None: - for pull in self.__pulls: - self.__pull_commits[pull.number] = list(self._safe_call(pull.get_commits)()) - - def get_commits_shas(self) -> set[str]: - set_of_commit_shas = set() - - for commits in self.__pull_commits.values(): - for c in commits: - set_of_commit_shas.add(c.sha) - set_of_commit_shas.add(c.commit.sha) - - for pull in self.__pulls: - set_of_commit_shas.add(pull.merge_commit_sha) - - return set_of_commit_shas diff --git a/release_notes_generator/model/service_chapters.py b/release_notes_generator/model/service_chapters.py index 66b01587..b632c413 100644 --- a/release_notes_generator/model/service_chapters.py +++ b/release_notes_generator/model/service_chapters.py @@ -23,14 +23,16 @@ from release_notes_generator.action_inputs import ActionInputs from release_notes_generator.model.base_chapters import BaseChapters from release_notes_generator.model.chapter import Chapter -from release_notes_generator.model.record import Record +from release_notes_generator.model.base_record import Record +from release_notes_generator.model.issue_record import IssueRecord +from release_notes_generator.model.pull_request_record import PullRequestRecord from release_notes_generator.utils.constants import ( CLOSED_ISSUES_WITHOUT_PULL_REQUESTS, CLOSED_ISSUES_WITHOUT_USER_DEFINED_LABELS, MERGED_PRS_WITHOUT_ISSUE_AND_USER_DEFINED_LABELS, CLOSED_PRS_WITHOUT_ISSUE_AND_USER_DEFINED_LABELS, MERGED_PRS_LINKED_TO_NOT_CLOSED_ISSUES, - OTHERS_NO_TOPIC, ISOLATED_COMMITS, + OTHERS_NO_TOPIC, ISOLATED_COMMITS, ISSUE_STATE_CLOSED, ISSUE_STATE_OPEN, PR_STATE_CLOSED, ) from release_notes_generator.utils.enums import DuplicityScopeEnum @@ -108,16 +110,16 @@ def populate(self, records: dict[int|str, Record]) -> None: if isinstance(nr, str): self.__populate_isolated_commits(records[nr], nr) - elif records[nr].is_closed_issue: + elif isinstance(records[nr], IssueRecord) and records[nr].is_state(ISSUE_STATE_CLOSED): self.__populate_closed_issues(records[nr], nr) - elif records[nr].is_pr: + elif isinstance(records[nr], PullRequestRecord): self.__populate_pr(records[nr], nr) else: - if records[nr].is_open_issue and records[nr].pulls_count == 0: + if isinstance(records[nr], IssueRecord) and records[nr].is_state(ISSUE_STATE_OPEN) and len(records[nr].pull_requests) == 0: pass - elif records[nr].is_open_issue and records[nr].pulls_count > 0: + elif isinstance(records[nr], IssueRecord) and records[nr].is_state(ISSUE_STATE_OPEN) and len(records[nr].pull_requests) > 0: self.chapters[MERGED_PRS_LINKED_TO_NOT_CLOSED_ISSUES].add_row(nr, records[nr].to_chapter_row()) self.used_record_numbers.append(nr) else: @@ -138,7 +140,7 @@ def __populate_closed_issues(self, r: Record, nr: int) -> None: """ # check record properties if it fits to a chapter: CLOSED_ISSUES_WITHOUT_PULL_REQUESTS populated = False - if r.pulls_count == 0: + if len(r.pull_requests) == 0: self.chapters[CLOSED_ISSUES_WITHOUT_PULL_REQUESTS].add_row(nr, r.to_chapter_row()) self.used_record_numbers.append(nr) populated = True @@ -153,7 +155,7 @@ def __populate_closed_issues(self, r: Record, nr: int) -> None: self.used_record_numbers.append(nr) populated = True - if r.pulls_count > 0: + if len(r.pull_requests) > 0: # the record looks to be valid closed issue with 1+ pull requests return @@ -172,7 +174,7 @@ def __populate_pr(self, r: Record, nr: int) -> None: @param nr: The number of the record. @return: None """ - if r.is_merged_pr: + if Record.is_pull_request_merged(r.pull_requests[0]): # check record properties if it fits to a chapter: MERGED_PRS_WITHOUT_ISSUE if not r.pr_contains_issue_mentions and not r.contains_min_one_label(self.user_defined_labels): if self.__is_row_present(nr) and not self.duplicity_allowed(): @@ -198,7 +200,7 @@ def __populate_pr(self, r: Record, nr: int) -> None: # check record properties if it fits to a chapter: CLOSED_PRS_WITHOUT_ISSUE elif ( - r.is_closed + r.is_state(PR_STATE_CLOSED) and not r.pr_contains_issue_mentions and not r.contains_min_one_label(self.user_defined_labels) ): diff --git a/release_notes_generator/record/record_factory.py b/release_notes_generator/record/record_factory.py index 8d42a4bb..055263ee 100644 --- a/release_notes_generator/record/record_factory.py +++ b/release_notes_generator/record/record_factory.py @@ -26,8 +26,10 @@ from github.Repository import Repository from github.Commit import Commit -from release_notes_generator.model.isolated_commits_record import IsolatedCommitsRecord -from release_notes_generator.model.record import Record +from release_notes_generator.model.commit_record import CommitRecord +from release_notes_generator.model.base_record import Record +from release_notes_generator.model.issue_record import IssueRecord +from release_notes_generator.model.pull_request_record import PullRequestRecord from release_notes_generator.utils.decorators import safe_call_decorator from release_notes_generator.utils.github_rate_limiter import GithubRateLimiter @@ -61,7 +63,7 @@ def generate( safe_call = safe_call_decorator(rate_limiter) def create_record_for_issue(i: Issue): - records[i.number] = Record(repo, safe_call, i) + records[i.number] = IssueRecord(repo, safe_call, i) logger.debug("Created record for issue %d: %s", i.number, i.title) def register_pull_request(pull: PullRequest): @@ -81,8 +83,7 @@ def register_pull_request(pull: PullRequest): records[parent_issue_number].register_pull_request(pull) logger.debug("Registering PR %d: %s to Issue %d", pull.number, pull.title, parent_issue_number) else: - records[pull.number] = Record(repo, safe_call) - records[pull.number].register_pull_request(pull) + records[pull.number] = PullRequestRecord(repo, safe_call, pull) logger.debug( "Registering stand-alone PR %d: %s as mentioned Issue %d not found.", pull.number, @@ -107,8 +108,7 @@ def register_pull_request(pull: PullRequest): logger.debug("Creating records from Pull Requests.") for pull in pulls: if not extract_issue_numbers_from_body(pull): - records[pull.number] = Record(repo, safe_call) - records[pull.number].register_pull_request(pull) + records[pull.number] = PullRequestRecord(repo, safe_call, pull) logger.debug("Created record for PR %d: %s", pull.number, pull.title) else: register_pull_request(pull) @@ -121,16 +121,16 @@ def register_pull_request(pull: PullRequest): """ # cycle across all record's PRs & ask for their commits for record in records.values(): - record.get_commits() + record.fetch_pr_commits() # cycle across all record's PR's commits & identify direct commits linked_commits_by_sha: set[str] = set() for r in records.values(): - linked_commits_by_sha.update(r.get_commits_shas()) + linked_commits_by_sha.update(r.get_sha_of_all_commits()) for c in commits: if c.sha not in linked_commits_by_sha: - isolated_record = IsolatedCommitsRecord(repo, safe_call) + isolated_record = CommitRecord(repo, safe_call) isolated_record.register_commit(c) records_for_isolated_commits[c.sha] = isolated_record diff --git a/tests/conftest.py b/tests/conftest.py index cdd09f7e..f6e54394 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -28,7 +28,7 @@ from github.Repository import Repository from release_notes_generator.model.service_chapters import ServiceChapters -from release_notes_generator.model.record import Record +from release_notes_generator.model.base_record import Record from release_notes_generator.model.chapter import Chapter from release_notes_generator.model.custom_chapters import CustomChapters from release_notes_generator.utils.constants import ISSUE_STATE_OPEN, ISSUE_STATE_CLOSED, PR_STATE_CLOSED, PR_STATE_OPEN @@ -120,7 +120,7 @@ def mock_issue_open(mocker): label2 = mocker.Mock(spec=MockLabel) label2.name = "label2" issue.labels = [label1, label2] - issue.number = 122 + issue.id = 122 issue.title = "I1 open" issue.state_reason = None return issue @@ -135,7 +135,7 @@ def mock_issue_open_2(mocker): label2 = mocker.Mock(spec=MockLabel) label2.name = "label2" issue.labels = [label1, label2] - issue.number = 123 + issue.id = 123 issue.title = "I2 open" issue.state_reason = None return issue @@ -151,7 +151,7 @@ def mock_issue_closed(mocker): label2.name = "label2" issue.labels = [label1, label2] issue.title = "Fix the bug" - issue.number = 121 + issue.id = 121 return issue @@ -165,7 +165,7 @@ def mock_issue_closed_i1_bug(mocker): label2.name = "bug" issue.labels = [label1, label2] issue.title = "I1+bug" - issue.number = 122 + issue.id = 122 return issue @@ -179,7 +179,7 @@ def mock_pull_closed(mocker): label1 = mocker.Mock(spec=MockLabel) label1.name = "label1" pull.labels = [label1] - pull.number = 123 + pull.id = 123 pull.merge_commit_sha = "merge_commit_sha" pull.title = "Fixed bug" pull.created_at = datetime.now() @@ -198,7 +198,7 @@ def mock_pull_closed_with_rls_notes_101(mocker): label1 = mocker.Mock(spec=MockLabel) label1.name = "label1" pull.labels = [label1] - pull.number = 101 + pull.id = 101 pull.merge_commit_sha = "merge_commit_sha" pull.title = "Fixed bug" pull.created_at = datetime.now() @@ -217,7 +217,7 @@ def mock_pull_closed_with_rls_notes_102(mocker): label1 = mocker.Mock(spec=MockLabel) label1.name = "label1" pull.labels = [label1] - pull.number = 102 + pull.id = 102 pull.merge_commit_sha = "merge_commit_sha" pull.title = "Fixed bug" pull.created_at = datetime.now() @@ -236,7 +236,7 @@ def mock_pull_merged_with_rls_notes_101(mocker): label1 = mocker.Mock(spec=MockLabel) label1.name = "label1" pull.labels = [label1] - pull.number = 101 + pull.id = 101 pull.merge_commit_sha = "merge_commit_sha" pull.title = "Fixed bug" pull.created_at = datetime.now() @@ -255,7 +255,7 @@ def mock_pull_merged_with_rls_notes_102(mocker): label1 = mocker.Mock(spec=MockLabel) label1.name = "label1" pull.labels = [label1] - pull.number = 102 + pull.id = 102 pull.merge_commit_sha = "merge_commit_sha" pull.title = "Fixed bug" pull.created_at = datetime.now() @@ -274,7 +274,7 @@ def mock_pull_merged(mocker): label1 = mocker.Mock(spec=MockLabel) label1.name = "label1" pull.labels = [label1] - pull.number = 123 + pull.id = 123 pull.merge_commit_sha = "merge_commit_sha" pull.title = "Fixed bug" pull.created_at = datetime.now() @@ -293,7 +293,7 @@ def mock_pull_open(mocker): label1 = mocker.Mock(spec=MockLabel) label1.name = "label1" pull.labels = [label1] - pull.number = 123 + pull.id = 123 pull.merge_commit_sha = None pull.title = "Fix bug" pull.created_at = datetime.now() @@ -311,7 +311,7 @@ def mock_pull_no_rls_notes(mocker): label1 = mocker.Mock(spec=MockLabel) label1.name = "label1" pull.labels = [label1] - pull.number = 123 + pull.id = 123 pull.title = "Fixed bug" return pull @@ -409,8 +409,8 @@ def record_with_two_issue_open_two_pulls_closed(request): mock_repo_fixture.full_name = "org/repo" records = {} - records[rec1.number] = rec1 - records[rec2.number] = rec2 + records[rec1.id] = rec1 + records[rec2.id] = rec2 return records From c118375db0216d24e080023017b31615557e9f1b Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Mon, 7 Oct 2024 21:19:52 +0200 Subject: [PATCH 44/51] - Add support for row format for direct commits. - Improved validation logic to support difference keywords for issue, pr and commit format rows. --- action.yml | 14 +- examples/output_example.md | 224 +++++++++++++----- release_notes_generator/action_inputs.py | 37 ++- release_notes_generator/model/base_record.py | 6 +- .../model/commit_record.py | 15 +- release_notes_generator/utils/constants.py | 6 +- release_notes_generator/utils/exceptions.py | 3 + release_notes_generator/utils/utils.py | 16 +- 8 files changed, 251 insertions(+), 70 deletions(-) create mode 100644 release_notes_generator/utils/exceptions.py diff --git a/action.yml b/action.yml index 22b0d6e0..79e94812 100644 --- a/action.yml +++ b/action.yml @@ -56,17 +56,25 @@ inputs: required: false default: 'false' row-format-issue: - description: 'Format of the issue row in the release notes. Available placeholders: {link}, {title}, {pull-requests}, {assignee}, {assignees}, {developed-by}, {co-authored-by}. Placeholders are case-insensitive.' + description: 'Format of the issue row in the release notes. Available placeholders: {number}, {title}, {pull-requests}, {assignee}, {assignees}, {developed-by}, {co-authored-by}. Placeholders are case-insensitive.' required: false default: '#{number} _{title}_ {pull-requests} {assignee} {developed-by} {co-authored-by}' row-format-pr: - description: 'Format of the pr row in the release notes. Available placeholders: {link}, {title}, {pull-requests}, {assignee}, {assignees}, {developed-by}, {co-authored-by}. Placeholders are case-insensitive.' + description: 'Format of the pr row in the release notes. Available placeholders: {number}, {title}, {assignee}, {assignees}, {developed-by}, {co-authored-by}. Placeholders are case-insensitive.' required: false default: '#{number} _{title}_ {assignee} {developed-by} {co-authored-by}' + row-format-commit: + description: 'Format of the commit row in the release notes. Available placeholders: {sha}, {author}, {co-authored-by}. Placeholders are case-insensitive.' + required: false + default: '#{sha} {author} {co-authored-by}' row-format-link-pr: description: 'Add prefix "PR:" before link to PR when not linked an Issue.' required: false default: 'true' + row-format-link-commit: + description: 'Add prefix "Commit:" before link to direct commit.' + required: false + default: 'true' outputs: release-notes: @@ -123,7 +131,9 @@ runs: INPUT_GITHUB_REPOSITORY: ${{ github.repository }} INPUT_ROW_FORMAT_ISSUE: ${{ inputs.row-format-issue }} INPUT_ROW_FORMAT_PR: ${{ inputs.row-format-pr }} + INPUT_ROW_FORMAT_COMMIT: ${{ inputs.row-format-commit }} INPUT_ROW_FORMAT_LINK_PR: ${{ inputs.row-format-link-pr }} + INPUT_ROW_FORMAT_LINK_COMMIT: ${{ inputs.row-format-link-commit }} run: | python ${{ github.action_path }}/main.py shell: bash diff --git a/examples/output_example.md b/examples/output_example.md index 57f05119..f24c4b97 100644 --- a/examples/output_example.md +++ b/examples/output_example.md @@ -2,75 +2,193 @@ No entries detected. ### New Features 🎉 -- #22 _REQ: Submit Reviews_ in [#31](https://github.com/company/test-project/pull/31) - - Now only book purchasers can submit reviews, with mandatory text and star ratings. -- #52 _Add Tag into Release Draft_ in [#59](https://github.com/company/test-project/pull/59), [#58](https://github.com/company/test-project/pull/58), [#57](https://github.com/company/test-project/pull/57), [#56](https://github.com/company/test-project/pull/56), [#55](https://github.com/company/test-project/pull/55), [#54](https://github.com/company/test-project/pull/54), [#53](https://github.com/company/test-project/pull/53) -- #82 _Create tag after success RLS notes generation_ in [#87](https://github.com/company/test-project/pull/87), [#86](https://github.com/company/test-project/pull/86), [#85](https://github.com/company/test-project/pull/85), [#84](https://github.com/company/test-project/pull/84), [#83](https://github.com/company/test-project/pull/83) +- #22 _REQ: Submit Reviews_ in [#31](https://github.com/absa-group/living-doc-example-project/pull/31) developed by @miroslavpojer +- Now only book purchasers can submit reviews, with mandatory text and star ratings. +- #52 _Add Tag into Release Draft_ in [#59](https://github.com/absa-group/living-doc-example-project/pull/59), [#58](https://github.com/absa-group/living-doc-example-project/pull/58), [#57](https://github.com/absa-group/living-doc-example-project/pull/57), [#56](https://github.com/absa-group/living-doc-example-project/pull/56), [#55](https://github.com/absa-group/living-doc-example-project/pull/55), [#54](https://github.com/absa-group/living-doc-example-project/pull/54), [#53](https://github.com/absa-group/living-doc-example-project/pull/53) assigned to @miroslavpojer developed by @miroslavpojer +- #82 _Create tag after success RLS notes generation_ in [#87](https://github.com/absa-group/living-doc-example-project/pull/87), [#86](https://github.com/absa-group/living-doc-example-project/pull/86), [#85](https://github.com/absa-group/living-doc-example-project/pull/85), [#84](https://github.com/absa-group/living-doc-example-project/pull/84), [#83](https://github.com/absa-group/living-doc-example-project/pull/83) assigned to @miroslavpojer developed by @miroslavpojer ### Bugfixes 🛠 -- #33 _Example bugfix_ in [#44](https://github.com/company/test-project/pull/44), [#36](https://github.com/company/test-project/pull/36), [#35](https://github.com/company/test-project/pull/35), [#34](https://github.com/company/test-project/pull/34) - - Another solved typos. Hello from second RLS notes comment. - - Solved some typos. -- PR: #41 _Initial commit._ - - Test release notes nr1 - - Test release notes nr2 +- #33 _Example bugfix_ in [#44](https://github.com/absa-group/living-doc-example-project/pull/44), [#36](https://github.com/absa-group/living-doc-example-project/pull/36), [#35](https://github.com/absa-group/living-doc-example-project/pull/35), [#34](https://github.com/absa-group/living-doc-example-project/pull/34) assigned to @miroslavpojer developed by @miroslavpojer co-authored by Saša Zejnilović +- Another solved typos. Hello from second RLS notes comment. +- Solved some typos. +- PR: #41 _Initial commit._ assigned to @miroslavpojer developed by @miroslavpojer +- Test release notes nr1 +- Test release notes nr2 ### Closed Issues without Pull Request ⚠️ -- #3 _FEAT: User Authentication_ -- #4 _FEAT: Book Browsing_ -- #6 _FEAT: Shopping Cart_ +- #3 _FEAT: User Authentication_ assigned to @miroslavpojer +- #4 _FEAT: Book Browsing_ assigned to @miroslavpojer +- #6 _FEAT: Shopping Cart_ assigned to @miroslavpojer - #37 _Example Issue without PR_ - #38 _Example Issue without Release notes comment_ - #88 _Test issue_ ### Closed Issues without User Defined Labels ⚠️ -- #1 _Initial version of project_ in [#2](https://github.com/company/test-project/pull/2) -- #7 _REQ: User Login Functionality_ in [#13](https://github.com/company/test-project/pull/13) -- #8 _REQ: User Registration Functionality_ in [#13](https://github.com/company/test-project/pull/13) -- #9 _REQ: View Book List_ in [#14](https://github.com/company/test-project/pull/14) -- #10 _REQ: Detailed Book Information_ in [#14](https://github.com/company/test-project/pull/14) -- #11 _REQ: Adding Books to Shopping Cart_ in [#15](https://github.com/company/test-project/pull/15) -- #12 _REQ: Viewing Shopping Cart Contents_ in [#15](https://github.com/company/test-project/pull/15) -- #23 _REQ: View Reviews_ in [#27](https://github.com/company/test-project/pull/27) -- #29 _Introduce workflow logic for Release notes_ in [#28](https://github.com/company/test-project/pull/28) -- #30 _Introduce Release notes logic_ in [#32](https://github.com/company/test-project/pull/32) +- #1 _Initial version of project_ in [#2](https://github.com/absa-group/living-doc-example-project/pull/2) assigned to @miroslavpojer developed by @miroslavpojer +- #7 _REQ: User Login Functionality_ in [#13](https://github.com/absa-group/living-doc-example-project/pull/13) assigned to @miroslavpojer developed by @miroslavpojer +- #8 _REQ: User Registration Functionality_ in [#13](https://github.com/absa-group/living-doc-example-project/pull/13) assigned to @miroslavpojer developed by @miroslavpojer +- #9 _REQ: View Book List_ in [#14](https://github.com/absa-group/living-doc-example-project/pull/14) assigned to @miroslavpojer developed by @miroslavpojer +- #10 _REQ: Detailed Book Information_ in [#14](https://github.com/absa-group/living-doc-example-project/pull/14) assigned to @miroslavpojer developed by @miroslavpojer +- #11 _REQ: Adding Books to Shopping Cart_ in [#15](https://github.com/absa-group/living-doc-example-project/pull/15) assigned to @miroslavpojer developed by @miroslavpojer +- #12 _REQ: Viewing Shopping Cart Contents_ in [#15](https://github.com/absa-group/living-doc-example-project/pull/15) assigned to @miroslavpojer developed by @miroslavpojer +- #23 _REQ: View Reviews_ in [#27](https://github.com/absa-group/living-doc-example-project/pull/27) developed by @miroslavpojer +- #29 _Introduce workflow logic for Release notes_ in [#28](https://github.com/absa-group/living-doc-example-project/pull/28) assigned to @miroslavpojer developed by @miroslavpojer +- #30 _Introduce Release notes logic_ in [#32](https://github.com/absa-group/living-doc-example-project/pull/32) assigned to @miroslavpojer developed by @miroslavpojer ### Merged PRs without Issue and User Defined Labels ⚠️ -- PR: #5 _BugFix - correct Issue GH folder location_ -- PR: #16 _repository improvement_ -- PR: #26 _Initial test headers_ -- PR: #39 _Initial commit._ -- PR: #40 _Initial commit._ -- PR: #42 _Initial commit._ -- PR: #43 _Feature/new tag_ -- PR: #45 _Initial commit._ -- PR: #46 _Revert "- Improved README.md (#36)"_ -- PR: #47 _- Added code for received tag format and correct version increase._ -- PR: #48 _Update of tag checks._ -- PR: #49 _Feature/tag checks update_ -- PR: #50 _Feature/tag checks update_ -- PR: #51 _Feature/tag checks update_ -- PR: #61 _New check implemented._ -- PR: #62 _Feature/add first tag check_ -- PR: #63 _New check implemented._ -- PR: #64 _Experiment with improving release worklflows._ -- PR: #66 _- Prepared workflow for RLS notes generation testing._ +All merged PRs are linked to issues. ### Closed PRs without Issue and User Defined Labels ⚠️ -- PR: #60 _Test change to test close of PR instead of Merge._ -- PR: #65 _Fake change in PR to get PR._ -- PR: #92 _Fake change._ +All closed PRs are linked to issues. ### Merged PRs Linked to 'Not Closed' Issue ⚠️ -- #20 _REQ: Search by Keywords_ in [#44](https://github.com/company/test-project/pull/44) -- 🔔 #33 _Example bugfix_ in [#44](https://github.com/company/test-project/pull/44), [#36](https://github.com/company/test-project/pull/36), [#35](https://github.com/company/test-project/pull/35), [#34](https://github.com/company/test-project/pull/34) - - Another solved typos. Hello from second RLS notes comment. - - Solved some typos. -- PR: #80 _Feature/multiline excludes_ -- #81 _Test multiline excludes in filename inspector related yml_ in [#79](https://github.com/company/test-project/pull/79), [#78](https://github.com/company/test-project/pull/78), [#77](https://github.com/company/test-project/pull/77), [#76](https://github.com/company/test-project/pull/76), [#75](https://github.com/company/test-project/pull/75), [#74](https://github.com/company/test-project/pull/74), [#73](https://github.com/company/test-project/pull/73), [#72](https://github.com/company/test-project/pull/72), [#71](https://github.com/company/test-project/pull/71), [#70](https://github.com/company/test-project/pull/70), [#69](https://github.com/company/test-project/pull/69), [#68](https://github.com/company/test-project/pull/68), [#67](https://github.com/company/test-project/pull/67) +- PR: #5 _BugFix - correct Issue GH folder location_ assigned to @miroslavpojer developed by @miroslavpojer +- PR: #16 _repository improvement_ assigned to @miroslavpojer developed by @miroslavpojer +- #20 _REQ: Search by Keywords_ in [#44](https://github.com/absa-group/living-doc-example-project/pull/44) assigned to @miroslavpojer developed by @miroslavpojer +- PR: #26 _Initial test headers_ assigned to @miroslavpojer developed by @miroslavpojer +- 🔔 #33 _Example bugfix_ in [#44](https://github.com/absa-group/living-doc-example-project/pull/44), [#36](https://github.com/absa-group/living-doc-example-project/pull/36), [#35](https://github.com/absa-group/living-doc-example-project/pull/35), [#34](https://github.com/absa-group/living-doc-example-project/pull/34) assigned to @miroslavpojer developed by @miroslavpojer co-authored by Saša Zejnilović +- Another solved typos. Hello from second RLS notes comment. +- Solved some typos. +- PR: #39 _Initial commit._ assigned to @miroslavpojer developed by @miroslavpojer +- PR: #40 _Initial commit._ assigned to @miroslavpojer developed by @miroslavpojer +- 🔔 PR: #41 _Initial commit._ assigned to @miroslavpojer developed by @miroslavpojer +- Test release notes nr1 +- Test release notes nr2 +- PR: #42 _Initial commit._ developed by @miroslavpojer +- PR: #43 _Feature/new tag_ developed by @miroslavpojer +- PR: #45 _Initial commit._ developed by @miroslavpojer +- PR: #46 _Revert "- Improved README.md (#36)"_ assigned to @miroslavpojer developed by @miroslavpojer +- PR: #47 _- Added code for received tag format and correct version increase._ assigned to @miroslavpojer developed by @miroslavpojer +- PR: #48 _Update of tag checks._ assigned to @miroslavpojer developed by @miroslavpojer +- PR: #49 _Feature/tag checks update_ developed by @miroslavpojer +- PR: #50 _Feature/tag checks update_ developed by @miroslavpojer +- PR: #51 _Feature/tag checks update_ developed by @miroslavpojer +- PR: #61 _New check implemented._ assigned to @miroslavpojer developed by @miroslavpojer +- PR: #62 _Feature/add first tag check_ assigned to @miroslavpojer developed by @miroslavpojer +- PR: #63 _New check implemented._ assigned to @miroslavpojer developed by @miroslavpojer +- PR: #64 _Experiment with improving release worklflows._ assigned to @miroslavpojer developed by @miroslavpojer +- PR: #66 _- Prepared workflow for RLS notes generation testing._ assigned to @miroslavpojer developed by @miroslavpojer +- PR: #80 _Feature/multiline excludes_ developed by @miroslavpojer +- #81 _Test multiline excludes in filename inspector related yml_ in [#79](https://github.com/absa-group/living-doc-example-project/pull/79), [#78](https://github.com/absa-group/living-doc-example-project/pull/78), [#77](https://github.com/absa-group/living-doc-example-project/pull/77), [#76](https://github.com/absa-group/living-doc-example-project/pull/76), [#75](https://github.com/absa-group/living-doc-example-project/pull/75), [#74](https://github.com/absa-group/living-doc-example-project/pull/74), [#73](https://github.com/absa-group/living-doc-example-project/pull/73), [#72](https://github.com/absa-group/living-doc-example-project/pull/72), [#71](https://github.com/absa-group/living-doc-example-project/pull/71), [#70](https://github.com/absa-group/living-doc-example-project/pull/70), [#69](https://github.com/absa-group/living-doc-example-project/pull/69), [#68](https://github.com/absa-group/living-doc-example-project/pull/68), [#67](https://github.com/absa-group/living-doc-example-project/pull/67) developed by @miroslavpojer + +### Isolated commits without Issue or PR ⚠️ +- Commit: #0079dab11f408346aeb755fd8feda74692798b70 developed by @MobiTikula +- Commit: #02d4c4942ea5b84f0772c30d7eb1656584e711d5 developed by @miroslavpojer +- Commit: #07e7d0b4c55ae77e76a25a09a6cffa0dd4ba64a0 developed by @miroslavpojer +- Commit: #1133a080f5b20f7233b1c581d8bed05ed8004bda developed by @MobiTikula +- Commit: #1245a0e1aac79d2f829b8c830cbf013cea2ee377 developed by @miroslavpojer +- Commit: #15e21c60c75dbcdf3c450e2d527f970ec58b13dd developed by @miroslavpojer +- Commit: #1ab371b180aa256d97ce083ae718ea00d051d1a0 developed by @miroslavpojer +- Commit: #1d9893e931a4fa4bcf14ddbdc769a61ee849f324 developed by @miroslavpojer +- Commit: #1ea2b6c56e3748a2263a04dd4ddfeddd1bcf950a developed by @miroslavpojer +- Commit: #2474c94397a01d9c415d36a81f00c968cecc8f7e developed by @miroslavpojer +- Commit: #24e3341c32700dce7087ac90eb67e50686e37bdf developed by @miroslavpojer +- Commit: #265b572f822e298b25a1e5f3b15f8bb687b28689 developed by @MobiTikula +- Commit: #28077755712474bcdef706b843e29225e9d62fa6 developed by @miroslavpojer +- Commit: #2b04213354bf6fefe0ed2014fc37efe25fac219d developed by @miroslavpojer +- Commit: #30b29bd8e349b1a3dfe987c73077ffa6062143d3 developed by @miroslavpojer +- Commit: #31d0f476a4261bc5e95aa9199550b7f34e28ce2f developed by @miroslavpojer +- Commit: #359fa1837cf50a59478c1baa1813ba8dc6c85a02 developed by @MobiTikula +- Commit: #372af6befcd4322a8373546ac7d2d2ada2df3239 developed by @miroslavpojer +- Commit: #37433594e49caac4b6d0be04717159af6524e9ae developed by @miroslavpojer +- Commit: #388a86018dfebff2a2a600c35ed4465ff46b6513 developed by @miroslavpojer +- Commit: #39262f97c18c2ea6e92840f3f391f9b9f21fbee9 developed by @MobiTikula +- Commit: #3acab3020b87add18536c0540930e14fea8735a5 developed by @miroslavpojer +- Commit: #3e2962b2a47c5e0b6c158919917e34ec030fbe46 developed by @miroslavpojer +- Commit: #431948ff52f2b3b2cd02937e37ce12db206ed409 developed by @miroslavpojer +- Commit: #4376c86428544a078cb9f4d4e94d5e04dffc90f2 developed by @miroslavpojer +- Commit: #45a5167d5ed8c9eb55d7e46fba59538984cea9ab developed by @miroslavpojer +- Commit: #45a6c4cf9ac216cb33283d266230a05ac5515f56 developed by @miroslavpojer +- Commit: #473435ef03bd134d313be99cce33230c94f121df developed by @MobiTikula +- Commit: #51515b9a7511a6fc9730381f084c1b3825c42a9c developed by @miroslavpojer +- Commit: #54c1701c6ec362a12ac410712f38a1b7c3e43a6f developed by @miroslavpojer +- Commit: #55573e7c2c735748f15ee3047715a3a8090c82f7 developed by @miroslavpojer +- Commit: #5773353f27a19b70023f441e0dfad0c521a61b2c developed by @MobiTikula +- Commit: #5af8b6541b5e7fc363b9c82680ab3ed5a80b9d00 developed by @miroslavpojer +- Commit: #5d56fb0093a55f0ac7be00b062b5d7435f2f7149 developed by @miroslavpojer +- Commit: #5e0819b024848ea47c816216d4d2611bf34325b4 developed by @miroslavpojer co-authored by @Zejnilovic +- Commit: #626c834012b9ff03be19ed136360fab156a39820 developed by @miroslavpojer +- Commit: #63a7c3649501e018e9058a212ec4ff3a7fb93f0f developed by @miroslavpojer +- Commit: #652257c0b0d6731cabb01d9a5250ccd705acf797 developed by @MobiTikula +- Commit: #6621a866272ecc11ccbc076932a2f4cf66c300f6 developed by @miroslavpojer +- Commit: #66c7ac96b6ac00cad18e0625325ef01e854c0ed4 developed by @miroslavpojer +- Commit: #6af306a84587e07279488c015a627524e914654f developed by @miroslavpojer +- Commit: #6d591b5a66520cc63454782d361ac2206049ad97 developed by @miroslavpojer +- Commit: #732e0d88b189753bae3107208bca15896f43ba91 developed by @miroslavpojer +- Commit: #73e53e7de0e1d32ea71f869c55974a4a4382928e developed by @miroslavpojer +- Commit: #793b152c85aadbfe1d629843be6ec13172781616 developed by @miroslavpojer +- Commit: #7b561dfd2c6aa10b35e9a0da742ec892c453fa5b developed by @miroslavpojer +- Commit: #7cf308c9fb871ca04007c86caebb417deff62fb2 developed by @miroslavpojer +- Commit: #7e2869e4af6f0c923dc94b32818caf0fb996ab3b developed by @miroslavpojer +- Commit: #7ef6ff7bf9ec287015a2089cb492df821cdc89bc developed by @miroslavpojer +- Commit: #8026c42ec13fe041a32497949d7547e3776f9a0c developed by @miroslavpojer +- Commit: #8329068487c8411405ae26283845d7da372229f0 developed by @MobiTikula +- Commit: #8bee957dad3e6b8292947fe0048c955031bab48c developed by @miroslavpojer +- Commit: #8cc758fdfd78c05182a607ae6766267a513a3b1b developed by @miroslavpojer +- Commit: #8dbfbd02cb95566cecef5d634d1ed3d011c3ebb7 developed by @MobiTikula +- Commit: #91a2f977cfe62673f884b8c3ea7d1c5676278c05 developed by @miroslavpojer +- Commit: #937431a9115c62e58a2632436a6a5e7da597602e developed by @MobiTikula +- Commit: #95e4b7511bd31f087adb237ca5295be4192f16a2 developed by @miroslavpojer +- Commit: #97b9045f27e5c2bc4c6ec6d8568753fb86576f3c developed by @miroslavpojer +- Commit: #99a5832f6352271f0b3c957dd3ff0659be9b7f9a developed by @miroslavpojer +- Commit: #a88317e4a1e4d71e9833730596d221372e250601 developed by @miroslavpojer +- Commit: #a9de37fb1ce57a347d6bd48f28822a4ba151449e developed by @miroslavpojer +- Commit: #ab08ab8fb88ad9b512c6af229f45fbd2f5ccd59c developed by @miroslavpojer +- Commit: #ac73f8a8df0c5daecaf3ab1bb12714c314bc3adc developed by @miroslavpojer +- Commit: #ad133333efa1804c9985dd5a74841edba545050a developed by @miroslavpojer +- Commit: #adcbb15e28a894f0164f46f013ccbdbb5fb332f0 developed by @MobiTikula +- Commit: #b02f55063ed92817f82845cbd6ebdd3ff3de831c developed by @MobiTikula +- Commit: #b0d0156d1667e8f54f8905f3f86d96a0e5d24570 developed by @miroslavpojer +- Commit: #b452951e76bfd4c3aa75afbc5adce220d6bb75e3 developed by @miroslavpojer +- Commit: #b56d5081a9abf6936fe6cd9f152c519681b7f8a0 developed by @miroslavpojer +- Commit: #b88b26e3e6c1564e147e67245d273f302d09dcfb developed by @MobiTikula +- Commit: #bb182b9c265e42362c463867701072fe6323aca5 developed by @miroslavpojer +- Commit: #bbfaba8537f8653726a09605f4f07eb2deb395da developed by @MobiTikula +- Commit: #bc4b15b27680ee87c0e6ad8a4c3ea4903bc57c7e developed by @miroslavpojer +- Commit: #be301dae3750fd22de8e71e50e687d67c8586457 developed by @miroslavpojer +- Commit: #bf6fbe45fd2d8f4c38fa4e1d43d8c694255e6c69 developed by @miroslavpojer +- Commit: #c005044299b3dcf8431e007d50fc7e8ea416edf1 developed by @miroslavpojer +- Commit: #c1e777634ad116ee1fb43696ae714183d1823cb1 developed by @MobiTikula +- Commit: #c5190a640fe08e52f656eaf5efdb430cc8316ccd developed by @miroslavpojer +- Commit: #c5a1eacee2ff8fbd71f3633ee6f89247d6cdba85 developed by @miroslavpojer +- Commit: #cb326bfa04e9d7ef0e6f4126c9f5623032b4949a developed by @miroslavpojer +- Commit: #d111b1a81cb26f9695af2114a1b718d553b26791 developed by @miroslavpojer +- Commit: #d1c9cd08118bee7f0e4a77e859dbd86f48c84ba2 developed by @miroslavpojer +- Commit: #d4ef636e0bdba9a796b9daa82f0a70964b19b702 developed by @miroslavpojer +- Commit: #d63a04d93b4234b5436522b412b4cff773c3711d developed by @miroslavpojer +- Commit: #da4f2b79a4f95ef9af2d7b10a974261b047c8647 developed by @miroslavpojer +- Commit: #dafee2947bb3e243e18e29bf5d374b52aa96650a developed by @miroslavpojer +- Commit: #dbc2e4c0b335ea65c6ef8fc63a7453fd9f8d1281 developed by @miroslavpojer +- Commit: #dd8077dbaeb3f932acefc5bb80676be12db05c66 developed by @miroslavpojer +- Commit: #df07e8f6ed2951781bdd41003dab830d3cf9853f developed by @miroslavpojer +- Commit: #df0b851314d44c908475d01305f0d77c6fec153b developed by @miroslavpojer +- Commit: #e15c3cdc66f3c629eaabe37bc62a8edab76b36ca developed by @MobiTikula +- Commit: #e2b9add863066bfb705484c93b1f0847414d05f5 developed by @miroslavpojer +- Commit: #e32a6ea8a05c7d48d5ab30a545119c718e3a3d37 developed by @miroslavpojer +- Commit: #e6b762e10943105c4e2eeebf47450f2523c14066 developed by @miroslavpojer +- Commit: #e8880e6f87bc1409d8873db80319aa01e3ab3138 developed by @miroslavpojer co-authored by @Zejnilovic +- Commit: #e8d31f7e3f762a9dcc4702b00288c2fb09ed4c4d developed by @miroslavpojer +- Commit: #ea32e110bfbfd7c8ff93423526fd104683f1654b developed by @miroslavpojer +- Commit: #ea77959ea7eadd0b45e88beb2b6076135ec1915c developed by @miroslavpojer +- Commit: #ec5f22a668f6a2dd78bafc57fc374b4f3a2b308e developed by @miroslavpojer +- Commit: #ed1abf3683fdfcdb2e1151dd429f13d83fe38d71 developed by @miroslavpojer +- Commit: #ef6c0511f4a47285de9a81148099e27c92697801 developed by @miroslavpojer +- Commit: #ef8af9f0438c976b200ff2f07b29c0c141376b28 developed by @miroslavpojer +- Commit: #f07ac856c926049947f99099cdf352b69ffae468 developed by @miroslavpojer +- Commit: #f13f8492165f1d546434d78300058fe498b66d36 developed by @miroslavpojer +- Commit: #f1f8a8c60e468a33cb09ccf676da549cc4655e23 developed by @MobiTikula +- Commit: #f3be9de73279f971b1d63bec7726989b1e9a4ebf developed by @miroslavpojer +- Commit: #f5ce04ef13ee4588ba5f5ee37d091f0843a3c8a1 developed by @miroslavpojer +- Commit: #f71e781a33799a5b25d32bc3f5079059e93ed116 developed by @MobiTikula +- Commit: #f871bbc609bdd91e2b02fd7f70a10090c1d2cb7c developed by @miroslavpojer +- Commit: #f87a5ed12bb41de12c8351ce139dd426341e68f4 developed by @miroslavpojer +- Commit: #fbd219a6d4d739cac5e69b8e0f727e1a666fea91 developed by @MobiTikula +- Commit: #fbe8e558f914cd58d8e7aab8c7d0c77f934aa707 developed by @miroslavpojer ### Others - No Topic ⚠️ -Previous filters caught all Issues or Pull Requests. +- PR: #60 _Test change to test close of PR instead of Merge._ assigned to @miroslavpojer developed by @miroslavpojer +- PR: #65 _Fake change in PR to get PR._ developed by @Zejnilovic, @miroslavpojer +- PR: #92 _Fake change._ developed by @miroslavpojer #### Full Changelog -https://github.com/company/test-project/commits/v0.1.0 +https://github.com/absa-group/living-doc-example-project/commits/v0.1.0 diff --git a/release_notes_generator/action_inputs.py b/release_notes_generator/action_inputs.py index af349834..9e879640 100644 --- a/release_notes_generator/action_inputs.py +++ b/release_notes_generator/action_inputs.py @@ -39,7 +39,7 @@ DUPLICITY_ICON, ROW_FORMAT_LINK_PR, ROW_FORMAT_ISSUE, - ROW_FORMAT_PR, + ROW_FORMAT_PR, ROW_FORMAT_COMMIT, ROW_FORMAT_LINK_COMMIT, ) from release_notes_generator.utils.enums import DuplicityScopeEnum from release_notes_generator.utils.gh_action import get_action_input @@ -176,6 +176,13 @@ def get_row_format_pr() -> str: """ return get_action_input(ROW_FORMAT_PR, "#{number} _{title}_ {assignee} {developed-by} {co-authored-by}").strip() + @staticmethod + def get_row_format_commit() -> str: + """ + Get the commit row format for the release notes. + """ + return get_action_input(ROW_FORMAT_COMMIT, "#{sha} {author} {co-authored-by}").strip() + @staticmethod def get_row_format_link_pr() -> bool: """ @@ -183,6 +190,13 @@ def get_row_format_link_pr() -> bool: """ return get_action_input(ROW_FORMAT_LINK_PR, "true").lower() == "true" + @staticmethod + def get_row_format_link_commit() -> bool: + """ + Get the value controlling whether the row format should include a 'Commit:' prefix when linking to direct commit. + """ + return get_action_input(ROW_FORMAT_LINK_COMMIT, "true").lower() == "true" + @staticmethod def validate_inputs(): """ @@ -230,6 +244,9 @@ def validate_inputs(): row_format_link_pr = ActionInputs.get_row_format_link_pr() ActionInputs.validate_input(row_format_link_pr, bool, "'row-format-link-pr' value must be a boolean.", errors) + row_format_link_commit = ActionInputs.get_row_format_link_commit() + ActionInputs.validate_input(row_format_link_commit, bool, "'row-format-link-commit' value must be a boolean.", errors) + # Features print_empty_chapters = ActionInputs.get_print_empty_chapters() ActionInputs.validate_input(print_empty_chapters, bool, "Print empty chapters must be a boolean.", errors) @@ -251,6 +268,12 @@ def validate_inputs(): errors.extend(detect_row_format_invalid_keywords(row_format_pr, row_type="PR")) + row_format_commit = ActionInputs.get_row_format_commit() + if not isinstance(row_format_commit, str) or not row_format_commit.strip(): + errors.append("Commit Row format must be a non-empty string.") + + errors.extend(detect_row_format_invalid_keywords(row_format_commit, row_type="Commit")) + # Log errors if any if errors: for error in errors: @@ -260,9 +283,17 @@ def validate_inputs(): logging.debug("Repository: %s/%s", owner, repo_name) logger.debug("Tag name: %s", tag_name) logger.debug("Chapters JSON: %s", chapters_json) + logger.debug("Duplication scope: %s", ActionInputs.get_duplicity_scope()) + logger.debug("Duplication icon: %s", duplicity_icon) + logger.debug("Warnings: %s", warnings) logger.debug("Published at: %s", published_at) logger.debug("Skip release notes label: %s", skip_release_notes_label) - logger.debug("Verbose logging: %s", verbose) - logger.debug("Warnings: %s", warnings) logger.debug("Print empty chapters: %s", print_empty_chapters) logger.debug("Chapters to PR without issue: %s", chapters_to_pr_without_issue) + logger.debug("Verbose logging: %s", verbose) + logger.debug("Github repository: %s", repository_id) + logger.debug("Row format issue: %s", row_format_issue) + logger.debug("Row format PR: %s", row_format_pr) + logger.debug("Row format commit: %s", row_format_commit) + logger.debug("Row format link PR: %s", row_format_link_pr) + logger.debug("Row format link commit: %s", row_format_commit) diff --git a/release_notes_generator/model/base_record.py b/release_notes_generator/model/base_record.py index 234d3d4e..3872f8e5 100644 --- a/release_notes_generator/model/base_record.py +++ b/release_notes_generator/model/base_record.py @@ -246,12 +246,10 @@ def get_rls_notes(self, detection_pattern=RELEASE_NOTE_DETECTION_PATTERN, line_m for line in body_lines: if detection_pattern in line: - logger.debug("Hello rls notes gen - pr.number %s, line: %s, detection_pattern: %s", pull.number, line, detection_pattern) inside_release_notes = True if detection_pattern not in line and inside_release_notes: if line.startswith(line_mark): - logger.debug("Hello rls notes gen - new line: %s", line) release_notes += f" {line.strip()}\n" else: break @@ -267,7 +265,6 @@ def contains_release_notes(self) -> bool: rls_notes = self.get_rls_notes() # if RELEASE_NOTE_LINE_MARK in self.get_rls_notes(): if RELEASE_NOTE_LINE_MARK in rls_notes: - logger.debug("DEBUG: Detected rls notes in value: %s", rls_notes) self.__is_release_note_detected = True return self.__is_release_note_detected @@ -317,6 +314,9 @@ def _get_row_format_values(self, row_format: str) -> dict: if "{assignees}" in row_format: assignees = self.assignees format_values["assignees"] = f"assigned to @{assignees}" if assignees is not None else "" + if "{author}" in row_format: + developers = self.developers + format_values["author"] = f"developed by {developers}" if developers is not None else "" if "{developed-by}" in row_format: developers = self.developers format_values["developed-by"] = f"developed by {developers}" if developers is not None else "" diff --git a/release_notes_generator/model/commit_record.py b/release_notes_generator/model/commit_record.py index b5bfb7ef..600598dd 100644 --- a/release_notes_generator/model/commit_record.py +++ b/release_notes_generator/model/commit_record.py @@ -84,7 +84,7 @@ def assignees(self) -> Optional[str]: @property def developers(self) -> Optional[str]: """Get the developers of the record.""" - return f"{self.commits[0].author.login}" if self.commits else None + return f"@{self.commits[0].author.login}" if self.commits else None @property def contributors(self) -> Optional[str]: @@ -131,13 +131,16 @@ def to_chapter_row(self) -> str: """ self.increment_present_in_chapters() row_prefix = f"{ActionInputs.get_duplicity_icon()} " if self.present_in_chapters > 1 else "" + format_values = { + "sha": self.__commit.sha if self.__commit is not None else None, + } - commit_sha = self.__commit.sha if self.__commit is not None else None - row = f"{row_prefix}Commit: {commit_sha} developed by {self.developers}" + format_values.update(self._get_row_format_values(ActionInputs.get_row_format_commit())) - contributors = self.contributors - if contributors: - row += f" co-authored by {contributors}." + prefix = "Commit: " if ActionInputs.get_row_format_link_commit() else "" + row = f"{row_prefix}{prefix}" + ActionInputs.get_row_format_commit().format(**format_values) + if self.contains_release_notes(): + row = f"{row}\n{self.get_rls_notes()}" return row.replace(" ", " ") diff --git a/release_notes_generator/utils/constants.py b/release_notes_generator/utils/constants.py index 11391342..e9de4ee5 100644 --- a/release_notes_generator/utils/constants.py +++ b/release_notes_generator/utils/constants.py @@ -31,8 +31,12 @@ RUNNER_DEBUG = "RUNNER_DEBUG" ROW_FORMAT_ISSUE = "row-format-issue" ROW_FORMAT_PR = "row-format-pr" +ROW_FORMAT_COMMIT = "row-format-commit" ROW_FORMAT_LINK_PR = "row-format-link-pr" -SUPPORTED_ROW_FORMAT_KEYS = ["number", "title", "pull-requests", "assignee", "assignees", "developed-by", "co-authored-by"] +ROW_FORMAT_LINK_COMMIT = "row-format-link-commit" +SUPPORTED_ROW_ISSUE_FORMAT_KEYS = ["number", "title", "pull-requests", "assignee", "assignees", "developed-by", "co-authored-by"] +SUPPORTED_ROW_PR_FORMAT_KEYS = ["number", "title", "assignee", "assignees", "developed-by", "co-authored-by"] +SUPPORTED_ROW_COMMIT_FORMAT_KEYS = ["sha", "author", "co-authored-by"] # Features WARNINGS = "warnings" diff --git a/release_notes_generator/utils/exceptions.py b/release_notes_generator/utils/exceptions.py new file mode 100644 index 00000000..12563240 --- /dev/null +++ b/release_notes_generator/utils/exceptions.py @@ -0,0 +1,3 @@ + +class NotSupportedException(Exception): + pass diff --git a/release_notes_generator/utils/utils.py b/release_notes_generator/utils/utils.py index fe10dc9d..b363336f 100644 --- a/release_notes_generator/utils/utils.py +++ b/release_notes_generator/utils/utils.py @@ -26,7 +26,9 @@ from github.GitRelease import GitRelease from github.Repository import Repository -from release_notes_generator.utils.constants import SUPPORTED_ROW_FORMAT_KEYS +from release_notes_generator.utils.constants import SUPPORTED_ROW_ISSUE_FORMAT_KEYS, \ + SUPPORTED_ROW_PR_FORMAT_KEYS, SUPPORTED_ROW_COMMIT_FORMAT_KEYS +from release_notes_generator.utils.exceptions import NotSupportedException logger = logging.getLogger(__name__) @@ -69,7 +71,17 @@ def detect_row_format_invalid_keywords(row_format: str, row_type: str = "Issue") """ errors = [] keywords_in_braces = re.findall(r"\{(.*?)\}", row_format) - invalid_keywords = [keyword for keyword in keywords_in_braces if keyword not in SUPPORTED_ROW_FORMAT_KEYS] + match row_type: + case "Issue": + supported_keys = SUPPORTED_ROW_ISSUE_FORMAT_KEYS + case "PR": + supported_keys = SUPPORTED_ROW_PR_FORMAT_KEYS + case "Commit": + supported_keys = SUPPORTED_ROW_COMMIT_FORMAT_KEYS + case _: + raise NotSupportedException(f"Row type '{row_type}' is not supported.") + + invalid_keywords = [keyword for keyword in keywords_in_braces if keyword not in supported_keys] if invalid_keywords: errors.append(f"Invalid {row_type} row format '{row_format}'. Invalid keyword(s) found: {', '.join(invalid_keywords)}") return errors From d5726548282ed8a2ee27a41e76be83fdd7095a73 Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Mon, 7 Oct 2024 21:57:09 +0200 Subject: [PATCH 45/51] - Fix commit row format. - Improved README to provide better description about format keyword and data sources. --- README.md | 88 ++++++--- action.yml | 2 +- examples/output_example.md | 226 +++++++++++------------ release_notes_generator/action_inputs.py | 2 +- 4 files changed, 176 insertions(+), 142 deletions(-) diff --git a/README.md b/README.md index 52fc3766..e803d4e8 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ - [Built-in](#built-in) - [Release Notes Extraction Process](#release-notes-extraction-process) - [Contributors Mention](#contributors-mention) - - [Handling Multiple PRs](#handling-multiple-prs) + - [Handling Issue Mentioned By Multiple PRs](#handling-issue-mentioned-by-multiple-prs) - [No Release Notes Found](#no-release-notes-found) - [Select start date for closed issues and PRs](#select-start-date-for-closed-issues-and-prs) - [Enable skipping of release notes for specific issues using label](#enable-skipping-of-release-notes-for-specific-issues-using-label) @@ -115,6 +115,29 @@ The output of the action is a markdown string containing the release notes for t See the [example of output](./examples/output_example.md). +### Supported Row Types +#### Issue Row +An issue row may have multiple pull requests linked to it. These pull requests are associated using GitHub-supported keywords like closes, fixes, or resolves. + +**Example** +- #33 _Example bugfix_ in [#44](https://github.com/absa-group/living-doc-example-project/pull/44), [#36](https://github.com/absa-group/living-doc-example-project/pull/36), [#35](https://github.com/absa-group/living-doc-example-project/pull/35), [#34](https://github.com/absa-group/living-doc-example-project/pull/34) assigned to @miroslavpojer developed by @miroslavpojer co-authored by Saša Zejnilović + - Another solved typos. Hello from second RLS notes comment. + - Solved some typos. + +#### Pull Request Row +A pull request row represents one PR made against the repository. This pull request does not mention any issues. + +**Example** +- PR: #41 _Initial commit._ assigned to @miroslavpojer developed by @miroslavpojer + - Test release notes nr1 + - Test release notes nr2 + +#### Direct Commit Row +A direct commit row represents a commit that is not tied to any pull request or issue. This commit is not associated with any pull request. + +**Example** +- Commit: fbe8e558f914cd58d8e7aab8c7d0c77f934aa707 developed by @miroslavpojer + ## Usage Example ### Prerequisites @@ -174,7 +197,7 @@ Add the following step to your GitHub workflow (in example are used non-default ## Features ### Built-in #### Release Notes Support -This action enables GitHub pull requests to include a dedicated section for release notes, making it easier for maintainers to track changes and updates. +This action enables GitHub pull requests to include a dedicated section for release notes in its description, making it easier for maintainers to track changes and updates. - **Format:** The section must begin with the title `Release Notes:`, followed by the release notes in bullet points. - **Example:** Here is an example of how to structure the release notes (case-sensitive): @@ -183,39 +206,50 @@ Release Notes: - This update introduces a new caching mechanism that improves performance by 20%. ``` - **Best Practice:** Use `-` for bullet points. The Markdown parser will automatically format them as a list. -- **Optional:** Including release notes is not mandatory for the action to function. If a pull request does not include a Release Notes: section, it will be flagged accordingly, helping maintainers identify which PRs require documentation updates. +- **Optional:** Including release notes is not mandatory for the action of this GH action. The action scans pull request descriptions for the `Release Notes:` section and extracts any content that follows the specified format. -Additionally, the action tracks issues closed after the most recent release, using the release creation time as a reference point. This ensures that all relevant updates since the last release are captured. -#### Handling Multiple PRs -If an issue is linked to multiple PRs, the action fetches and aggregates contributions from all linked PRs. +#### Handling Issue Mentioned By Multiple PRs +If an issue is linked from multiple PRs, the action fetches and aggregates developers and contributions from all linked PRs. #### No Release Notes Found -If no valid "Release Notes" comment is found in an issue, it will be marked accordingly. This helps maintainers quickly identify which issues need attention for documentation. +If no valid `Release Notes:` section is found in a pull request description, it will be mentioned in dedicated service chapters. This helps maintainers quickly identify which pull request need attention for documentation. -#### Issue or PR Row formatting -Format of the row for the issue and PR in the release notes can be customized. The placeholders are case-sensitive. +#### Issue, Pull Request or Commit Row formatting +Format of the different row types can be customized. The placeholders are case-sensitive. Each row type supports different set of keywords. **Supported row format keywords:** -- `{number}`: Issue or PR number. -- `{title}`: Issue or PR title. -- `{pull-requests}`: List of PRs linked to the issue. Adds a list of PRs linked to the issue in the row with `in` prefix: - - `#{number} _{title}_ {pull-requests}` => "[#43]() _title_ in [#PR1](), [#PR2](), [#PR3]()" - - Not used in PR row format. See default value. -- `{assingee}`: Issue or PR assignee. Adds a login of assignees in the row with `assigned to` prefix: - - `#{number} _{title}_ {assignee}` => "[#43]() _title_ implemented by @login1" - - TODO - mention problem with public and private email -- `{assignees}`: Issue or PR assignees. Adds a list of assignees logins in the row with `assigned to` prefix: - - `#{number} _{title}_ {assignees}` => "[#43]() _title_ implemented by @login1, @login2" - - This is alternative representation of multiple assignees provided by GitHub. -- `{developed-by}`: List of PR developer(s) login(s). Adds a login of commit authors for PR(s) in the row with `developed by` prefix: - - `#{number} _{title}_ {developed-by}` => "[#43]() _title_ developed by @login1" - - TODO - mention problem with public and private email -- `{co-authored-by}`: List of PR contributors. Adds a login of contributors in the row with `co-authored by` prefix: - - `#{number} _{title}_ {co-authored-by}` => "[#43]() _title_ co-authored by @login1 @login2" - - Contribution is detected in PR commit messages by detection of GitHub supported trailer `Co-authored-by`. - - TODO - mention problem with public and private email +- **Issue & Pull Request** + - `{number}`: + - Issue or PR number. + - `{title}`: + - Issue or PR title. + - `{pull-requests}`: + - List of PRs linked to the issue. Adds a list of PRs linked to the issue in the row with `in` prefix: + - _Example:_ `#{number} _{title}_ {pull-requests}` => "[#43]() _title_ in [#PR1](), [#PR2](), [#PR3]()" + - Not used in PR row format. Pull Request type already define single PR. + - `{assingee}`: + - Issue or PR assignee. Adds a login of assignees in the row with `assigned to` prefix: + - `#{number} _{title}_ {assignee}` => "[#43]() _title_ implemented by @login1" + - `{assignees}`: + - Issue or PR assignees. Adds a list of assignees logins in the row with `assigned to` prefix: + - `#{number} _{title}_ {assignees}` => "[#43]() _title_ implemented by @login1, @login2" + - This is alternative representation of multiple assignees provided by GitHub. + - `{developed-by}`: + - List of PR developer(s) login(s). Adds a login of commit authors for PR(s) in the row with `developed by` prefix: + - `#{number} _{title}_ {developed-by}` => "[#43]() _title_ developed by @login1" + - `{co-authored-by}`: + - List of PR contributors. Adds a login of contributors in the row with `co-authored by` prefix: + - `#{number} _{title}_ {co-authored-by}` => "[#43]() _title_ co-authored by @login1 @login2" + - Co-authors are detected in PR commit messages by detection of GitHub supported trailer `Co-authored-by`. +- **Commit** + - `{sha}`: + - Commit SHA. + - `{author}`: + - Commit author login. + - `{co-authors}`: + - List of commit contributors. Co-authors are detected in commit messages by detection of GitHub supported trailer `Co-authored-by`. ### Select start date for closed issues and PRs By set **published-at** to true the action will use the `published-at` timestamp of the latest release as the reference point for searching closed issues and PRs, instead of the `created-at` date. If first release, repository creation date is used. diff --git a/action.yml b/action.yml index 79e94812..af66205a 100644 --- a/action.yml +++ b/action.yml @@ -66,7 +66,7 @@ inputs: row-format-commit: description: 'Format of the commit row in the release notes. Available placeholders: {sha}, {author}, {co-authored-by}. Placeholders are case-insensitive.' required: false - default: '#{sha} {author} {co-authored-by}' + default: '{sha} {author} {co-authored-by}' row-format-link-pr: description: 'Add prefix "PR:" before link to PR when not linked an Issue.' required: false diff --git a/examples/output_example.md b/examples/output_example.md index f24c4b97..36f26dd0 100644 --- a/examples/output_example.md +++ b/examples/output_example.md @@ -72,122 +72,122 @@ All closed PRs are linked to issues. - #81 _Test multiline excludes in filename inspector related yml_ in [#79](https://github.com/absa-group/living-doc-example-project/pull/79), [#78](https://github.com/absa-group/living-doc-example-project/pull/78), [#77](https://github.com/absa-group/living-doc-example-project/pull/77), [#76](https://github.com/absa-group/living-doc-example-project/pull/76), [#75](https://github.com/absa-group/living-doc-example-project/pull/75), [#74](https://github.com/absa-group/living-doc-example-project/pull/74), [#73](https://github.com/absa-group/living-doc-example-project/pull/73), [#72](https://github.com/absa-group/living-doc-example-project/pull/72), [#71](https://github.com/absa-group/living-doc-example-project/pull/71), [#70](https://github.com/absa-group/living-doc-example-project/pull/70), [#69](https://github.com/absa-group/living-doc-example-project/pull/69), [#68](https://github.com/absa-group/living-doc-example-project/pull/68), [#67](https://github.com/absa-group/living-doc-example-project/pull/67) developed by @miroslavpojer ### Isolated commits without Issue or PR ⚠️ -- Commit: #0079dab11f408346aeb755fd8feda74692798b70 developed by @MobiTikula -- Commit: #02d4c4942ea5b84f0772c30d7eb1656584e711d5 developed by @miroslavpojer -- Commit: #07e7d0b4c55ae77e76a25a09a6cffa0dd4ba64a0 developed by @miroslavpojer -- Commit: #1133a080f5b20f7233b1c581d8bed05ed8004bda developed by @MobiTikula -- Commit: #1245a0e1aac79d2f829b8c830cbf013cea2ee377 developed by @miroslavpojer -- Commit: #15e21c60c75dbcdf3c450e2d527f970ec58b13dd developed by @miroslavpojer -- Commit: #1ab371b180aa256d97ce083ae718ea00d051d1a0 developed by @miroslavpojer -- Commit: #1d9893e931a4fa4bcf14ddbdc769a61ee849f324 developed by @miroslavpojer -- Commit: #1ea2b6c56e3748a2263a04dd4ddfeddd1bcf950a developed by @miroslavpojer -- Commit: #2474c94397a01d9c415d36a81f00c968cecc8f7e developed by @miroslavpojer -- Commit: #24e3341c32700dce7087ac90eb67e50686e37bdf developed by @miroslavpojer -- Commit: #265b572f822e298b25a1e5f3b15f8bb687b28689 developed by @MobiTikula -- Commit: #28077755712474bcdef706b843e29225e9d62fa6 developed by @miroslavpojer -- Commit: #2b04213354bf6fefe0ed2014fc37efe25fac219d developed by @miroslavpojer -- Commit: #30b29bd8e349b1a3dfe987c73077ffa6062143d3 developed by @miroslavpojer -- Commit: #31d0f476a4261bc5e95aa9199550b7f34e28ce2f developed by @miroslavpojer -- Commit: #359fa1837cf50a59478c1baa1813ba8dc6c85a02 developed by @MobiTikula -- Commit: #372af6befcd4322a8373546ac7d2d2ada2df3239 developed by @miroslavpojer -- Commit: #37433594e49caac4b6d0be04717159af6524e9ae developed by @miroslavpojer -- Commit: #388a86018dfebff2a2a600c35ed4465ff46b6513 developed by @miroslavpojer -- Commit: #39262f97c18c2ea6e92840f3f391f9b9f21fbee9 developed by @MobiTikula -- Commit: #3acab3020b87add18536c0540930e14fea8735a5 developed by @miroslavpojer -- Commit: #3e2962b2a47c5e0b6c158919917e34ec030fbe46 developed by @miroslavpojer -- Commit: #431948ff52f2b3b2cd02937e37ce12db206ed409 developed by @miroslavpojer -- Commit: #4376c86428544a078cb9f4d4e94d5e04dffc90f2 developed by @miroslavpojer -- Commit: #45a5167d5ed8c9eb55d7e46fba59538984cea9ab developed by @miroslavpojer -- Commit: #45a6c4cf9ac216cb33283d266230a05ac5515f56 developed by @miroslavpojer -- Commit: #473435ef03bd134d313be99cce33230c94f121df developed by @MobiTikula -- Commit: #51515b9a7511a6fc9730381f084c1b3825c42a9c developed by @miroslavpojer -- Commit: #54c1701c6ec362a12ac410712f38a1b7c3e43a6f developed by @miroslavpojer -- Commit: #55573e7c2c735748f15ee3047715a3a8090c82f7 developed by @miroslavpojer -- Commit: #5773353f27a19b70023f441e0dfad0c521a61b2c developed by @MobiTikula -- Commit: #5af8b6541b5e7fc363b9c82680ab3ed5a80b9d00 developed by @miroslavpojer -- Commit: #5d56fb0093a55f0ac7be00b062b5d7435f2f7149 developed by @miroslavpojer -- Commit: #5e0819b024848ea47c816216d4d2611bf34325b4 developed by @miroslavpojer co-authored by @Zejnilovic -- Commit: #626c834012b9ff03be19ed136360fab156a39820 developed by @miroslavpojer -- Commit: #63a7c3649501e018e9058a212ec4ff3a7fb93f0f developed by @miroslavpojer -- Commit: #652257c0b0d6731cabb01d9a5250ccd705acf797 developed by @MobiTikula -- Commit: #6621a866272ecc11ccbc076932a2f4cf66c300f6 developed by @miroslavpojer -- Commit: #66c7ac96b6ac00cad18e0625325ef01e854c0ed4 developed by @miroslavpojer -- Commit: #6af306a84587e07279488c015a627524e914654f developed by @miroslavpojer -- Commit: #6d591b5a66520cc63454782d361ac2206049ad97 developed by @miroslavpojer -- Commit: #732e0d88b189753bae3107208bca15896f43ba91 developed by @miroslavpojer -- Commit: #73e53e7de0e1d32ea71f869c55974a4a4382928e developed by @miroslavpojer -- Commit: #793b152c85aadbfe1d629843be6ec13172781616 developed by @miroslavpojer -- Commit: #7b561dfd2c6aa10b35e9a0da742ec892c453fa5b developed by @miroslavpojer -- Commit: #7cf308c9fb871ca04007c86caebb417deff62fb2 developed by @miroslavpojer -- Commit: #7e2869e4af6f0c923dc94b32818caf0fb996ab3b developed by @miroslavpojer -- Commit: #7ef6ff7bf9ec287015a2089cb492df821cdc89bc developed by @miroslavpojer -- Commit: #8026c42ec13fe041a32497949d7547e3776f9a0c developed by @miroslavpojer -- Commit: #8329068487c8411405ae26283845d7da372229f0 developed by @MobiTikula -- Commit: #8bee957dad3e6b8292947fe0048c955031bab48c developed by @miroslavpojer -- Commit: #8cc758fdfd78c05182a607ae6766267a513a3b1b developed by @miroslavpojer -- Commit: #8dbfbd02cb95566cecef5d634d1ed3d011c3ebb7 developed by @MobiTikula -- Commit: #91a2f977cfe62673f884b8c3ea7d1c5676278c05 developed by @miroslavpojer -- Commit: #937431a9115c62e58a2632436a6a5e7da597602e developed by @MobiTikula -- Commit: #95e4b7511bd31f087adb237ca5295be4192f16a2 developed by @miroslavpojer -- Commit: #97b9045f27e5c2bc4c6ec6d8568753fb86576f3c developed by @miroslavpojer -- Commit: #99a5832f6352271f0b3c957dd3ff0659be9b7f9a developed by @miroslavpojer -- Commit: #a88317e4a1e4d71e9833730596d221372e250601 developed by @miroslavpojer -- Commit: #a9de37fb1ce57a347d6bd48f28822a4ba151449e developed by @miroslavpojer -- Commit: #ab08ab8fb88ad9b512c6af229f45fbd2f5ccd59c developed by @miroslavpojer -- Commit: #ac73f8a8df0c5daecaf3ab1bb12714c314bc3adc developed by @miroslavpojer -- Commit: #ad133333efa1804c9985dd5a74841edba545050a developed by @miroslavpojer -- Commit: #adcbb15e28a894f0164f46f013ccbdbb5fb332f0 developed by @MobiTikula -- Commit: #b02f55063ed92817f82845cbd6ebdd3ff3de831c developed by @MobiTikula -- Commit: #b0d0156d1667e8f54f8905f3f86d96a0e5d24570 developed by @miroslavpojer -- Commit: #b452951e76bfd4c3aa75afbc5adce220d6bb75e3 developed by @miroslavpojer -- Commit: #b56d5081a9abf6936fe6cd9f152c519681b7f8a0 developed by @miroslavpojer -- Commit: #b88b26e3e6c1564e147e67245d273f302d09dcfb developed by @MobiTikula -- Commit: #bb182b9c265e42362c463867701072fe6323aca5 developed by @miroslavpojer -- Commit: #bbfaba8537f8653726a09605f4f07eb2deb395da developed by @MobiTikula -- Commit: #bc4b15b27680ee87c0e6ad8a4c3ea4903bc57c7e developed by @miroslavpojer -- Commit: #be301dae3750fd22de8e71e50e687d67c8586457 developed by @miroslavpojer -- Commit: #bf6fbe45fd2d8f4c38fa4e1d43d8c694255e6c69 developed by @miroslavpojer -- Commit: #c005044299b3dcf8431e007d50fc7e8ea416edf1 developed by @miroslavpojer -- Commit: #c1e777634ad116ee1fb43696ae714183d1823cb1 developed by @MobiTikula -- Commit: #c5190a640fe08e52f656eaf5efdb430cc8316ccd developed by @miroslavpojer -- Commit: #c5a1eacee2ff8fbd71f3633ee6f89247d6cdba85 developed by @miroslavpojer -- Commit: #cb326bfa04e9d7ef0e6f4126c9f5623032b4949a developed by @miroslavpojer -- Commit: #d111b1a81cb26f9695af2114a1b718d553b26791 developed by @miroslavpojer -- Commit: #d1c9cd08118bee7f0e4a77e859dbd86f48c84ba2 developed by @miroslavpojer -- Commit: #d4ef636e0bdba9a796b9daa82f0a70964b19b702 developed by @miroslavpojer -- Commit: #d63a04d93b4234b5436522b412b4cff773c3711d developed by @miroslavpojer -- Commit: #da4f2b79a4f95ef9af2d7b10a974261b047c8647 developed by @miroslavpojer -- Commit: #dafee2947bb3e243e18e29bf5d374b52aa96650a developed by @miroslavpojer -- Commit: #dbc2e4c0b335ea65c6ef8fc63a7453fd9f8d1281 developed by @miroslavpojer -- Commit: #dd8077dbaeb3f932acefc5bb80676be12db05c66 developed by @miroslavpojer -- Commit: #df07e8f6ed2951781bdd41003dab830d3cf9853f developed by @miroslavpojer -- Commit: #df0b851314d44c908475d01305f0d77c6fec153b developed by @miroslavpojer -- Commit: #e15c3cdc66f3c629eaabe37bc62a8edab76b36ca developed by @MobiTikula -- Commit: #e2b9add863066bfb705484c93b1f0847414d05f5 developed by @miroslavpojer -- Commit: #e32a6ea8a05c7d48d5ab30a545119c718e3a3d37 developed by @miroslavpojer -- Commit: #e6b762e10943105c4e2eeebf47450f2523c14066 developed by @miroslavpojer -- Commit: #e8880e6f87bc1409d8873db80319aa01e3ab3138 developed by @miroslavpojer co-authored by @Zejnilovic -- Commit: #e8d31f7e3f762a9dcc4702b00288c2fb09ed4c4d developed by @miroslavpojer -- Commit: #ea32e110bfbfd7c8ff93423526fd104683f1654b developed by @miroslavpojer -- Commit: #ea77959ea7eadd0b45e88beb2b6076135ec1915c developed by @miroslavpojer -- Commit: #ec5f22a668f6a2dd78bafc57fc374b4f3a2b308e developed by @miroslavpojer -- Commit: #ed1abf3683fdfcdb2e1151dd429f13d83fe38d71 developed by @miroslavpojer -- Commit: #ef6c0511f4a47285de9a81148099e27c92697801 developed by @miroslavpojer -- Commit: #ef8af9f0438c976b200ff2f07b29c0c141376b28 developed by @miroslavpojer -- Commit: #f07ac856c926049947f99099cdf352b69ffae468 developed by @miroslavpojer -- Commit: #f13f8492165f1d546434d78300058fe498b66d36 developed by @miroslavpojer -- Commit: #f1f8a8c60e468a33cb09ccf676da549cc4655e23 developed by @MobiTikula -- Commit: #f3be9de73279f971b1d63bec7726989b1e9a4ebf developed by @miroslavpojer -- Commit: #f5ce04ef13ee4588ba5f5ee37d091f0843a3c8a1 developed by @miroslavpojer -- Commit: #f71e781a33799a5b25d32bc3f5079059e93ed116 developed by @MobiTikula -- Commit: #f871bbc609bdd91e2b02fd7f70a10090c1d2cb7c developed by @miroslavpojer -- Commit: #f87a5ed12bb41de12c8351ce139dd426341e68f4 developed by @miroslavpojer -- Commit: #fbd219a6d4d739cac5e69b8e0f727e1a666fea91 developed by @MobiTikula -- Commit: #fbe8e558f914cd58d8e7aab8c7d0c77f934aa707 developed by @miroslavpojer +- Commit: 0079dab11f408346aeb755fd8feda74692798b70 developed by @MobiTikula +- Commit: 02d4c4942ea5b84f0772c30d7eb1656584e711d5 developed by @miroslavpojer +- Commit: 07e7d0b4c55ae77e76a25a09a6cffa0dd4ba64a0 developed by @miroslavpojer +- Commit: 1133a080f5b20f7233b1c581d8bed05ed8004bda developed by @MobiTikula +- Commit: 1245a0e1aac79d2f829b8c830cbf013cea2ee377 developed by @miroslavpojer +- Commit: 15e21c60c75dbcdf3c450e2d527f970ec58b13dd developed by @miroslavpojer +- Commit: 1ab371b180aa256d97ce083ae718ea00d051d1a0 developed by @miroslavpojer +- Commit: 1d9893e931a4fa4bcf14ddbdc769a61ee849f324 developed by @miroslavpojer +- Commit: 1ea2b6c56e3748a2263a04dd4ddfeddd1bcf950a developed by @miroslavpojer +- Commit: 2474c94397a01d9c415d36a81f00c968cecc8f7e developed by @miroslavpojer +- Commit: 24e3341c32700dce7087ac90eb67e50686e37bdf developed by @miroslavpojer +- Commit: 265b572f822e298b25a1e5f3b15f8bb687b28689 developed by @MobiTikula +- Commit: 28077755712474bcdef706b843e29225e9d62fa6 developed by @miroslavpojer +- Commit: 2b04213354bf6fefe0ed2014fc37efe25fac219d developed by @miroslavpojer +- Commit: 30b29bd8e349b1a3dfe987c73077ffa6062143d3 developed by @miroslavpojer +- Commit: 31d0f476a4261bc5e95aa9199550b7f34e28ce2f developed by @miroslavpojer +- Commit: 359fa1837cf50a59478c1baa1813ba8dc6c85a02 developed by @MobiTikula +- Commit: 372af6befcd4322a8373546ac7d2d2ada2df3239 developed by @miroslavpojer +- Commit: 37433594e49caac4b6d0be04717159af6524e9ae developed by @miroslavpojer +- Commit: 388a86018dfebff2a2a600c35ed4465ff46b6513 developed by @miroslavpojer +- Commit: 39262f97c18c2ea6e92840f3f391f9b9f21fbee9 developed by @MobiTikula +- Commit: 3acab3020b87add18536c0540930e14fea8735a5 developed by @miroslavpojer +- Commit: 3e2962b2a47c5e0b6c158919917e34ec030fbe46 developed by @miroslavpojer +- Commit: 431948ff52f2b3b2cd02937e37ce12db206ed409 developed by @miroslavpojer +- Commit: 4376c86428544a078cb9f4d4e94d5e04dffc90f2 developed by @miroslavpojer +- Commit: 45a5167d5ed8c9eb55d7e46fba59538984cea9ab developed by @miroslavpojer +- Commit: 45a6c4cf9ac216cb33283d266230a05ac5515f56 developed by @miroslavpojer +- Commit: 473435ef03bd134d313be99cce33230c94f121df developed by @MobiTikula +- Commit: 51515b9a7511a6fc9730381f084c1b3825c42a9c developed by @miroslavpojer +- Commit: 54c1701c6ec362a12ac410712f38a1b7c3e43a6f developed by @miroslavpojer +- Commit: 55573e7c2c735748f15ee3047715a3a8090c82f7 developed by @miroslavpojer +- Commit: 5773353f27a19b70023f441e0dfad0c521a61b2c developed by @MobiTikula +- Commit: 5af8b6541b5e7fc363b9c82680ab3ed5a80b9d00 developed by @miroslavpojer +- Commit: 5d56fb0093a55f0ac7be00b062b5d7435f2f7149 developed by @miroslavpojer +- Commit: 5e0819b024848ea47c816216d4d2611bf34325b4 developed by @miroslavpojer co-authored by @Zejnilovic +- Commit: 626c834012b9ff03be19ed136360fab156a39820 developed by @miroslavpojer +- Commit: 63a7c3649501e018e9058a212ec4ff3a7fb93f0f developed by @miroslavpojer +- Commit: 652257c0b0d6731cabb01d9a5250ccd705acf797 developed by @MobiTikula +- Commit: 6621a866272ecc11ccbc076932a2f4cf66c300f6 developed by @miroslavpojer +- Commit: 66c7ac96b6ac00cad18e0625325ef01e854c0ed4 developed by @miroslavpojer +- Commit: 6af306a84587e07279488c015a627524e914654f developed by @miroslavpojer +- Commit: 6d591b5a66520cc63454782d361ac2206049ad97 developed by @miroslavpojer +- Commit: 732e0d88b189753bae3107208bca15896f43ba91 developed by @miroslavpojer +- Commit: 73e53e7de0e1d32ea71f869c55974a4a4382928e developed by @miroslavpojer +- Commit: 793b152c85aadbfe1d629843be6ec13172781616 developed by @miroslavpojer +- Commit: 7b561dfd2c6aa10b35e9a0da742ec892c453fa5b developed by @miroslavpojer +- Commit: 7cf308c9fb871ca04007c86caebb417deff62fb2 developed by @miroslavpojer +- Commit: 7e2869e4af6f0c923dc94b32818caf0fb996ab3b developed by @miroslavpojer +- Commit: 7ef6ff7bf9ec287015a2089cb492df821cdc89bc developed by @miroslavpojer +- Commit: 8026c42ec13fe041a32497949d7547e3776f9a0c developed by @miroslavpojer +- Commit: 8329068487c8411405ae26283845d7da372229f0 developed by @MobiTikula +- Commit: 8bee957dad3e6b8292947fe0048c955031bab48c developed by @miroslavpojer +- Commit: 8cc758fdfd78c05182a607ae6766267a513a3b1b developed by @miroslavpojer +- Commit: 8dbfbd02cb95566cecef5d634d1ed3d011c3ebb7 developed by @MobiTikula +- Commit: 91a2f977cfe62673f884b8c3ea7d1c5676278c05 developed by @miroslavpojer +- Commit: 937431a9115c62e58a2632436a6a5e7da597602e developed by @MobiTikula +- Commit: 95e4b7511bd31f087adb237ca5295be4192f16a2 developed by @miroslavpojer +- Commit: 97b9045f27e5c2bc4c6ec6d8568753fb86576f3c developed by @miroslavpojer +- Commit: 99a5832f6352271f0b3c957dd3ff0659be9b7f9a developed by @miroslavpojer +- Commit: a88317e4a1e4d71e9833730596d221372e250601 developed by @miroslavpojer +- Commit: a9de37fb1ce57a347d6bd48f28822a4ba151449e developed by @miroslavpojer +- Commit: ab08ab8fb88ad9b512c6af229f45fbd2f5ccd59c developed by @miroslavpojer +- Commit: ac73f8a8df0c5daecaf3ab1bb12714c314bc3adc developed by @miroslavpojer +- Commit: ad133333efa1804c9985dd5a74841edba545050a developed by @miroslavpojer +- Commit: adcbb15e28a894f0164f46f013ccbdbb5fb332f0 developed by @MobiTikula +- Commit: b02f55063ed92817f82845cbd6ebdd3ff3de831c developed by @MobiTikula +- Commit: b0d0156d1667e8f54f8905f3f86d96a0e5d24570 developed by @miroslavpojer +- Commit: b452951e76bfd4c3aa75afbc5adce220d6bb75e3 developed by @miroslavpojer +- Commit: b56d5081a9abf6936fe6cd9f152c519681b7f8a0 developed by @miroslavpojer +- Commit: b88b26e3e6c1564e147e67245d273f302d09dcfb developed by @MobiTikula +- Commit: bb182b9c265e42362c463867701072fe6323aca5 developed by @miroslavpojer +- Commit: bbfaba8537f8653726a09605f4f07eb2deb395da developed by @MobiTikula +- Commit: bc4b15b27680ee87c0e6ad8a4c3ea4903bc57c7e developed by @miroslavpojer +- Commit: be301dae3750fd22de8e71e50e687d67c8586457 developed by @miroslavpojer +- Commit: bf6fbe45fd2d8f4c38fa4e1d43d8c694255e6c69 developed by @miroslavpojer +- Commit: c005044299b3dcf8431e007d50fc7e8ea416edf1 developed by @miroslavpojer +- Commit: c1e777634ad116ee1fb43696ae714183d1823cb1 developed by @MobiTikula +- Commit: c5190a640fe08e52f656eaf5efdb430cc8316ccd developed by @miroslavpojer +- Commit: c5a1eacee2ff8fbd71f3633ee6f89247d6cdba85 developed by @miroslavpojer +- Commit: cb326bfa04e9d7ef0e6f4126c9f5623032b4949a developed by @miroslavpojer +- Commit: d111b1a81cb26f9695af2114a1b718d553b26791 developed by @miroslavpojer +- Commit: d1c9cd08118bee7f0e4a77e859dbd86f48c84ba2 developed by @miroslavpojer +- Commit: d4ef636e0bdba9a796b9daa82f0a70964b19b702 developed by @miroslavpojer +- Commit: d63a04d93b4234b5436522b412b4cff773c3711d developed by @miroslavpojer +- Commit: da4f2b79a4f95ef9af2d7b10a974261b047c8647 developed by @miroslavpojer +- Commit: dafee2947bb3e243e18e29bf5d374b52aa96650a developed by @miroslavpojer +- Commit: dbc2e4c0b335ea65c6ef8fc63a7453fd9f8d1281 developed by @miroslavpojer +- Commit: dd8077dbaeb3f932acefc5bb80676be12db05c66 developed by @miroslavpojer +- Commit: df07e8f6ed2951781bdd41003dab830d3cf9853f developed by @miroslavpojer +- Commit: df0b851314d44c908475d01305f0d77c6fec153b developed by @miroslavpojer +- Commit: e15c3cdc66f3c629eaabe37bc62a8edab76b36ca developed by @MobiTikula +- Commit: e2b9add863066bfb705484c93b1f0847414d05f5 developed by @miroslavpojer +- Commit: e32a6ea8a05c7d48d5ab30a545119c718e3a3d37 developed by @miroslavpojer +- Commit: e6b762e10943105c4e2eeebf47450f2523c14066 developed by @miroslavpojer +- Commit: e8880e6f87bc1409d8873db80319aa01e3ab3138 developed by @miroslavpojer co-authored by @Zejnilovic +- Commit: e8d31f7e3f762a9dcc4702b00288c2fb09ed4c4d developed by @miroslavpojer +- Commit: ea32e110bfbfd7c8ff93423526fd104683f1654b developed by @miroslavpojer +- Commit: ea77959ea7eadd0b45e88beb2b6076135ec1915c developed by @miroslavpojer +- Commit: ec5f22a668f6a2dd78bafc57fc374b4f3a2b308e developed by @miroslavpojer +- Commit: ed1abf3683fdfcdb2e1151dd429f13d83fe38d71 developed by @miroslavpojer +- Commit: ef6c0511f4a47285de9a81148099e27c92697801 developed by @miroslavpojer +- Commit: ef8af9f0438c976b200ff2f07b29c0c141376b28 developed by @miroslavpojer +- Commit: f07ac856c926049947f99099cdf352b69ffae468 developed by @miroslavpojer +- Commit: f13f8492165f1d546434d78300058fe498b66d36 developed by @miroslavpojer +- Commit: f1f8a8c60e468a33cb09ccf676da549cc4655e23 developed by @MobiTikula +- Commit: f3be9de73279f971b1d63bec7726989b1e9a4ebf developed by @miroslavpojer +- Commit: f5ce04ef13ee4588ba5f5ee37d091f0843a3c8a1 developed by @miroslavpojer +- Commit: f71e781a33799a5b25d32bc3f5079059e93ed116 developed by @MobiTikula +- Commit: f871bbc609bdd91e2b02fd7f70a10090c1d2cb7c developed by @miroslavpojer +- Commit: f87a5ed12bb41de12c8351ce139dd426341e68f4 developed by @miroslavpojer +- Commit: fbd219a6d4d739cac5e69b8e0f727e1a666fea91 developed by @MobiTikula +- Commit: fbe8e558f914cd58d8e7aab8c7d0c77f934aa707 developed by @miroslavpojer ### Others - No Topic ⚠️ - PR: #60 _Test change to test close of PR instead of Merge._ assigned to @miroslavpojer developed by @miroslavpojer -- PR: #65 _Fake change in PR to get PR._ developed by @Zejnilovic, @miroslavpojer +- PR: #65 _Fake change in PR to get PR._ developed by @miroslavpojer, @Zejnilovic - PR: #92 _Fake change._ developed by @miroslavpojer #### Full Changelog diff --git a/release_notes_generator/action_inputs.py b/release_notes_generator/action_inputs.py index 9e879640..16e6911f 100644 --- a/release_notes_generator/action_inputs.py +++ b/release_notes_generator/action_inputs.py @@ -181,7 +181,7 @@ def get_row_format_commit() -> str: """ Get the commit row format for the release notes. """ - return get_action_input(ROW_FORMAT_COMMIT, "#{sha} {author} {co-authored-by}").strip() + return get_action_input(ROW_FORMAT_COMMIT, "{sha} {author} {co-authored-by}").strip() @staticmethod def get_row_format_link_pr() -> bool: From b525e2925ce8a0b92979d556a3fdbc3a6d12fb12 Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Mon, 7 Oct 2024 22:09:23 +0200 Subject: [PATCH 46/51] - Pylint and pytest scope reduced. Tests files excluded. --- .pylintrc | 3 +++ pyproject.toml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/.pylintrc b/.pylintrc index 67b06997..714ca34f 100644 --- a/.pylintrc +++ b/.pylintrc @@ -115,6 +115,9 @@ unsafe-load-any-extension=no # In verbose mode, extra non-checker-related info will be displayed. #verbose= +[MASTER] + +ignore-paths=tests [BASIC] diff --git a/pyproject.toml b/pyproject.toml index 68eb39c1..7cf6438e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,3 +2,6 @@ line-length = 120 target-version = ['py311'] force-exclude = '''test''' + +[tool.coverage.run] +omit = ["tests/*"] From 4bd8460025c00f3d443bcdd553046b9dec8c76bc Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 8 Oct 2024 09:36:14 +0200 Subject: [PATCH 47/51] - Remove no more supported feature. In new version this behavior is default. --- README.md | 23 +++++++++++++--------- action.yml | 5 ----- release_notes_generator/action_inputs.py | 14 ------------- release_notes_generator/utils/constants.py | 1 - 4 files changed, 14 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index e803d4e8..2315ca0c 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,11 @@ - [Usage Example](#usage-example) - [Features](#features) - [Built-in](#built-in) - - [Release Notes Extraction Process](#release-notes-extraction-process) + - [Release Notes Support](#release-notes-support) + - [Handling Issue Mentioned By Multiple PRs](#handling-issue-mentioned-by-multiple-prs) + - [No Release Notes Found](#no-release-notes-found) + - [Issue, Pull Request or Commit Row formatting](#issue-pull-request-or-commit-row-formatting) + - [Supported row format keywords](#supported-row-format-keywords) - [Contributors Mention](#contributors-mention) - [Handling Issue Mentioned By Multiple PRs](#handling-issue-mentioned-by-multiple-prs) - [No Release Notes Found](#no-release-notes-found) @@ -104,11 +108,6 @@ Generate Release Notes action is dedicated to enhance the quality and organizati - **Required**: No - **Default**: true (Empty chapters are printed.) -### `chapters-to-pr-without-issue` -- **Description**: Set it to false to avoid the application of custom chapters for PRs without linked issues. -- **Required**: No -- **Default**: true (Custom chapters are applied to PRs without linked issues.) - ## Outputs The output of the action is a markdown string containing the release notes for the specified tag. This string can be used in subsequent steps to publish the release notes to a file, create a GitHub release, or send notifications. @@ -188,7 +187,6 @@ Add the following step to your GitHub workflow (in example are used non-default warnings: false print-empty-chapters: false - chapters-to-pr-without-issue: false row-format-issue: '#{number} _{title}_ {pull-requests} {assignee} {developed-by} {co-authored-by}' row-format-pr: '#{number} _{title}_ {assignee} {developed-by} {co-authored-by}' row-format-link-pr: true @@ -219,7 +217,7 @@ If no valid `Release Notes:` section is found in a pull request description, it #### Issue, Pull Request or Commit Row formatting Format of the different row types can be customized. The placeholders are case-sensitive. Each row type supports different set of keywords. -**Supported row format keywords:** +##### Supported row format keywords - **Issue & Pull Request** - `{number}`: - Issue or PR number. @@ -404,6 +402,12 @@ pytest tests/ This will execute all tests located in the tests directory and generate a code coverage report. +TODO: add another example for partial run +pytest tests/release_notes_generator/utils/test_utils.py + +debug only - how to run measuremnt on isolated test set +pytest --cov=. tests/release_notes_generator/utils/test_utils.py --cov-fail-under=80 --cov-report=html + ## Code Coverage Code coverage is collected using pytest-cov coverage tool. To run the tests and collect coverage information, use the following command: @@ -412,6 +416,8 @@ Code coverage is collected using pytest-cov coverage tool. To run the tests and pytest --cov=release_notes_generator --cov-report html tests/ ``` + + See the coverage report on the path: ``` @@ -436,7 +442,6 @@ export INPUT_WARNINGS="true" export INPUT_PUBLISHED_AT="true" export INPUT_SKIP_RELEASE_NOTES_LABEL="ignore-in-release" export INPUT_PRINT_EMPTY_CHAPTERS="true" -export INPUT_CHAPTERS_TO_PR_WITHOUT_ISSUE="true" export INPUT_VERBOSE="true" # CI in-build variables diff --git a/action.yml b/action.yml index af66205a..3492062e 100644 --- a/action.yml +++ b/action.yml @@ -47,10 +47,6 @@ inputs: description: 'Print chapters even if they are empty.' required: false default: 'true' - chapters-to-pr-without-issue: - description: 'Apply custom chapters for PRs without linked issues.' - required: false - default: 'true' verbose: description: 'Print verbose logs.' required: false @@ -126,7 +122,6 @@ runs: INPUT_PUBLISHED_AT: ${{ inputs.published-at }} INPUT_SKIP_RELEASE_NOTES_LABEL: ${{ inputs.skip-release-notes-label }} INPUT_PRINT_EMPTY_CHAPTERS: ${{ inputs.print-empty-chapters }} - INPUT_CHAPTERS_TO_PR_WITHOUT_ISSUE: ${{ inputs.chapters-to-pr-without-issue }} INPUT_VERBOSE: ${{ inputs.verbose }} INPUT_GITHUB_REPOSITORY: ${{ github.repository }} INPUT_ROW_FORMAT_ISSUE: ${{ inputs.row-format-issue }} diff --git a/release_notes_generator/action_inputs.py b/release_notes_generator/action_inputs.py index 16e6911f..c8800beb 100644 --- a/release_notes_generator/action_inputs.py +++ b/release_notes_generator/action_inputs.py @@ -34,7 +34,6 @@ WARNINGS, RUNNER_DEBUG, PRINT_EMPTY_CHAPTERS, - CHAPTERS_TO_PR_WITHOUT_ISSUE, DUPLICITY_SCOPE, DUPLICITY_ICON, ROW_FORMAT_LINK_PR, @@ -138,13 +137,6 @@ def get_print_empty_chapters() -> bool: """ return get_action_input(PRINT_EMPTY_CHAPTERS, "true").lower() == "true" - @staticmethod - def get_chapters_to_pr_without_issue() -> bool: - """ - Get the chapters to PR without issue parameter value from the action inputs. - """ - return get_action_input(CHAPTERS_TO_PR_WITHOUT_ISSUE, "true").lower() == "true" - @staticmethod def validate_input(input_value, expected_type: type, error_message: str, error_buffer: list) -> bool: """ @@ -251,11 +243,6 @@ def validate_inputs(): print_empty_chapters = ActionInputs.get_print_empty_chapters() ActionInputs.validate_input(print_empty_chapters, bool, "Print empty chapters must be a boolean.", errors) - chapters_to_pr_without_issue = ActionInputs.get_chapters_to_pr_without_issue() - ActionInputs.validate_input( - chapters_to_pr_without_issue, bool, "Chapters to PR without issue must be a boolean.", errors - ) - row_format_issue = ActionInputs.get_row_format_issue() if not isinstance(row_format_issue, str) or not row_format_issue.strip(): errors.append("Issue row format must be a non-empty string.") @@ -289,7 +276,6 @@ def validate_inputs(): logger.debug("Published at: %s", published_at) logger.debug("Skip release notes label: %s", skip_release_notes_label) logger.debug("Print empty chapters: %s", print_empty_chapters) - logger.debug("Chapters to PR without issue: %s", chapters_to_pr_without_issue) logger.debug("Verbose logging: %s", verbose) logger.debug("Github repository: %s", repository_id) logger.debug("Row format issue: %s", row_format_issue) diff --git a/release_notes_generator/utils/constants.py b/release_notes_generator/utils/constants.py index e9de4ee5..468cd420 100644 --- a/release_notes_generator/utils/constants.py +++ b/release_notes_generator/utils/constants.py @@ -41,7 +41,6 @@ # Features WARNINGS = "warnings" PRINT_EMPTY_CHAPTERS = "print-empty-chapters" -CHAPTERS_TO_PR_WITHOUT_ISSUE = "chapters-to-pr-without-issue" # Pull Request states PR_STATE_CLOSED = "closed" From 0d2d326a87aaf94010566b87248efa8ef500d985 Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 8 Oct 2024 10:19:40 +0200 Subject: [PATCH 48/51] - Applied black. --- release_notes_generator/action_inputs.py | 12 ++++++-- release_notes_generator/generator.py | 2 +- release_notes_generator/model/base_record.py | 5 ++-- .../model/commit_record.py | 5 ++-- release_notes_generator/model/issue_record.py | 13 ++++++--- .../model/pull_request_record.py | 11 ++++---- .../model/service_chapters.py | 28 +++++++++++++------ .../record/record_factory.py | 11 ++++---- release_notes_generator/utils/constants.py | 10 ++++++- release_notes_generator/utils/exceptions.py | 1 - release_notes_generator/utils/utils.py | 11 ++++++-- 11 files changed, 73 insertions(+), 36 deletions(-) diff --git a/release_notes_generator/action_inputs.py b/release_notes_generator/action_inputs.py index c8800beb..f428af39 100644 --- a/release_notes_generator/action_inputs.py +++ b/release_notes_generator/action_inputs.py @@ -38,7 +38,9 @@ DUPLICITY_ICON, ROW_FORMAT_LINK_PR, ROW_FORMAT_ISSUE, - ROW_FORMAT_PR, ROW_FORMAT_COMMIT, ROW_FORMAT_LINK_COMMIT, + ROW_FORMAT_PR, + ROW_FORMAT_COMMIT, + ROW_FORMAT_LINK_COMMIT, ) from release_notes_generator.utils.enums import DuplicityScopeEnum from release_notes_generator.utils.gh_action import get_action_input @@ -159,7 +161,9 @@ def get_row_format_issue() -> str: """ Get the issue row format for the release notes. """ - return get_action_input(ROW_FORMAT_ISSUE, "#{number} _{title}_ {pull-requests} {assignee} {developed-by} {co-authored-by}").strip() + return get_action_input( + ROW_FORMAT_ISSUE, "#{number} _{title}_ {pull-requests} {assignee} {developed-by} {co-authored-by}" + ).strip() @staticmethod def get_row_format_pr() -> str: @@ -237,7 +241,9 @@ def validate_inputs(): ActionInputs.validate_input(row_format_link_pr, bool, "'row-format-link-pr' value must be a boolean.", errors) row_format_link_commit = ActionInputs.get_row_format_link_commit() - ActionInputs.validate_input(row_format_link_commit, bool, "'row-format-link-commit' value must be a boolean.", errors) + ActionInputs.validate_input( + row_format_link_commit, bool, "'row-format-link-commit' value must be a boolean.", errors + ) # Features print_empty_chapters = ActionInputs.get_print_empty_chapters() diff --git a/release_notes_generator/generator.py b/release_notes_generator/generator.py index dc98d413..8ad37d2c 100644 --- a/release_notes_generator/generator.py +++ b/release_notes_generator/generator.py @@ -103,7 +103,7 @@ def generate(self) -> Optional[str]: changelog_url = get_change_url(tag_name=ActionInputs.get_tag_name(), repository=repo, git_release=rls) - rls_notes_records: dict[int|str, Record] = RecordFactory.generate( + rls_notes_records: dict[int | str, Record] = RecordFactory.generate( github=self.github_instance, repo=repo, issues=list(issues), # PaginatedList --> list diff --git a/release_notes_generator/model/base_record.py b/release_notes_generator/model/base_record.py index 3872f8e5..473042e8 100644 --- a/release_notes_generator/model/base_record.py +++ b/release_notes_generator/model/base_record.py @@ -29,7 +29,8 @@ from release_notes_generator.utils.constants import ( RELEASE_NOTE_DETECTION_PATTERN, - RELEASE_NOTE_LINE_MARK, PR_STATE_CLOSED, + RELEASE_NOTE_LINE_MARK, + PR_STATE_CLOSED, ) logger = logging.getLogger(__name__) @@ -125,7 +126,7 @@ def pr_contains_issue_mentions(self) -> bool: pass @abstractmethod - def is_state(self, state:str) -> bool: + def is_state(self, state: str) -> bool: """Check if the record's state is the specified state.""" pass diff --git a/release_notes_generator/model/commit_record.py b/release_notes_generator/model/commit_record.py index 600598dd..a8ef8b48 100644 --- a/release_notes_generator/model/commit_record.py +++ b/release_notes_generator/model/commit_record.py @@ -29,7 +29,8 @@ from release_notes_generator.action_inputs import ActionInputs from release_notes_generator.model.base_record import Record from release_notes_generator.utils.constants import ( - RELEASE_NOTE_DETECTION_PATTERN, RELEASE_NOTE_LINE_MARK, + RELEASE_NOTE_DETECTION_PATTERN, + RELEASE_NOTE_LINE_MARK, ) logger = logging.getLogger(__name__) @@ -92,7 +93,7 @@ def contributors(self) -> Optional[str]: logins = self.get_contributors_for_commit(self.__commit) return ", ".join(logins) if len(logins) > 0 else None - def is_state(self, state:str) -> bool: + def is_state(self, state: str) -> bool: """Check if the record is in the given state.""" return False diff --git a/release_notes_generator/model/issue_record.py b/release_notes_generator/model/issue_record.py index 0b5ab0e2..63850751 100644 --- a/release_notes_generator/model/issue_record.py +++ b/release_notes_generator/model/issue_record.py @@ -95,7 +95,9 @@ def developers(self) -> Optional[str]: logins.add(f"@{commit.author.login}") if not logins: - logger.warning("Found issue record %s with %d pull requests and no commits", self.id, len(self.__pulls_requests)) + logger.warning( + "Found issue record %s with %d pull requests and no commits", self.id, len(self.__pulls_requests) + ) return ", ".join(logins) if len(logins) > 0 else None @@ -112,7 +114,7 @@ def contributors(self) -> Optional[str]: return ", ".join(logins) if len(logins) > 0 else None - def is_state(self, state:str) -> bool: + def is_state(self, state: str) -> bool: """Check if the record is in a specific state.""" return self.__issue.state == state if self.__issue is not None else False @@ -125,7 +127,10 @@ def pr_links(self) -> Optional[str]: if len(self.__pulls_requests) == 0: return None - res = [self.LINK_TO_PR_TEMPLATE.format(number=pull.number, full_name=self._repo.full_name) for pull in self.__pulls_requests] + res = [ + self.LINK_TO_PR_TEMPLATE.format(number=pull.number, full_name=self._repo.full_name) + for pull in self.__pulls_requests + ] return ", ".join(res) def register_pull_request(self, pr: PullRequest) -> None: @@ -166,7 +171,7 @@ def to_chapter_row(self) -> str: format_values = { "number": self.__issue.number, "title": self.__issue.title, - "pull-requests": f"in {self.pr_links()}" if len(self.__pulls_requests) > 0 else "" + "pull-requests": f"in {self.pr_links()}" if len(self.__pulls_requests) > 0 else "", } format_values.update(self._get_row_format_values(ActionInputs.get_row_format_issue())) diff --git a/release_notes_generator/model/pull_request_record.py b/release_notes_generator/model/pull_request_record.py index 26f84634..76ae00b5 100644 --- a/release_notes_generator/model/pull_request_record.py +++ b/release_notes_generator/model/pull_request_record.py @@ -103,7 +103,7 @@ def contributors(self) -> Optional[str]: return ", ".join(logins) if len(logins) > 0 else None - def is_state(self, state:str) -> bool: + def is_state(self, state: str) -> bool: """Check if the record is in a specific state.""" return self.__pull_request.state == state if self.__pull_request is not None else False @@ -134,7 +134,9 @@ def register_commit(self, commit: Commit) -> bool: sha = commit.sha if sha == self.__pull_request.merge_commit_sha or sha == self.__pull_request.head.sha: self.__commits.append(commit) - logger.debug("Commit %s registered using sha in PR %s of record %s", commit.sha, self.__pull_request.number, self.id) + logger.debug( + "Commit %s registered using sha in PR %s of record %s", commit.sha, self.__pull_request.number, self.id + ) return True return False @@ -147,10 +149,7 @@ def to_chapter_row(self) -> str: """ self.increment_present_in_chapters() row_prefix = f"{ActionInputs.get_duplicity_icon()} " if self.present_in_chapters > 1 else "" - format_values = { - "number": self.__pull_request.number, - "title": self.__pull_request.title - } + format_values = {"number": self.__pull_request.number, "title": self.__pull_request.title} format_values.update(self._get_row_format_values(ActionInputs.get_row_format_pr())) diff --git a/release_notes_generator/model/service_chapters.py b/release_notes_generator/model/service_chapters.py index b632c413..363c46bd 100644 --- a/release_notes_generator/model/service_chapters.py +++ b/release_notes_generator/model/service_chapters.py @@ -32,7 +32,11 @@ MERGED_PRS_WITHOUT_ISSUE_AND_USER_DEFINED_LABELS, CLOSED_PRS_WITHOUT_ISSUE_AND_USER_DEFINED_LABELS, MERGED_PRS_LINKED_TO_NOT_CLOSED_ISSUES, - OTHERS_NO_TOPIC, ISOLATED_COMMITS, ISSUE_STATE_CLOSED, ISSUE_STATE_OPEN, PR_STATE_CLOSED, + OTHERS_NO_TOPIC, + ISOLATED_COMMITS, + ISSUE_STATE_CLOSED, + ISSUE_STATE_OPEN, + PR_STATE_CLOSED, ) from release_notes_generator.utils.enums import DuplicityScopeEnum @@ -94,7 +98,7 @@ def __init__( self.show_chapter_merged_prs_linked_to_open_issues = True - def populate(self, records: dict[int|str, Record]) -> None: + def populate(self, records: dict[int | str, Record]) -> None: """ Populates the service chapters with records. @@ -117,9 +121,17 @@ def populate(self, records: dict[int|str, Record]) -> None: self.__populate_pr(records[nr], nr) else: - if isinstance(records[nr], IssueRecord) and records[nr].is_state(ISSUE_STATE_OPEN) and len(records[nr].pull_requests) == 0: + if ( + isinstance(records[nr], IssueRecord) + and records[nr].is_state(ISSUE_STATE_OPEN) + and len(records[nr].pull_requests) == 0 + ): pass - elif isinstance(records[nr], IssueRecord) and records[nr].is_state(ISSUE_STATE_OPEN) and len(records[nr].pull_requests) > 0: + elif ( + isinstance(records[nr], IssueRecord) + and records[nr].is_state(ISSUE_STATE_OPEN) + and len(records[nr].pull_requests) > 0 + ): self.chapters[MERGED_PRS_LINKED_TO_NOT_CLOSED_ISSUES].add_row(nr, records[nr].to_chapter_row()) self.used_record_numbers.append(nr) else: @@ -200,9 +212,9 @@ def __populate_pr(self, r: Record, nr: int) -> None: # check record properties if it fits to a chapter: CLOSED_PRS_WITHOUT_ISSUE elif ( - r.is_state(PR_STATE_CLOSED) - and not r.pr_contains_issue_mentions - and not r.contains_min_one_label(self.user_defined_labels) + r.is_state(PR_STATE_CLOSED) + and not r.pr_contains_issue_mentions + and not r.contains_min_one_label(self.user_defined_labels) ): if self.__is_row_present(nr) and not self.duplicity_allowed(): return @@ -218,7 +230,7 @@ def __populate_pr(self, r: Record, nr: int) -> None: self.chapters[OTHERS_NO_TOPIC].add_row(nr, r.to_chapter_row()) self.used_record_numbers.append(nr) - def __is_row_present(self, nr: int|str) -> bool: + def __is_row_present(self, nr: int | str) -> bool: return nr in self.used_record_numbers @staticmethod diff --git a/release_notes_generator/record/record_factory.py b/release_notes_generator/record/record_factory.py index 055263ee..0c7cc887 100644 --- a/release_notes_generator/record/record_factory.py +++ b/release_notes_generator/record/record_factory.py @@ -39,6 +39,7 @@ # TODO - make record types a classes + # pylint: disable=too-few-public-methods class RecordFactory: """ @@ -48,7 +49,7 @@ class RecordFactory: @staticmethod def generate( github: Github, repo: Repository, issues: list[Issue], pulls: list[PullRequest], commits: list[Commit] - ) -> dict[int|str, Record]: + ) -> dict[int | str, Record]: """ Generate records for release notes. @@ -91,12 +92,12 @@ def register_pull_request(pull: PullRequest): parent_issue_number, ) - records: dict[int|str, Record] = {} - records_for_isolated_commits: dict[int|str, Record] = {} + records: dict[int | str, Record] = {} + records_for_isolated_commits: dict[int | str, Record] = {} pull_numbers = [pull.number for pull in pulls] logger.debug("Creating records from issue.") - real_issue_counts = len(issues) # issues could contain PRs too - known behaviour from API + real_issue_counts = len(issues) # issues could contain PRs too - known behaviour from API for issue in issues: if issue.number not in pull_numbers: logger.debug("Calling create issue for number %s", issue.number) @@ -141,6 +142,6 @@ def register_pull_request(pull: PullRequest): real_issue_counts, len(pulls), len(commits), - len(records_for_isolated_commits) + len(records_for_isolated_commits), ) return records diff --git a/release_notes_generator/utils/constants.py b/release_notes_generator/utils/constants.py index 468cd420..4a9afa88 100644 --- a/release_notes_generator/utils/constants.py +++ b/release_notes_generator/utils/constants.py @@ -34,7 +34,15 @@ ROW_FORMAT_COMMIT = "row-format-commit" ROW_FORMAT_LINK_PR = "row-format-link-pr" ROW_FORMAT_LINK_COMMIT = "row-format-link-commit" -SUPPORTED_ROW_ISSUE_FORMAT_KEYS = ["number", "title", "pull-requests", "assignee", "assignees", "developed-by", "co-authored-by"] +SUPPORTED_ROW_ISSUE_FORMAT_KEYS = [ + "number", + "title", + "pull-requests", + "assignee", + "assignees", + "developed-by", + "co-authored-by", +] SUPPORTED_ROW_PR_FORMAT_KEYS = ["number", "title", "assignee", "assignees", "developed-by", "co-authored-by"] SUPPORTED_ROW_COMMIT_FORMAT_KEYS = ["sha", "author", "co-authored-by"] diff --git a/release_notes_generator/utils/exceptions.py b/release_notes_generator/utils/exceptions.py index 12563240..9d01dd7a 100644 --- a/release_notes_generator/utils/exceptions.py +++ b/release_notes_generator/utils/exceptions.py @@ -1,3 +1,2 @@ - class NotSupportedException(Exception): pass diff --git a/release_notes_generator/utils/utils.py b/release_notes_generator/utils/utils.py index b363336f..1562316f 100644 --- a/release_notes_generator/utils/utils.py +++ b/release_notes_generator/utils/utils.py @@ -26,8 +26,11 @@ from github.GitRelease import GitRelease from github.Repository import Repository -from release_notes_generator.utils.constants import SUPPORTED_ROW_ISSUE_FORMAT_KEYS, \ - SUPPORTED_ROW_PR_FORMAT_KEYS, SUPPORTED_ROW_COMMIT_FORMAT_KEYS +from release_notes_generator.utils.constants import ( + SUPPORTED_ROW_ISSUE_FORMAT_KEYS, + SUPPORTED_ROW_PR_FORMAT_KEYS, + SUPPORTED_ROW_COMMIT_FORMAT_KEYS, +) from release_notes_generator.utils.exceptions import NotSupportedException logger = logging.getLogger(__name__) @@ -83,5 +86,7 @@ def detect_row_format_invalid_keywords(row_format: str, row_type: str = "Issue") invalid_keywords = [keyword for keyword in keywords_in_braces if keyword not in supported_keys] if invalid_keywords: - errors.append(f"Invalid {row_type} row format '{row_format}'. Invalid keyword(s) found: {', '.join(invalid_keywords)}") + errors.append( + f"Invalid {row_type} row format '{row_format}'. Invalid keyword(s) found: {', '.join(invalid_keywords)}" + ) return errors From abdce26c3373ef80d50d0c6751ed844bd0616e0f Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 8 Oct 2024 10:53:59 +0200 Subject: [PATCH 49/51] - Fixed problem with few space in bullet list. --- examples/output_example.md | 20 +++++++++---------- release_notes_generator/model/base_record.py | 4 ++-- .../model/commit_record.py | 3 ++- release_notes_generator/model/issue_record.py | 3 ++- .../model/pull_request_record.py | 3 ++- 5 files changed, 18 insertions(+), 15 deletions(-) diff --git a/examples/output_example.md b/examples/output_example.md index 36f26dd0..d3ee59e3 100644 --- a/examples/output_example.md +++ b/examples/output_example.md @@ -3,17 +3,17 @@ No entries detected. ### New Features 🎉 - #22 _REQ: Submit Reviews_ in [#31](https://github.com/absa-group/living-doc-example-project/pull/31) developed by @miroslavpojer -- Now only book purchasers can submit reviews, with mandatory text and star ratings. + - Now only book purchasers can submit reviews, with mandatory text and star ratings. - #52 _Add Tag into Release Draft_ in [#59](https://github.com/absa-group/living-doc-example-project/pull/59), [#58](https://github.com/absa-group/living-doc-example-project/pull/58), [#57](https://github.com/absa-group/living-doc-example-project/pull/57), [#56](https://github.com/absa-group/living-doc-example-project/pull/56), [#55](https://github.com/absa-group/living-doc-example-project/pull/55), [#54](https://github.com/absa-group/living-doc-example-project/pull/54), [#53](https://github.com/absa-group/living-doc-example-project/pull/53) assigned to @miroslavpojer developed by @miroslavpojer - #82 _Create tag after success RLS notes generation_ in [#87](https://github.com/absa-group/living-doc-example-project/pull/87), [#86](https://github.com/absa-group/living-doc-example-project/pull/86), [#85](https://github.com/absa-group/living-doc-example-project/pull/85), [#84](https://github.com/absa-group/living-doc-example-project/pull/84), [#83](https://github.com/absa-group/living-doc-example-project/pull/83) assigned to @miroslavpojer developed by @miroslavpojer ### Bugfixes 🛠 - #33 _Example bugfix_ in [#44](https://github.com/absa-group/living-doc-example-project/pull/44), [#36](https://github.com/absa-group/living-doc-example-project/pull/36), [#35](https://github.com/absa-group/living-doc-example-project/pull/35), [#34](https://github.com/absa-group/living-doc-example-project/pull/34) assigned to @miroslavpojer developed by @miroslavpojer co-authored by Saša Zejnilović -- Another solved typos. Hello from second RLS notes comment. -- Solved some typos. + - Another solved typos. Hello from second RLS notes comment. + - Solved some typos. - PR: #41 _Initial commit._ assigned to @miroslavpojer developed by @miroslavpojer -- Test release notes nr1 -- Test release notes nr2 + - Test release notes nr1 + - Test release notes nr2 ### Closed Issues without Pull Request ⚠️ - #3 _FEAT: User Authentication_ assigned to @miroslavpojer @@ -47,13 +47,13 @@ All closed PRs are linked to issues. - #20 _REQ: Search by Keywords_ in [#44](https://github.com/absa-group/living-doc-example-project/pull/44) assigned to @miroslavpojer developed by @miroslavpojer - PR: #26 _Initial test headers_ assigned to @miroslavpojer developed by @miroslavpojer - 🔔 #33 _Example bugfix_ in [#44](https://github.com/absa-group/living-doc-example-project/pull/44), [#36](https://github.com/absa-group/living-doc-example-project/pull/36), [#35](https://github.com/absa-group/living-doc-example-project/pull/35), [#34](https://github.com/absa-group/living-doc-example-project/pull/34) assigned to @miroslavpojer developed by @miroslavpojer co-authored by Saša Zejnilović -- Another solved typos. Hello from second RLS notes comment. -- Solved some typos. + - Another solved typos. Hello from second RLS notes comment. + - Solved some typos. - PR: #39 _Initial commit._ assigned to @miroslavpojer developed by @miroslavpojer - PR: #40 _Initial commit._ assigned to @miroslavpojer developed by @miroslavpojer - 🔔 PR: #41 _Initial commit._ assigned to @miroslavpojer developed by @miroslavpojer -- Test release notes nr1 -- Test release notes nr2 + - Test release notes nr1 + - Test release notes nr2 - PR: #42 _Initial commit._ developed by @miroslavpojer - PR: #43 _Feature/new tag_ developed by @miroslavpojer - PR: #45 _Initial commit._ developed by @miroslavpojer @@ -187,7 +187,7 @@ All closed PRs are linked to issues. ### Others - No Topic ⚠️ - PR: #60 _Test change to test close of PR instead of Merge._ assigned to @miroslavpojer developed by @miroslavpojer -- PR: #65 _Fake change in PR to get PR._ developed by @miroslavpojer, @Zejnilovic +- PR: #65 _Fake change in PR to get PR._ developed by @Zejnilovic, @miroslavpojer - PR: #92 _Fake change._ developed by @miroslavpojer #### Full Changelog diff --git a/release_notes_generator/model/base_record.py b/release_notes_generator/model/base_record.py index 473042e8..2981deef 100644 --- a/release_notes_generator/model/base_record.py +++ b/release_notes_generator/model/base_record.py @@ -250,8 +250,8 @@ def get_rls_notes(self, detection_pattern=RELEASE_NOTE_DETECTION_PATTERN, line_m inside_release_notes = True if detection_pattern not in line and inside_release_notes: - if line.startswith(line_mark): - release_notes += f" {line.strip()}\n" + if line.strip().startswith(line_mark): + release_notes += f" {line}\n" else: break diff --git a/release_notes_generator/model/commit_record.py b/release_notes_generator/model/commit_record.py index a8ef8b48..9b540f3e 100644 --- a/release_notes_generator/model/commit_record.py +++ b/release_notes_generator/model/commit_record.py @@ -140,10 +140,11 @@ def to_chapter_row(self) -> str: prefix = "Commit: " if ActionInputs.get_row_format_link_commit() else "" row = f"{row_prefix}{prefix}" + ActionInputs.get_row_format_commit().format(**format_values) + row = row.replace(" ", " ") if self.contains_release_notes(): row = f"{row}\n{self.get_rls_notes()}" - return row.replace(" ", " ") + return row def fetch_pr_commits(self) -> None: """Fetch the pull request commits of the record.""" diff --git a/release_notes_generator/model/issue_record.py b/release_notes_generator/model/issue_record.py index 63850751..2fb9c8fb 100644 --- a/release_notes_generator/model/issue_record.py +++ b/release_notes_generator/model/issue_record.py @@ -177,10 +177,11 @@ def to_chapter_row(self) -> str: format_values.update(self._get_row_format_values(ActionInputs.get_row_format_issue())) row = f"{row_prefix}" + ActionInputs.get_row_format_issue().format(**format_values) + row = row.replace(" ", " ") if self.contains_release_notes(): row = f"{row}\n{self.get_rls_notes()}" - return row.replace(" ", " ") + return row def fetch_pr_commits(self) -> None: """Fetch commits of the record's pull requests.""" diff --git a/release_notes_generator/model/pull_request_record.py b/release_notes_generator/model/pull_request_record.py index 76ae00b5..e5d80fd6 100644 --- a/release_notes_generator/model/pull_request_record.py +++ b/release_notes_generator/model/pull_request_record.py @@ -155,10 +155,11 @@ def to_chapter_row(self) -> str: pr_prefix = "PR: " if ActionInputs.get_row_format_link_pr() else "" row = f"{row_prefix}{pr_prefix}" + ActionInputs.get_row_format_pr().format(**format_values) + row = row.replace(" ", " ") if self.contains_release_notes(): row = f"{row}\n{self.get_rls_notes()}" - return row.replace(" ", " ") + return row def fetch_pr_commits(self) -> None: """Fetches the commits of the record.""" From 20d19ae93725fe65585211d98c8de9353e98dfe7 Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 8 Oct 2024 13:08:43 +0200 Subject: [PATCH 50/51] - Fixed unit tests for utils.py. - Fixed typo in name of files. --- release_notes_generator/model/issue_record.py | 2 +- .../model/pull_request_record.py | 2 +- .../record/record_factory.py | 2 +- ...reuqest_utils.py => pull_request_utils.py} | 0 .../utils/test_utils.py | 28 ++++++++++++++++--- 5 files changed, 27 insertions(+), 7 deletions(-) rename release_notes_generator/utils/{pull_reuqest_utils.py => pull_request_utils.py} (100%) diff --git a/release_notes_generator/model/issue_record.py b/release_notes_generator/model/issue_record.py index 2fb9c8fb..c91cd4d8 100644 --- a/release_notes_generator/model/issue_record.py +++ b/release_notes_generator/model/issue_record.py @@ -28,7 +28,7 @@ from release_notes_generator.action_inputs import ActionInputs from release_notes_generator.model.base_record import Record -from release_notes_generator.utils.pull_reuqest_utils import extract_issue_numbers_from_body +from release_notes_generator.utils.pull_request_utils import extract_issue_numbers_from_body logger = logging.getLogger(__name__) diff --git a/release_notes_generator/model/pull_request_record.py b/release_notes_generator/model/pull_request_record.py index e5d80fd6..dbaa636c 100644 --- a/release_notes_generator/model/pull_request_record.py +++ b/release_notes_generator/model/pull_request_record.py @@ -28,7 +28,7 @@ from release_notes_generator.action_inputs import ActionInputs from release_notes_generator.model.base_record import Record -from release_notes_generator.utils.pull_reuqest_utils import extract_issue_numbers_from_body +from release_notes_generator.utils.pull_request_utils import extract_issue_numbers_from_body logger = logging.getLogger(__name__) diff --git a/release_notes_generator/record/record_factory.py b/release_notes_generator/record/record_factory.py index 0c7cc887..74f7a5cf 100644 --- a/release_notes_generator/record/record_factory.py +++ b/release_notes_generator/record/record_factory.py @@ -33,7 +33,7 @@ from release_notes_generator.utils.decorators import safe_call_decorator from release_notes_generator.utils.github_rate_limiter import GithubRateLimiter -from release_notes_generator.utils.pull_reuqest_utils import extract_issue_numbers_from_body +from release_notes_generator.utils.pull_request_utils import extract_issue_numbers_from_body logger = logging.getLogger(__name__) diff --git a/release_notes_generator/utils/pull_reuqest_utils.py b/release_notes_generator/utils/pull_request_utils.py similarity index 100% rename from release_notes_generator/utils/pull_reuqest_utils.py rename to release_notes_generator/utils/pull_request_utils.py diff --git a/tests/release_notes_generator/utils/test_utils.py b/tests/release_notes_generator/utils/test_utils.py index c7d3f15f..aae1d262 100644 --- a/tests/release_notes_generator/utils/test_utils.py +++ b/tests/release_notes_generator/utils/test_utils.py @@ -14,6 +14,9 @@ # limitations under the License. # +from pytest import raises + +from release_notes_generator.utils.exceptions import NotSupportedException from release_notes_generator.utils.utils import get_change_url, detect_row_format_invalid_keywords @@ -38,14 +41,31 @@ def test_get_change_url_with_git_release(mock_repo, mock_git_release): # detect_row_format_invalid_keywords -def test_valid_row_format(): - row_format = "{number} - {title} in {pull-requests}" +def test_valid_row_format_issue(): + row_format = "{number} - {title} in {pull-requests} {assignee} {assignees} {developed-by} {co-authored-by}" errors = detect_row_format_invalid_keywords(row_format) assert not errors, "Expected no errors for valid keywords" +def test_valid_row_format_pr(): + row_format = "{number} - {title} {assignee} {assignees} {developed-by} {co-authored-by}" + errors = detect_row_format_invalid_keywords(row_format, row_type="PR") + assert not errors, "Expected no errors for valid keywords" + + +def test_valid_row_format_commit(): + row_format = "{sha} - {author} {co-authored-by}" + errors = detect_row_format_invalid_keywords(row_format, row_type="Commit") + assert not errors, "Expected no errors for valid keywords" + + +def test_valid_row_format_another(): + with raises(NotSupportedException, match="Row type 'another' is not supported."): + detect_row_format_invalid_keywords("_", row_type="another") + + def test_multiple_invalid_keywords(): row_format = "{number} - {link} - {Title} and {Pull-requests}" - errors = detect_row_format_invalid_keywords(row_format) + errors = detect_row_format_invalid_keywords(row_format, row_type="Issue") assert len(errors) == 1 - assert "Invalid Issue row format keyword(s) found: link, Title, Pull-requests" in errors[0] + assert "Invalid Issue row format '{number} - {link} - {Title} and {Pull-requests}'. Invalid keyword(s) found: link, Title, Pull-requests" in errors[0] From e6428d7f801cb88752de44203f777ffb0a463af6 Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Tue, 8 Oct 2024 13:30:40 +0200 Subject: [PATCH 51/51] - Fixed unit tests for utils directory. --- release_notes_generator/utils/decorators.py | 6 +- .../utils/test_decorators.py | 93 +++++++++++++++++-- ...st_utils.py => test_pull_request_utils.py} | 2 +- 3 files changed, 88 insertions(+), 13 deletions(-) rename tests/release_notes_generator/utils/{test_pull_reuqest_utils.py => test_pull_request_utils.py} (97%) diff --git a/release_notes_generator/utils/decorators.py b/release_notes_generator/utils/decorators.py index 5cd94eea..9f7428cb 100644 --- a/release_notes_generator/utils/decorators.py +++ b/release_notes_generator/utils/decorators.py @@ -23,7 +23,7 @@ from functools import wraps from typing import Callable, Optional, Any from github import GithubException -from requests.exceptions import Timeout, RequestException, ConnectionError as RequestsConnectionError +from requests.exceptions import Timeout, RequestException, ConnectionError from release_notes_generator.utils.github_rate_limiter import GithubRateLimiter logger = logging.getLogger(__name__) @@ -64,7 +64,7 @@ def decorator(method: Callable) -> Callable: def wrapped(*args, **kwargs) -> Optional[Any]: try: return method(*args, **kwargs) - except (RequestsConnectionError, Timeout) as e: + except (ConnectionError, Timeout) as e: logger.error("Network error calling %s: %s", method.__name__, e, exc_info=True) return None except GithubException as e: @@ -74,7 +74,7 @@ def wrapped(*args, **kwargs) -> Optional[Any]: logger.error("HTTP error calling %s: %s", method.__name__, e, exc_info=True) return None except Exception as e: - logger.error("Unexpected error calling %s: %s", method.__name__, e, exc_info=True) + logger.error("%s by calling %s: %s.", type(e).__name__, method.__name__, e, exc_info=True) return None return wrapped diff --git a/tests/release_notes_generator/utils/test_decorators.py b/tests/release_notes_generator/utils/test_decorators.py index 0e6f60c7..65dab39e 100644 --- a/tests/release_notes_generator/utils/test_decorators.py +++ b/tests/release_notes_generator/utils/test_decorators.py @@ -13,9 +13,10 @@ # See the License for the specific language governing permissions and # limitations under the License. # +from github import GithubException from release_notes_generator.utils.decorators import debug_log_decorator, safe_call_decorator - +from requests.exceptions import Timeout, RequestException, ConnectionError # sample function to be decorated def sample_function(x, y): @@ -27,7 +28,7 @@ def sample_function(x, y): def test_debug_log_decorator(mocker): # Mock logging - mock_log_debug = mocker.patch("release_notes_generator.release_notes_generator.utils.decorators.logger.debug") + mock_log_debug = mocker.patch("release_notes_generator.utils.decorators.logger.debug") decorated_function = debug_log_decorator(sample_function) expected_call = [ @@ -49,19 +50,93 @@ def test_safe_call_decorator_success(rate_limiter): def sample_method(x, y): return x + y - result = sample_method(2, 3) - assert 5 == result + actual = sample_method(2, 3) + assert 5 == actual + + +def test_safe_call_decorator_network_error(rate_limiter, mocker): + mock_log_error = mocker.patch("release_notes_generator.utils.decorators.logger.error") + + @safe_call_decorator(rate_limiter) + def sample_method(): + raise ConnectionError("Test connection error") + + actual = sample_method() + + assert actual is None + assert mock_log_error.call_count == 1 + + args, kwargs = mock_log_error.call_args + assert "Network error calling %s: %s" == args[0] + assert "sample_method" == args[1] + assert isinstance(args[2], ConnectionError) + assert "Test connection error" ==str(args[2]) + assert kwargs['exc_info'] + + +def test_safe_call_decorator_github_api_error(rate_limiter, mocker): + mock_log_error = mocker.patch("release_notes_generator.utils.decorators.logger.error") + + @safe_call_decorator(rate_limiter) + def sample_method(): + status_code = 404 + error_data = { + "message": "Not Found", + "documentation_url": "https://developer.github.com/v3" + } + response_headers = { + "X-RateLimit-Limit": "60", + "X-RateLimit-Remaining": "0", + } + raise GithubException(status_code, error_data, response_headers) + + actual = sample_method() + + assert actual is None + assert mock_log_error.call_count == 1 + + args, kwargs = mock_log_error.call_args + assert 'GitHub API error calling %s: %s' == args[0] + assert 'sample_method' == args[1] + assert isinstance(args[2], GithubException) + assert '404 {"message": "Not Found", "documentation_url": "https://developer.github.com/v3"}' == str(args[2]) + assert kwargs['exc_info'] + + +def test_safe_call_decorator_http_error(mocker, rate_limiter): + mock_log_error = mocker.patch("release_notes_generator.utils.decorators.logger.error") + + @safe_call_decorator(rate_limiter) + def sample_method(): + raise RequestException("Test HTTP error") + + actual = sample_method() + + assert actual is None + assert mock_log_error.call_count == 1 + + args, kwargs = mock_log_error.call_args + assert "HTTP error calling %s: %s" == args[0] + assert "sample_method" == args[1] + assert isinstance(args[2], RequestException) + assert "Test HTTP error" == str(args[2]) + assert kwargs['exc_info'] def test_safe_call_decorator_exception(rate_limiter, mocker): - mock_log_error = mocker.patch("release_notes_generator.release_notes_generator.utils.decorators.logger.error") + mock_log_error = mocker.patch("release_notes_generator.utils.decorators.logger.error") @safe_call_decorator(rate_limiter) def sample_method(x, y): return x / y - result = sample_method(2, 0) - assert result is None + actual = sample_method(2, 0) + + assert actual is None mock_log_error.assert_called_once() - assert "Unexpected error calling %s:" in mock_log_error.call_args[0][0] - assert "sample_method" in mock_log_error.call_args[0][1] + exception_message = mock_log_error.call_args[0][0] + assert "%s by calling %s: %s" in exception_message + exception_type = mock_log_error.call_args[0][1] + assert "ZeroDivisionError" in exception_type + method_name = mock_log_error.call_args[0][2] + assert "sample_method" in method_name diff --git a/tests/release_notes_generator/utils/test_pull_reuqest_utils.py b/tests/release_notes_generator/utils/test_pull_request_utils.py similarity index 97% rename from tests/release_notes_generator/utils/test_pull_reuqest_utils.py rename to tests/release_notes_generator/utils/test_pull_request_utils.py index c2352f43..5b39ec92 100644 --- a/tests/release_notes_generator/utils/test_pull_reuqest_utils.py +++ b/tests/release_notes_generator/utils/test_pull_request_utils.py @@ -16,7 +16,7 @@ from github.PullRequest import PullRequest -from release_notes_generator.utils.pull_reuqest_utils import extract_issue_numbers_from_body +from release_notes_generator.utils.pull_request_utils import extract_issue_numbers_from_body # extract_issue_numbers_from_body