| #!/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:])) |