blob: 4e142f0e138768e1c80ff467390869aae4e59430 [file] [log] [blame]
#!/usr/bin/env python
# Copyright 2014 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import logging
import re
import subprocess
import sys
import traceback
_BUG_PREFIX = 'BUG='
_PERF_PREFIX = 'PERF='
_TEST_PREFIX = 'TEST='
_PREFIXES = [_TEST_PREFIX, _PERF_PREFIX, _BUG_PREFIX]
_CHANGED_FILE_RE = re.compile(r'.*?:\s+(.*)')
_CRBUG_URL_RE = re.compile(r'crbug.com/(\d{6,})')
def _append_prefixes(existing_prefixes, output_lines):
"""Appends missing prefix lines."""
if len(existing_prefixes) != 3:
# Remove trailing line breaks.
while output_lines and output_lines[-1] == '\n':
output_lines.pop()
if not existing_prefixes:
# Add a blank line before TEST/PERF/BUG lines if none present.
output_lines.append('\n')
for prefix in _PREFIXES:
if prefix not in existing_prefixes:
output_lines.append(prefix + '\n')
# Add a space after what we added.
output_lines.append('\n')
def add_mandatory_lines(lines):
"""Adds mandatory lines such as TEST= to the commit message."""
output_lines = []
existing_prefixes = set()
while lines:
line = lines.pop(0)
for prefix in _PREFIXES:
if line.startswith(prefix):
existing_prefixes.add(prefix)
if line.startswith('#') or line.startswith('Change-Id:'):
lines = [line] + lines
break
output_lines.append(line)
_append_prefixes(existing_prefixes, output_lines)
return output_lines + lines
def get_bug_ids_from_diffs(diffs):
"""Gets removed bug IDs from diffs.
When a bug is removed but also added into another line, this
function does not return it.
"""
added_bug_ids = set()
removed_bug_ids = set()
for diff in diffs:
for diff_line in diff.splitlines():
bug_ids = set(_CRBUG_URL_RE.findall(diff_line))
if diff_line.startswith('+'):
added_bug_ids.update(bug_ids)
elif diff_line.startswith('-'):
removed_bug_ids.update(bug_ids)
return removed_bug_ids - added_bug_ids
def update_bug_line(lines, bug_ids):
"""Updates BUG= line.
When no bug_ids is specified, this function does nothing. When
bug_ids will be specified, add them into the existing BUG= line. In
this case, this function removes existing bug description which
claims there is no existing bug (e.g., N/A or None).
"""
if not bug_ids:
return lines
def _reject_empty_bug(bug_id):
return not re.match(r'(n/a|none)', bug_id, flags=re.IGNORECASE)
for index, line in enumerate(lines):
if line.startswith(_BUG_PREFIX):
orig_bug_ids = re.split(r',\s*', line[len(_BUG_PREFIX):].strip())
orig_bug_ids = [bug_id for bug_id in orig_bug_ids if bug_id]
bug_ids.update(set(filter(_reject_empty_bug, orig_bug_ids)))
orig_bug_ids_str = ', '.join(sorted(orig_bug_ids))
bug_ids_str = ', '.join(sorted(bug_ids))
if orig_bug_ids_str != bug_ids_str:
if orig_bug_ids_str == '':
lines[index] = _BUG_PREFIX + '%s\n' % bug_ids_str
else:
lines[index] = _BUG_PREFIX + '%s\n' % orig_bug_ids_str
lines.insert(index + 1, '# Suggestion: %s%s\n' % (
_BUG_PREFIX, bug_ids_str))
return lines
logging.error('No BUG= line')
return lines
def _get_changed_files(base_git_commit):
git_diff_output = subprocess.check_output([
'git', 'diff', '--staged',
'--diff-filter=ADM', # List only {Add,Delete,Modify} changes.
'--name-status', base_git_commit])
# The output format of 'git diff --name-status' is something like:
#
# A path/to/added_file
# D path/to/deleted_file
# M path/to/modified_file
# ...
return [line.split('\t')[1] for line in git_diff_output.splitlines()]
def _detect_and_update_bug_id(lines):
"""Updates BUG= line based on the change."""
optional_args = sys.argv[2:]
# Decide the base git commit from the type of this commit.
if not optional_args:
# A normal commit without --amend.
base_git_commit = 'HEAD'
elif optional_args == ['commit', 'HEAD']:
# An --amend commit.
base_git_commit = 'HEAD~'
else:
# It seems there are some other cases such as merge commit
# but we do not support them as we do not use them often.
return
changed_files = _get_changed_files(base_git_commit)
diffs = []
for changed_file in changed_files:
# MOD for Chromium code may contain non-ARC bugs.
if changed_file.startswith('mods/') and 'chromium' in changed_file:
continue
# Whether or not the commit was -a, hooks run under the index staging all
# changes to be committed. Thus, the desired diff is printed by --staged.
diffs.append(subprocess.check_output(['git', 'diff', '--staged',
base_git_commit, '--', changed_file]))
bug_ids = get_bug_ids_from_diffs(diffs)
lines = update_bug_line(lines, bug_ids)
if __name__ == '__main__':
commit_file = sys.argv[1]
with open(commit_file) as f:
lines = f.readlines()
lines = add_mandatory_lines(lines)
# This function is not trivial and may raise an exception. As bug ID
# detection is an optional feature, the failure in this step must
# not prevent us from creating a commit.
try:
_detect_and_update_bug_id(lines)
except Exception, e:
sys.stderr.write('*** An exception is raised, bug IDs will not be '
'filled automatically ***\n')
traceback.print_exc()
with open(commit_file, 'w') as f:
f.write(''.join(lines))