blob: a1f1bcd0012b70775d0d56aceae2f6cee8c91824 [file] [log] [blame] [edit]
#!/usr/bin/env {{ python }}
import os
import re
import subprocess
import sys
ENCODING_KWARGS = dict()
if sys.version_info >= (3, 6):
ENCODING_KWARGS = dict(encoding='utf-8')
TOPLEVEL = subprocess.check_output(['git', 'rev-parse', '--show-toplevel'], **ENCODING_KWARGS).strip()
LOCATION = os.path.join(TOPLEVEL, r'{{ location }}')
SPACING = 8
IDENT = 4
SCRIPTS = os.path.dirname(os.path.dirname(LOCATION))
CHERRY_PICKING_RE = re.compile(r'^# You are currently cherry-picking commit (?P<hash>[a-f0-9A-F]+)\.$')
CHERRY_PICK_COMMIT_RE = re.compile(r'^(?P<a>\S+)( \((?P<b>\S+)\))?$')
REFNAME_RE = re.compile(r'^refs/remotes/(?P<remote>[^/ ]+)/(?P<branch>\S+)$')
HASH_RE = re.compile(r'^[a-f0-9A-F]+$')
IDENTIFIER_RE = re.compile(r'(\d+\.)?\d+@\S+')
PREFER_RADAR = {{ prefer_radar }}
DEFAULT_BRANCH = '{{ default_branch }}'
SOURCE_REMOTES = {{ source_remotes }}
TRAILERS_TO_STRIP = {{ trailers_to_strip }}
COMMIT_REF_BASE = r'r?R?[a-f0-9A-F]+(\.\d+)?@?([0-9a-zA-z\-\/\.]+[0-9a-zA-z\-\/])?'
COMPOUND_COMMIT_REF = r'(?P<primary>{})(?P<secondary> \({}\))?'.format(COMMIT_REF_BASE, COMMIT_REF_BASE)
CHERRY_PICK_RE = [
re.compile(r'\S* ?[Cc]herry[- ][Pp]ick of {}'.format(COMPOUND_COMMIT_REF)),
re.compile(r'\S* ?[Cc]herry[- ][Pp]ick {}'.format(COMPOUND_COMMIT_REF)),
re.compile(r'\S* ?[Cc]herry[- ][Pp]icked {}'.format(COMPOUND_COMMIT_REF)),
]
UNPACK_SECONDARY_RE = re.compile(r' \(({})\)'.format(COMMIT_REF_BASE))
sys.path.append(SCRIPTS)
from webkitpy.common.checkout.diff_parser import DiffParser
from webkitbugspy import radar
def set_env_variables_from_branch_config():
BRANCH = subprocess.check_output(['git', 'rev-parse', '--abbrev-ref', 'HEAD'], **ENCODING_KWARGS).strip()
for name, data in dict(
title=('COMMIT_MESSAGE_TITLE', 1),
bug=('COMMIT_MESSAGE_BUG', re.compile(r'^(\D+)\d+$')),
cherry_picked=('GIT_WEBKIT_CHERRY_PICKED', 1),
).items():
variable, mode = data
if variable in os.environ:
continue
try:
value = ''
count = 0
for line in reversed(subprocess.check_output(
['git', 'config', '--get-all', 'branch.{}.{}'.format(BRANCH, name)],
stderr=subprocess.PIPE,
**ENCODING_KWARGS
).strip().splitlines()):
if isinstance(mode, int) and count >= mode:
break
elif isinstance(mode, re.Pattern):
match = mode.match(line)
if match and match.group(1) in value:
continue
if line:
value += line + '\n'
count += 1
value = value.strip()
if value:
os.environ[variable] = value
except subprocess.CalledProcessError as error:
# `1` means that key did not exist, which is valid.
if error.returncode != 1:
sys.stderr.write(error.stderr)
def get_bugs_string():
"""Determines what bug URLs to fill or suggest in a WebKit commit message."""
need_the_bug_url = 'Need the bug URL (OOPS!).'
need_the_radar = 'Include a Radar link (OOPS!).'
has_radar = any([bool(regex.match(line))
for line in os.environ.get('COMMIT_MESSAGE_BUG', '').splitlines()
for regex in radar.Tracker.RES])
if os.environ.get('COMMIT_MESSAGE_BUG'):
if has_radar or not bool(radar.Tracker.radarclient()):
return os.environ['COMMIT_MESSAGE_BUG']
else:
return os.environ['COMMIT_MESSAGE_BUG'] + '\n' + need_the_radar
bugs_string = need_the_bug_url
if bool(radar.Tracker.radarclient()):
bugs_string += '\n' + need_the_radar
return bugs_string
def parseChanges(command, commit_message):
dirname = None
try:
for line in subprocess.check_output(
command,
**ENCODING_KWARGS
).splitlines():
if line == '~':
dirname = None
continue
if line.startswith(' ' * SPACING):
if dirname:
line = line.replace('* ', '* {}/'.format(dirname))
commit_message.append(line[SPACING:])
continue
if line.endswith(':'):
dirname = line.split(':')[0]
continue
except subprocess.CalledProcessError:
commit_message.append('')
return
def message(source=None, sha=None):
commit_message = []
command = [{{ perl }}, os.path.join(SCRIPTS, 'prepare-ChangeLog'), '--no-write', '--only-files', '--delimiters', '--git-index']
if sha:
commit_message.append('Amend changes:')
parseChanges(command, commit_message)
commit_message.append('')
commit_message.append('Combined changes:')
command.append('--git-commit')
command.append('HEAD')
parseChanges(command, commit_message)
revert_msg = os.environ.get('COMMIT_MESSAGE_REVERT', '')
if revert_msg:
return '''{title}\n{revert}'''.format(
title=os.environ.get('COMMIT_MESSAGE_TITLE', ''),
revert=revert_msg,
)
else:
bugs_string = get_bugs_string()
return '''{title}
{bugs}
Reviewed by NOBODY (OOPS!).
Explanation of why this fixes the bug (OOPS!).
{content}
'''.format(
title=os.environ.get('COMMIT_MESSAGE_TITLE', '') or 'Need a short description (OOPS!).',
bugs=bugs_string,
content='\n'.join(commit_message) + os.environ.get('COMMIT_MESSAGE_CONTENT', ''),
)
def source_branches():
proc = None
try:
proc = subprocess.Popen(
['git', 'rev-list', 'HEAD'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
**ENCODING_KWARGS
)
outer_line = proc.stdout.readline()
while outer_line:
branches = set()
for inner_line in subprocess.check_output(
['git', 'branch', '--contains', outer_line.strip(), '-a', '--format', '%(refname)'],
**ENCODING_KWARGS
).splitlines():
if not inner_line.startswith('refs/remotes'):
continue
_, _, remote, branch = inner_line.split('/', 3)
if remote in SOURCE_REMOTES and branch != 'HEAD':
branches.add(branch)
if branches:
return sorted(branches)
outer_line = proc.stdout.readline()
finally:
if proc and proc.poll() is None:
proc.kill()
return []
def cherry_pick(content):
cherry_picked = os.environ.get('GIT_WEBKIT_CHERRY_PICKED')
bugs = os.environ.get('COMMIT_MESSAGE_BUG', '').splitlines()
bug = bugs[0] if bugs else None
if PREFER_RADAR and not bug:
RADAR_RES = [
re.compile(r'<?rdar://problem/(?P<id>\d+)>?'),
re.compile(r'<?radar://problem/(?P<id>\d+)>?'),
re.compile(r'<?rdar:\/\/(?P<id>\d+)>?'),
re.compile(r'<?radar:\/\/(?P<id>\d+)>?'),
]
seen_empty = False
for line in content.splitlines():
if not line and seen_empty:
break
elif not line:
seen_empty = True
continue
words = line.split()
for word in [words[0], words[-1]] if words[0] != words[-1] else [words[0]]:
for regex in RADAR_RES:
if regex.match(word):
bug = word
break
if bug:
break
if not cherry_picked or not bug:
LINK_RE = re.compile(r'^\S+:\/\/\S+\d+\S*$')
last_line = ''
for line in content.splitlines():
if not line:
continue
if not line.startswith('#'):
last_line = line
if not bug and LINK_RE.match(line):
bug = line
match = None if cherry_picked else CHERRY_PICKING_RE.match(line)
if match:
cherry_picked = match.group('hash')
if bug and cherry_picked:
break
if last_line and (not cherry_picked or not IDENTIFIER_RE.search(cherry_picked)):
from_last_line = IDENTIFIER_RE.search(last_line)
if from_last_line and not cherry_picked:
cherry_picked = from_last_line.group(0)
elif from_last_line:
cherry_picked = '{} ({})'.format(from_last_line.group(0), cherry_picked)
production_hash = None
match = CHERRY_PICK_COMMIT_RE.match(cherry_picked)
for candidate in ([match.group('a'), match.group('b')] if match else []):
if not candidate:
continue
if HASH_RE.match(candidate):
production_hash = candidate
if production_hash:
try:
subprocess.check_output(
['git', 'merge-base', '--is-ancestor', production_hash, DEFAULT_BRANCH],
**ENCODING_KWARGS
)
except subprocess.CalledProcessError:
remotes = set()
for line in subprocess.check_output(
['git', 'branch', '--contains', production_hash, '-a', '--format', '%(refname)'],
**ENCODING_KWARGS
).splitlines():
match = REFNAME_RE.match(line.strip())
if not match:
continue
remote = match.group('remote')
if remote in SOURCE_REMOTES:
remotes.add(match.group('remote'))
if not remotes:
production_hash = None
is_trunk_bound = DEFAULT_BRANCH in source_branches()
in_suffix = False
result = []
unindent = False
lines = list(content.splitlines())
for header in CHERRY_PICK_RE:
match = header.match(lines[0])
if not match:
continue
cherry_picked = '{}{}'.format(match.group('primary'), match.group('secondary') or '')
unindent = True
lines = lines[1:]
while not lines[0].rstrip():
lines = lines[1:]
break
cherry_pick_metadata = '{}. {}'.format(cherry_picked or '???', bug or '<bug>')
if not is_trunk_bound:
result += ['Cherry-pick {}'.format(cherry_pick_metadata), '']
for line in lines:
line = line.rstrip()
if not line:
result.append('')
continue
if unindent and line.startswith(IDENT*' '):
line = line[IDENT:]
if line[0] == '#' and not in_suffix:
in_suffix = True
if is_trunk_bound and production_hash:
while len(result) and not result[-1] or (result[-1].split(':', 1)[0] in TRAILERS_TO_STRIP):
del result[-1]
result += ['', 'Originally-landed-as: {}'.format(cherry_pick_metadata)]
if line[0] != '#' and not is_trunk_bound and production_hash:
result.append(IDENT*' ' + line)
else:
result.append(line)
return '\n'.join(result)
def main(file_name=None, source=None, sha=None):
with open(file_name, 'r') as commit_message_file:
content = commit_message_file.read()
if source not in (None, 'commit', 'template', 'merge'):
return 0
if os.environ.get('COMMIT_MESSAGE_TITLE', '').startswith('Unreviewed, reverting'):
pass
elif source == 'merge' and content.startswith('Revert'):
return 0
else:
set_env_variables_from_branch_config()
for line in content.splitlines():
if CHERRY_PICKING_RE.match(line):
os.environ['GIT_REFLOG_ACTION'] = 'cherry-pick'
break
if source == 'merge' and os.environ.get('GIT_REFLOG_ACTION') == 'cherry-pick':
with open(file_name, 'w') as commit_message_file:
commit_message_file.write(cherry_pick(content))
return 0
if os.environ.get('GIT_EDITOR', '') == ':':
# When there's no editor being launched, do nothing.
return 0
with open(file_name, 'w') as commit_message_file:
if sha:
commit_message_file.write(subprocess.check_output(
['git', 'log', sha, '-1', '--pretty=format:%B'],
**ENCODING_KWARGS
))
else:
commit_message_file.write(message(source=source, sha=sha))
commit_message_file.write('''
# Please populate the above commit message. Lines starting
# with '#' will be ignored
''')
if sha:
for line in message(source=source, sha=sha).splitlines():
commit_message_file.write('# {}\n'.format(line))
commit_message_file.write('\n')
for line in subprocess.check_output(
['git', 'status'],
**ENCODING_KWARGS
).splitlines():
commit_message_file.write('# {}\n'.format(line))
return 0
if __name__ == '__main__':
sys.exit(main(*sys.argv[1:]))