codeowners.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. #!/usr/bin/env python
  2. #
  3. # Utility script for ESP-IDF developers to work with the CODEOWNERS file.
  4. #
  5. # Copyright 2020 Espressif Systems (Shanghai) PTE LTD
  6. #
  7. # Licensed under the Apache License, Version 2.0 (the "License");
  8. # you may not use this file except in compliance with the License.
  9. # You may obtain a copy of the License at
  10. #
  11. # http://www.apache.org/licenses/LICENSE-2.0
  12. #
  13. # Unless required by applicable law or agreed to in writing, software
  14. # distributed under the License is distributed on an "AS IS" BASIS,
  15. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  16. # See the License for the specific language governing permissions and
  17. # limitations under the License.
  18. import argparse
  19. from collections import defaultdict
  20. import os
  21. import subprocess
  22. import sys
  23. CODEOWNERS_PATH = os.path.join(os.path.dirname(__file__), "..", ".gitlab", "CODEOWNERS")
  24. CODEOWNER_GROUP_PREFIX = "@esp-idf-codeowners/"
  25. def action_identify(args):
  26. best_match = []
  27. with open(CODEOWNERS_PATH) as f:
  28. for line in f:
  29. line = line.strip()
  30. if not line or line.startswith("#"):
  31. continue
  32. tokens = line.split()
  33. path_pattern = tokens[0]
  34. owners = tokens[1:]
  35. files = files_by_pattern(path_pattern)
  36. if args.path in files:
  37. best_match = owners
  38. for owner in best_match:
  39. print(owner)
  40. def files_by_pattern(pattern=None):
  41. args = ["git", "ls-files"]
  42. if pattern:
  43. args.append(pattern)
  44. idf_root = os.path.join(os.path.dirname(__file__), "..")
  45. return subprocess.check_output(args, cwd=idf_root).decode("utf-8").split()
  46. def action_ci_check(args):
  47. errors = []
  48. def add_error(msg):
  49. errors.append("Error at CODEOWNERS:{}: {}".format(line_no, msg))
  50. files_by_owner = defaultdict(int)
  51. prev_path_pattern = ""
  52. with open(CODEOWNERS_PATH) as f:
  53. for line_no, line in enumerate(f, start=1):
  54. # Skip empty lines and comments
  55. line = line.strip()
  56. if not line or line.startswith("#"):
  57. continue
  58. # Each line has a form of "<path> <owners>+"
  59. tokens = line.split()
  60. path_pattern = tokens[0]
  61. owners = tokens[1:]
  62. if not owners:
  63. add_error("no owners specified for {}".format(path_pattern))
  64. # Check that the file is sorted by path patterns
  65. path_pattern_for_cmp = path_pattern.replace("-", "_") # ignore difference between _ and - for ordering
  66. if path_pattern_for_cmp < prev_path_pattern:
  67. add_error("file is not sorted: {} < {}".format(path_pattern_for_cmp, prev_path_pattern))
  68. prev_path_pattern = path_pattern_for_cmp
  69. # Check that the pattern matches at least one file
  70. files = files_by_pattern(path_pattern)
  71. if not files:
  72. add_error("no files matched by pattern {}".format(path_pattern))
  73. # Count the number of files per owner
  74. for o in owners:
  75. # Sanity-check the owner group name
  76. if not o.startswith(CODEOWNER_GROUP_PREFIX):
  77. add_error("owner {} doesn't start with {}".format(o, CODEOWNER_GROUP_PREFIX))
  78. files_by_owner[o] += len(files)
  79. owners_sorted = sorted([(owner, cnt) for owner, cnt in files_by_owner.items()], key=lambda p: p[0])
  80. print("File count per owner (not including submodules):")
  81. for owner, cnt in owners_sorted:
  82. print("{}: {} files".format(owner, cnt))
  83. if not errors:
  84. print("No errors found.")
  85. else:
  86. print("Errors found!")
  87. for e in errors:
  88. print(e)
  89. raise SystemExit(1)
  90. def main():
  91. parser = argparse.ArgumentParser(
  92. sys.argv[0], description="Internal helper script for working with the CODEOWNERS file."
  93. )
  94. subparsers = parser.add_subparsers(dest="action")
  95. identify = subparsers.add_parser(
  96. "identify",
  97. help="Lists the owners of the specified path within IDF."
  98. "This command doesn't support files inside submodules, or files not added to git repository.",
  99. )
  100. identify.add_argument("path", help="Path of the file relative to the root of the repository")
  101. subparsers.add_parser(
  102. "ci-check",
  103. help="Check CODEOWNERS file: every line should match at least one file, sanity-check group names, "
  104. "check that the file is sorted by paths",
  105. )
  106. args = parser.parse_args()
  107. if args.action is None:
  108. parser.print_help()
  109. parser.exit(1)
  110. action_func_name = "action_" + args.action.replace("-", "_")
  111. action_func = globals()[action_func_name]
  112. action_func(args)
  113. if __name__ == "__main__":
  114. main()