diff --git a/.github/workflows/pr-code-format.yml b/.github/workflows/pr-code-format.yml new file mode 100644 index 00000000000000..102e1a263b15a8 --- /dev/null +++ b/.github/workflows/pr-code-format.yml @@ -0,0 +1,54 @@ +name: "Check code formatting" +on: pull_request +permissions: + pull-requests: write + +jobs: + code_formatter: + runs-on: ubuntu-latest + steps: + - name: Fetch LLVM sources + uses: actions/checkout@v4 + with: + persist-credentials: false + fetch-depth: 2 + + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v39 + with: + separator: "," + + - name: "Listed files" + run: | + echo "Formatting files:" + echo "${{ steps.changed-files.outputs.all_changed_files }}" + + - name: Install clang-format + uses: aminya/setup-cpp@v1 + with: + clangformat: 16.0.6 + + - name: Setup Python env + uses: actions/setup-python@v4 + with: + python-version: '3.11' + cache: 'pip' + cache-dependency-path: 'llvm/utils/git/requirements_formatting.txt' + + - name: Install python dependencies + run: pip install -r llvm/utils/git/requirements_formatting.txt + + - name: Run code formatter + env: + GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }} + START_REV: ${{ github.event.pull_request.base.sha }} + END_REV: ${{ github.event.pull_request.head.sha }} + CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }} + run: | + python llvm/utils/git/code-format-helper.py \ + --token ${{ secrets.GITHUB_TOKEN }} \ + --issue-number $GITHUB_PR_NUMBER \ + --start-rev $START_REV \ + --end-rev $END_REV \ + --changed-files "$CHANGED_FILES" diff --git a/.github/workflows/pr-python-format.yml b/.github/workflows/pr-python-format.yml deleted file mode 100644 index c6122958826545..00000000000000 --- a/.github/workflows/pr-python-format.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: "Check Python Formatting" -on: - pull_request: - # run on .py - paths: - - '**.py' - -jobs: - python_formatting: - runs-on: ubuntu-latest - steps: - - name: Fetch LLVM sources - uses: actions/checkout@v4 - with: - persist-credentials: false - fetch-depth: 2 - - - name: Get changed files - id: changed-files - uses: tj-actions/changed-files@v39 - with: - files: '**/*.py' - - - name: "Listed files" - run: | - echo "Formatting files:" - echo "${{ steps.changed-files.outputs.all_changed_files }}" - - - name: Setup Python env - uses: actions/setup-python@v4 - with: - python-version: '3.11' - - - name: Python Formatting - uses: akaihola/darker@1.7.2 - with: - options: "--check --diff --color" - version: "~=1.7.2" - src: "${{ steps.changed-files.outputs.all_changed_files }}" diff --git a/llvm/utils/git/code-format-helper.py b/llvm/utils/git/code-format-helper.py new file mode 100644 index 00000000000000..8d3c30b309d015 --- /dev/null +++ b/llvm/utils/git/code-format-helper.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python3 +# +# ====- code-format-helper, runs code formatters from the ci --*- python -*--==# +# +# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +# See https://llvm.org/LICENSE.txt for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# ==-------------------------------------------------------------------------==# + +import argparse +import os +import subprocess +import sys +from functools import cached_property + +import github +from github import IssueComment, PullRequest + + +class FormatHelper: + COMMENT_TAG = "" + name = "unknown" + + @property + def comment_tag(self) -> str: + return self.COMMENT_TAG.replace("fmt", self.name) + + def format_run(self, changed_files: [str], args: argparse.Namespace) -> str | None: + pass + + def pr_comment_text(self, diff: str) -> str: + return f""" +{self.comment_tag} + +:warning: {self.friendly_name}, {self.name} found issues in your code. :warning: + +
+ +You can test this locally with the following command: + + +``````````bash +{self.instructions} +`````````` + +
+ +
+ +View the diff from {self.name} here. + + +``````````diff +{diff} +`````````` + +
+""" + + def find_comment( + self, pr: PullRequest.PullRequest + ) -> IssueComment.IssueComment | None: + for comment in pr.as_issue().get_comments(): + if self.comment_tag in comment.body: + return comment + return None + + def update_pr(self, diff: str, args: argparse.Namespace): + repo = github.Github(args.token).get_repo(args.repo) + pr = repo.get_issue(args.issue_number).as_pull_request() + + existing_comment = self.find_comment(pr) + pr_text = self.pr_comment_text(diff) + + if existing_comment: + existing_comment.edit(pr_text) + else: + pr.as_issue().create_comment(pr_text) + + def update_pr_success(self, args: argparse.Namespace): + repo = github.Github(args.token).get_repo(args.repo) + pr = repo.get_issue(args.issue_number).as_pull_request() + + existing_comment = self.find_comment(pr) + if existing_comment: + existing_comment.edit( + f""" +{self.comment_tag} +:white_check_mark: With the latest revision this PR passed the {self.friendly_name}. +""" + ) + + def run(self, changed_files: [str], args: argparse.Namespace): + diff = self.format_run(changed_files, args) + if diff: + self.update_pr(diff, args) + return False + else: + self.update_pr_success(args) + return True + + +class ClangFormatHelper(FormatHelper): + name = "clang-format" + friendly_name = "C/C++ code formatter" + + @property + def instructions(self): + return " ".join(self.cf_cmd) + + @cached_property + def libcxx_excluded_files(self): + with open("libcxx/utils/data/ignore_format.txt", "r") as ifd: + return [excl.strip() for excl in ifd.readlines()] + + def should_be_excluded(self, path: str) -> bool: + if path in self.libcxx_excluded_files: + print(f"Excluding file {path}") + return True + return False + + def filter_changed_files(self, changed_files: [str]) -> [str]: + filtered_files = [] + for path in changed_files: + _, ext = os.path.splitext(path) + if ext in (".cpp", ".c", ".h", ".hpp", ".hxx", ".cxx"): + if not self.should_be_excluded(path): + filtered_files.append(path) + return filtered_files + + def format_run(self, changed_files: [str], args: argparse.Namespace) -> str | None: + cpp_files = self.filter_changed_files(changed_files) + if not cpp_files: + return + cf_cmd = [ + "git-clang-format", + "--diff", + args.start_rev, + args.end_rev, + "--", + ] + cpp_files + print(f"Running: {' '.join(cf_cmd)}") + self.cf_cmd = cf_cmd + proc = subprocess.run(cf_cmd, capture_output=True) + + # formatting needed + if proc.returncode == 1: + return proc.stdout.decode("utf-8") + + return None + + +class DarkerFormatHelper(FormatHelper): + name = "darker" + friendly_name = "Python code formatter" + + @property + def instructions(self): + return " ".join(self.darker_cmd) + + def filter_changed_files(self, changed_files: [str]) -> [str]: + filtered_files = [] + for path in changed_files: + name, ext = os.path.splitext(path) + if ext == ".py": + filtered_files.append(path) + + return filtered_files + + def format_run(self, changed_files: [str], args: argparse.Namespace) -> str | None: + py_files = self.filter_changed_files(changed_files) + if not py_files: + return + darker_cmd = [ + "darker", + "--check", + "--diff", + "-r", + f"{args.start_rev}..{args.end_rev}", + ] + py_files + print(f"Running: {' '.join(darker_cmd)}") + self.darker_cmd = darker_cmd + proc = subprocess.run(darker_cmd, capture_output=True) + + # formatting needed + if proc.returncode == 1: + return proc.stdout.decode("utf-8") + + return None + + +ALL_FORMATTERS = (DarkerFormatHelper(), ClangFormatHelper()) + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument( + "--token", type=str, required=True, help="GitHub authentiation token" + ) + parser.add_argument( + "--repo", + type=str, + default=os.getenv("GITHUB_REPOSITORY", "llvm/llvm-project"), + help="The GitHub repository that we are working with in the form of / (e.g. llvm/llvm-project)", + ) + parser.add_argument("--issue-number", type=int, required=True) + parser.add_argument( + "--start-rev", + type=str, + required=True, + help="Compute changes from this revision.", + ) + parser.add_argument( + "--end-rev", type=str, required=True, help="Compute changes to this revision" + ) + parser.add_argument( + "--changed-files", + type=str, + help="Comma separated list of files that has been changed", + ) + + args = parser.parse_args() + + changed_files = [] + if args.changed_files: + changed_files = args.changed_files.split(",") + + exit_code = 0 + for fmt in ALL_FORMATTERS: + if not fmt.run(changed_files, args): + exit_code = 1 + + sys.exit(exit_code) diff --git a/llvm/utils/git/requirements_formatting.txt b/llvm/utils/git/requirements_formatting.txt new file mode 100644 index 00000000000000..ff744f0d4225f5 --- /dev/null +++ b/llvm/utils/git/requirements_formatting.txt @@ -0,0 +1,52 @@ +# +# This file is autogenerated by pip-compile with Python 3.11 +# by the following command: +# +# pip-compile --output-file=llvm/utils/git/requirements_formatting.txt llvm/utils/git/requirements_formatting.txt.in +# +black==23.9.1 + # via + # -r llvm/utils/git/requirements_formatting.txt.in + # darker +certifi==2023.7.22 + # via requests +cffi==1.15.1 + # via + # cryptography + # pynacl +charset-normalizer==3.2.0 + # via requests +click==8.1.7 + # via black +cryptography==41.0.3 + # via pyjwt +darker==1.7.2 + # via -r llvm/utils/git/requirements_formatting.txt.in +deprecated==1.2.14 + # via pygithub +idna==3.4 + # via requests +mypy-extensions==1.0.0 + # via black +packaging==23.1 + # via black +pathspec==0.11.2 + # via black +platformdirs==3.10.0 + # via black +pycparser==2.21 + # via cffi +pygithub==1.59.1 + # via -r llvm/utils/git/requirements_formatting.txt.in +pyjwt[crypto]==2.8.0 + # via pygithub +pynacl==1.5.0 + # via pygithub +requests==2.31.0 + # via pygithub +toml==0.10.2 + # via darker +urllib3==2.0.4 + # via requests +wrapt==1.15.0 + # via deprecated diff --git a/llvm/utils/git/requirements_formatting.txt.in b/llvm/utils/git/requirements_formatting.txt.in new file mode 100644 index 00000000000000..4aac571af1cf51 --- /dev/null +++ b/llvm/utils/git/requirements_formatting.txt.in @@ -0,0 +1,3 @@ +black~=23.0 +darker==1.7.2 +PyGithub==1.59.1