mirror of
				https://github.com/espressif/esp-idf.git
				synced 2025-11-04 00:51:42 +01:00 
			
		
		
		
	The previous version used an incorrect /dir1/dir2 pattern to match the content of dir2. The correct pattern should be /dir1/dir2/ (with the trailing slash). This commit fixes these patterns. Regarding codeowners.py: 'git ls-files' can not be used to correctly implement the logic of CODEOWNERS file patterns, since it doesn't distinguish between /path/* and /path/. The former pattern in CODEOWNERS file should only match the files inside /path/, while the latter also matches files in nested directories. Because of this, the logic for evaluating patterns is re-implemented, by converting CODEOWNERS patterns into regular expressions. Gitlab CODEOWNERS parsing code was used as a reference, in addition to the approach for converting glob patterns into regular expressions proposed in https://stackoverflow.com/a/29354254.
		
			
				
	
	
		
			209 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			209 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
#!/usr/bin/env python
 | 
						|
#
 | 
						|
# Utility script for ESP-IDF developers to work with the CODEOWNERS file.
 | 
						|
#
 | 
						|
# Copyright 2020 Espressif Systems (Shanghai) PTE LTD
 | 
						|
#
 | 
						|
# 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.
 | 
						|
 | 
						|
import argparse
 | 
						|
import os
 | 
						|
import re
 | 
						|
import subprocess
 | 
						|
import sys
 | 
						|
 | 
						|
 | 
						|
CODEOWNERS_PATH = os.path.join(os.path.dirname(__file__), "..", ".gitlab", "CODEOWNERS")
 | 
						|
CODEOWNER_GROUP_PREFIX = "@esp-idf-codeowners/"
 | 
						|
 | 
						|
 | 
						|
def get_all_files():
 | 
						|
    """
 | 
						|
    Get list of all file paths in the repository.
 | 
						|
    """
 | 
						|
    idf_root = os.path.join(os.path.dirname(__file__), "..")
 | 
						|
    # only split on newlines, since file names may contain spaces
 | 
						|
    return subprocess.check_output(["git", "ls-files"], cwd=idf_root).decode("utf-8").strip().split('\n')
 | 
						|
 | 
						|
 | 
						|
def pattern_to_regex(pattern):
 | 
						|
    """
 | 
						|
    Convert the CODEOWNERS path pattern into a regular expression string.
 | 
						|
    """
 | 
						|
    orig_pattern = pattern  # for printing errors later
 | 
						|
 | 
						|
    # Replicates the logic from normalize_pattern function in Gitlab ee/lib/gitlab/code_owners/file.rb:
 | 
						|
    if not pattern.startswith('/'):
 | 
						|
        pattern = '/**/' + pattern
 | 
						|
    if pattern.endswith('/'):
 | 
						|
        pattern = pattern + '**/*'
 | 
						|
 | 
						|
    # Convert the glob pattern into a regular expression:
 | 
						|
    # first into intermediate tokens
 | 
						|
    pattern = (pattern.replace('**/', ':REGLOB:')
 | 
						|
                      .replace('**', ':INVALID:')
 | 
						|
                      .replace('*', ':GLOB:')
 | 
						|
                      .replace('.', ':DOT:')
 | 
						|
                      .replace('?', ':ANY:'))
 | 
						|
 | 
						|
    if pattern.find(':INVALID:') >= 0:
 | 
						|
        raise ValueError("Likely invalid pattern '{}': '**' should be followed by '/'".format(orig_pattern))
 | 
						|
 | 
						|
    # then into the final regex pattern:
 | 
						|
    re_pattern = (pattern.replace(':REGLOB:', '(?:.*/)?')
 | 
						|
                         .replace(':GLOB:', '[^/]*')
 | 
						|
                         .replace(':DOT:', '[.]')
 | 
						|
                         .replace(':ANY:', '.') + '$')
 | 
						|
    if re_pattern.startswith('/'):
 | 
						|
        re_pattern = '^' + re_pattern
 | 
						|
 | 
						|
    return re_pattern
 | 
						|
 | 
						|
 | 
						|
def files_by_regex(all_files, regex):
 | 
						|
    """
 | 
						|
    Return all files in the repository matching the given regular expresion.
 | 
						|
    """
 | 
						|
    return [file for file in all_files if regex.search('/' + file)]
 | 
						|
 | 
						|
 | 
						|
def files_by_pattern(all_files, pattern=None):
 | 
						|
    """
 | 
						|
    Return all the files in the repository matching the given CODEOWNERS pattern.
 | 
						|
    """
 | 
						|
    if not pattern:
 | 
						|
        return all_files
 | 
						|
 | 
						|
    return files_by_regex(all_files, re.compile(pattern_to_regex(pattern)))
 | 
						|
 | 
						|
 | 
						|
def action_identify(args):
 | 
						|
    best_match = []
 | 
						|
    all_files = get_all_files()
 | 
						|
    with open(CODEOWNERS_PATH) as f:
 | 
						|
        for line in f:
 | 
						|
            line = line.strip()
 | 
						|
            if not line or line.startswith("#"):
 | 
						|
                continue
 | 
						|
            tokens = line.split()
 | 
						|
            path_pattern = tokens[0]
 | 
						|
            owners = tokens[1:]
 | 
						|
            files = files_by_pattern(all_files, path_pattern)
 | 
						|
            if args.path in files:
 | 
						|
                best_match = owners
 | 
						|
    for owner in best_match:
 | 
						|
        print(owner)
 | 
						|
 | 
						|
 | 
						|
def action_test_pattern(args):
 | 
						|
    re_pattern = pattern_to_regex(args.pattern)
 | 
						|
 | 
						|
    if args.regex:
 | 
						|
        print(re_pattern)
 | 
						|
        return
 | 
						|
 | 
						|
    files = files_by_regex(get_all_files(), re.compile(re_pattern))
 | 
						|
    for f in files:
 | 
						|
        print(f)
 | 
						|
 | 
						|
 | 
						|
def action_ci_check(args):
 | 
						|
    errors = []
 | 
						|
 | 
						|
    def add_error(msg):
 | 
						|
        errors.append("Error at CODEOWNERS:{}: {}".format(line_no, msg))
 | 
						|
 | 
						|
    all_files = get_all_files()
 | 
						|
    prev_path_pattern = ""
 | 
						|
    with open(CODEOWNERS_PATH) as f:
 | 
						|
        for line_no, line in enumerate(f, start=1):
 | 
						|
            # Skip empty lines and comments
 | 
						|
            line = line.strip()
 | 
						|
            if line.startswith("# sort-order-reset"):
 | 
						|
                prev_path_pattern = ""
 | 
						|
 | 
						|
            if not line or line.startswith("#"):
 | 
						|
                continue
 | 
						|
 | 
						|
            # Each line has a form of "<path> <owners>+"
 | 
						|
            tokens = line.split()
 | 
						|
            path_pattern = tokens[0]
 | 
						|
            owners = tokens[1:]
 | 
						|
            if not owners:
 | 
						|
                add_error("no owners specified for {}".format(path_pattern))
 | 
						|
 | 
						|
            # Check that the file is sorted by path patterns
 | 
						|
            path_pattern_for_cmp = path_pattern.replace("-", "_")  # ignore difference between _ and - for ordering
 | 
						|
            if prev_path_pattern and path_pattern_for_cmp < prev_path_pattern:
 | 
						|
                add_error("file is not sorted: {} < {}".format(path_pattern_for_cmp, prev_path_pattern))
 | 
						|
            prev_path_pattern = path_pattern_for_cmp
 | 
						|
 | 
						|
            # Check that the pattern matches at least one file
 | 
						|
            files = files_by_pattern(all_files, path_pattern)
 | 
						|
            if not files:
 | 
						|
                add_error("no files matched by pattern {}".format(path_pattern))
 | 
						|
 | 
						|
            for o in owners:
 | 
						|
                # Sanity-check the owner group name
 | 
						|
                if not o.startswith(CODEOWNER_GROUP_PREFIX):
 | 
						|
                    add_error("owner {} doesn't start with {}".format(o, CODEOWNER_GROUP_PREFIX))
 | 
						|
 | 
						|
    if not errors:
 | 
						|
        print("No errors found.")
 | 
						|
    else:
 | 
						|
        print("Errors found!")
 | 
						|
        for e in errors:
 | 
						|
            print(e)
 | 
						|
        raise SystemExit(1)
 | 
						|
 | 
						|
 | 
						|
def main():
 | 
						|
    parser = argparse.ArgumentParser(
 | 
						|
        sys.argv[0], description="Internal helper script for working with the CODEOWNERS file."
 | 
						|
    )
 | 
						|
    subparsers = parser.add_subparsers(dest="action")
 | 
						|
 | 
						|
    identify = subparsers.add_parser(
 | 
						|
        "identify",
 | 
						|
        help="List the owners of the specified path within IDF."
 | 
						|
        "This command doesn't support files inside submodules, or files not added to git repository.",
 | 
						|
    )
 | 
						|
    identify.add_argument("path", help="Path of the file relative to the root of the repository")
 | 
						|
 | 
						|
    subparsers.add_parser(
 | 
						|
        "ci-check",
 | 
						|
        help="Check CODEOWNERS file: every line should match at least one file, sanity-check group names, "
 | 
						|
        "check that the file is sorted by paths",
 | 
						|
    )
 | 
						|
 | 
						|
    test_pattern = subparsers.add_parser(
 | 
						|
        "test-pattern",
 | 
						|
        help="Print files in the repository for a given CODEOWNERS pattern. Useful when adding new rules."
 | 
						|
    )
 | 
						|
    test_pattern.add_argument("--regex", action="store_true", help="Print the equivalent regular expression instead of the file list.")
 | 
						|
    test_pattern.add_argument("pattern", help="Path pattern to get the list of files for")
 | 
						|
 | 
						|
    args = parser.parse_args()
 | 
						|
 | 
						|
    if args.action is None:
 | 
						|
        parser.print_help()
 | 
						|
        parser.exit(1)
 | 
						|
 | 
						|
    action_func_name = "action_" + args.action.replace("-", "_")
 | 
						|
    action_func = globals()[action_func_name]
 | 
						|
    action_func(args)
 | 
						|
 | 
						|
 | 
						|
if __name__ == "__main__":
 | 
						|
    main()
 |