diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml index 7ca7b91..85d428c 100644 --- a/.github/workflows/pr-lint.yml +++ b/.github/workflows/pr-lint.yml @@ -10,9 +10,8 @@ jobs: steps: - id: pr-lint - uses: livingbio/pr-lint@fix-10-compose-action + uses: livingbio/pr-lint@main with: github_token: ${{ secrets.GITHUB_TOKEN }} github_repository: ${{ github.repository }} pr_number: ${{ github.event.number }} - diff --git a/src/pr_lint/formatter.py b/src/pr_lint/formatter.py index f96fe3f..0da9291 100644 --- a/src/pr_lint/formatter.py +++ b/src/pr_lint/formatter.py @@ -1,21 +1,9 @@ -import re from typing import Iterable from github.Label import Label from github.PullRequest import PullRequest -emoji_pattern = re.compile( - "[" - "\U0001F600-\U0001F64F" # Emoticons - "\U0001F300-\U0001F5FF" # Symbols & pictographs - "\U0001F680-\U0001F6FF" # Transport & map symbols - "\U0001F1E0-\U0001F1FF" # Flags (iOS) - "\U0001F900-\U0001F9FF" # Supplemental Symbols and Pictographs - "\U0001FA70-\U0001FAFF" # Symbols and Pictographs Extended-A - "\u2600-\u26FF" # Miscellaneous Symbols - "\u2700-\u27BF" # Dingbats - "]" -) +from .utils import collect_emojis_from_labels, extract_pure_title, get_owner def format(pr: PullRequest) -> None: @@ -30,22 +18,13 @@ def format(pr: PullRequest) -> None: labels: Iterable[Label] = pr.get_labels() # remove all emojis from the left of title - cleaned_title = pr.title.lstrip("".join(emoji_pattern.findall(pr.title))).strip() - - # Sort the labels in the following order - # any label with "Impact:" - # any label with "Type:" - # other labels - labels = sorted(labels, key=lambda label: ("Impact:" in label.name, "Type:" in label.name), reverse=True) - - emojis = [] - for label in labels: - if _emjojis := emoji_pattern.findall(label.name): - for _emjoin in _emjojis: - if _emjoin not in emojis: - emojis.append(_emjoin) - - new_title = f"{''.join(emojis)} {cleaned_title}" + cleaned_title = extract_pure_title(pr.title) + + emojis = collect_emojis_from_labels(label.name for label in labels) + + owner = get_owner(pr) + + new_title = f"{''.join(emojis)} {cleaned_title} @{owner}" if new_title != pr.title: print(f"Updated PR title: {pr.title} -> {new_title}") pr.edit(title=new_title) diff --git a/src/pr_lint/linter.py b/src/pr_lint/linter.py index 081da75..04ce446 100644 --- a/src/pr_lint/linter.py +++ b/src/pr_lint/linter.py @@ -1,8 +1,8 @@ -import re - from github.Label import Label from github.PullRequest import PullRequest +from .utils import get_owner + def lint(pr: PullRequest) -> None: """ @@ -15,9 +15,9 @@ def lint(pr: PullRequest) -> None: Args: pr: The pull request to lint """ + owner = get_owner(pr) + assert owner, "PR title should end with a GitHub username" - pr_owners = re.findall(r"(@[\w-]+)$", pr.title.strip()) - assert pr_owners, "PR title should end with a GitHub username" # FIXME: for some reason the has_in_collaborators method is not working # pr_owner = pr_owners[0][1:] # assert repo.has_in_collaborators(pr_owner), f"{pr_owner} is not a collaborator" diff --git a/src/pr_lint/tests/__init__.py b/src/pr_lint/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/pr_lint/tests/test_utils.py b/src/pr_lint/tests/test_utils.py new file mode 100644 index 0000000..c7c62c2 --- /dev/null +++ b/src/pr_lint/tests/test_utils.py @@ -0,0 +1,29 @@ +from ..utils import collect_emojis_from_labels, extract_owner_from_title, extract_pure_title + + +def test_extract_owner_from_title() -> None: + title = "This is a PR title @owner" + assert extract_owner_from_title(title) == "owner" + + title = "This is a PR title" + assert extract_owner_from_title(title) is None + + +def test_extract_pure_title() -> None: + title = "🚀 This is a PR title @owner" + assert extract_pure_title(title) == "This is a PR title" + + title = "This is a PR title @owner" + assert extract_pure_title(title) == "This is a PR title" + + title = "This is a PR title" + assert extract_pure_title(title) == title + + +def test_collect_emojis_from_labels() -> None: + labels = ["🚀 Type: Feature", "🔥 Impact: Major"] + assert collect_emojis_from_labels(labels) == ["🔥", "🚀"] + + # different order + labels = ["🔥 Impact: Major", "🚀 Type: Feature"] + assert collect_emojis_from_labels(labels) == ["🔥", "🚀"] diff --git a/src/pr_lint/utils.py b/src/pr_lint/utils.py new file mode 100644 index 0000000..2f1b738 --- /dev/null +++ b/src/pr_lint/utils.py @@ -0,0 +1,96 @@ +import re +from typing import Iterable + +from github.PullRequest import PullRequest + +emoji_pattern = ( + "[" + "\U0001F600-\U0001F64F" # Emoticons + "\U0001F300-\U0001F5FF" # Symbols & pictographs + "\U0001F680-\U0001F6FF" # Transport & map symbols + "\U0001F1E0-\U0001F1FF" # Flags (iOS) + "\U0001F900-\U0001F9FF" # Supplemental Symbols and Pictographs + "\U0001FA70-\U0001FAFF" # Symbols and Pictographs Extended-A + "\u2600-\u26FF" # Miscellaneous Symbols + "\u2700-\u27BF" # Dingbats + "]" +) + + +def extract_owner_from_title(title: str) -> str | None: + """ + Extract the owner from the title. + + Args: + title: The title of the pull request + + Returns: + The owner of the pull request + """ + pr_owners = re.findall(r"(@[\w-]+)$", title.strip()) + if pr_owners: + return pr_owners[0][1:] + + return None + + +def get_owner(pr: PullRequest) -> str | None: + """ + Get the owner of a pull request. + + Args: + pr: The pull request + + Returns: + The owner of the pull request + """ + if owner := extract_owner_from_title(pr.title): + return owner + + # if the PR title does not end with a username, check if there is an assignee + if pr.assignee: + return pr.assignee.login + return None + + +def extract_pure_title(title: str) -> str: + """ + Extract the title without emojis and the owner. + + Args: + title: The title to clean + + Returns: + The title without emojis and the owner + """ + match_title = re.findall(rf"^{emoji_pattern}*(.*?)(@[\w-]+)?$", title) + assert match_title, "cannot extract title" + return match_title[0][0].strip() + + +def collect_emojis_from_labels(labels: Iterable[str]) -> list[str]: + """ + Collect emojis from labels. + + Args: + labels: The labels to collect emojis from + + Returns: + The emojis from the labels + """ + + # Sort the labels in the following order + # any label with "Impact:" + # any label with "Type:" + # other labels + + labels = sorted(labels, key=lambda label: ("Impact:" in label, "Type:" in label), reverse=True) + + emojis = [] + for label in labels: + if _emojis := re.findall(emoji_pattern, label): + for _emoji in _emojis: + if _emoji not in emojis: + emojis.append(_emoji) + + return emojis