• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2
3#
4# Copyright 2021, The Android Open Source Project
5#
6# Licensed under the Apache License, Version 2.0 (the "License");
7# you may not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10#     http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS,
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
17#
18
19"""Repohook script to run checks on TODOs in CHRE.
20
21This script runs the following checks on TODOs in a commit:
22
231: Prints a warning if a TODO references the bug ID in the commit message.
24This is mainly intended to minimize TODOs in the CHRE codebase, and to be
25an active reminder to remove a TODO once a commit addresses the debt
26mentioned.
27
282: Fails the repo upload if the current commit adds a TODO, but fails to
29associate it with a bug-ID in the (usual) expected format of
30'TODO(b/13371337).
31
32A bug ID field in the commit message is REQUIRED for this script to work.
33This can be ensured by adding a 'commit_msg_bug_field = true' hook to the
34project's PREUPLOAD.cfg file. It is also recommended to add the
35'ignore_merged_commits' option to avoid unexpected script behavior.
36
37This script will work with any number of commits in the current repo
38checkout.
39"""
40
41import os
42import re
43import subprocess
44import sys
45
46COMMIT_HASH = os.environ['PREUPLOAD_COMMIT']
47
48# According to the repohooks documentation, only the warning and success IDs
49# are mentioned - we use a random non-zero value (that's high enough to
50# avoid confusion with errno values) as our error code.
51REPO_ERROR_RETURN_CODE = 1337
52REPO_WARNING_RETURN_CODE = 77
53REPO_SUCCESS_RETURN_CODE = 0
54
55def check_for_unassociated_todos() -> int:
56  """Check if a TODO has a bug ID associated with it.
57
58  Check if a TODO has a bug ID, in the usual 'TODO(b/13371337): {desc}'
59  format. Also prints the line where said TODO was found.
60
61  Returns:
62    An error code if a TODO has no bugs associated with it.
63  """
64  rc = REPO_SUCCESS_RETURN_CODE
65  commit_contents_cmd = 'git diff ' + COMMIT_HASH + '~ ' + COMMIT_HASH
66  diff_result_lines = subprocess.check_output(commit_contents_cmd,
67                                              shell=True,
68                                              encoding='UTF-8') \
69                                              .split('\n')
70  regex = r'TODO\(b\/([0-9]+)'
71
72  for line in diff_result_lines:
73    if line.startswith('+') and not line.startswith('+++') and \
74        'TODO' in line and not re.findall(regex, line):
75      print('Found a TODO in the following line in the commit without an \
76            associated bug-ID!')
77      print(line)
78      print('Please include a bug ID in the format TODO(b/13371337)')
79      rc = REPO_ERROR_RETURN_CODE
80
81  return rc
82
83def grep_for_todos(bug_id : str) -> int:
84  """Searches for TODOs associated with the BUG ID referenced in the commit.
85
86  Args:
87    bug_id: Bug ID referenced in the commit.
88
89  Returns:
90    A warning code if current bug ID references any TODOs.
91  """
92  grep_result = None
93  rc = REPO_SUCCESS_RETURN_CODE
94  git_repo_path_cmd = 'git rev-parse --show-toplevel'
95  repo_path = ' ' + subprocess.check_output(git_repo_path_cmd, shell=True,
96                                            encoding='UTF-8')
97
98  grep_base_cmd = 'grep -nri '
99  grep_file_filters = '--include \*.h --include \*.cc --include \*.cpp --include \*.c '
100  grep_shell_cmd = grep_base_cmd + grep_file_filters + bug_id + repo_path
101  try:
102    grep_result = subprocess.check_output(grep_shell_cmd, shell=True,
103                                          encoding='UTF-8')
104  except subprocess.CalledProcessError as e:
105    if e.returncode != 1:
106      # A return code of 1 means that grep returned a 'NOT_FOUND', which is
107      # our ideal scenario! A return code of > 1 means something went very
108      # wrong with grep. We still return a success here, since there's
109      # nothing much else we can do (and this tool is intended to be mostly
110      # informational).
111      print('ERROR: grep failed with err code {}'.format(e.returncode),
112            file=sys.stderr)
113      print('The grep command that was run was:\n{}'.format(grep_shell_cmd),
114            file=sys.stderr)
115
116  if grep_result is not None:
117    print('Matching TODOs found for the Bug-ID in the commit message..')
118    print('Hash of the current commit being checked: {}'
119          .format(COMMIT_HASH))
120    grep_result = grep_result.replace(repo_path + '/', '')
121    print(grep_result)
122    rc = REPO_WARNING_RETURN_CODE
123
124  return rc
125
126def get_bug_id_for_current_commit() -> str:
127  """Get the Bug ID for the current commit
128
129  Returns:
130    The bug ID for the current commit.
131  """
132  git_current_commit_msg_cmd = 'git log --format=%B -n 1 '
133  commit_msg_lines_cmd = git_current_commit_msg_cmd + COMMIT_HASH
134  commit_msg_lines_list = subprocess.check_output(commit_msg_lines_cmd,
135                                                  shell=True,
136                                                  encoding='UTF-8') \
137                                                  .split('\n')
138  try:
139    bug_id_line = \
140      [line for line in commit_msg_lines_list if \
141        any(word in line.lower() for word in ['bug:', 'fixes:'])][0]
142  except IndexError:
143    print('Please include a Bug or Fixes field in the commit message')
144    sys.exit(-1);
145  return bug_id_line.split(':')[1].strip()
146
147def is_file_in_diff(filename : str) -> bool:
148  """Check if a given filename is part of the commit.
149
150  Args:
151    filename: filename to check in the git diff.
152
153  Returns:
154    True if the file is part of the commit.
155  """
156  commit_contents_cmd = 'git diff ' + COMMIT_HASH + '~ ' + COMMIT_HASH
157  diff_result = subprocess.check_output(commit_contents_cmd, shell=True,
158                                        encoding='UTF-8')
159  return filename in diff_result
160
161def main():
162  # This script has a bunch of TODOs peppered around, though not with the
163  # same intention as the checks that are being performed. Skip the checks
164  # if we're committing changes to this script! One caveat is that we
165  # should avoid pushing in changes to other code if we're committing
166  # changes to this script.
167  rc = REPO_SUCCESS_RETURN_CODE
168  if not is_file_in_diff(os.path.basename(__file__)):
169    bug_id = get_bug_id_for_current_commit()
170    grep_rc = grep_for_todos(bug_id)
171    check_rc = check_for_unassociated_todos()
172    rc = max(grep_rc, check_rc)
173  sys.exit(rc)
174
175if __name__ == '__main__':
176  main()
177