blob: f99640f8c9062f957503bb87a35634588514203a [file] [log] [blame]
#!/usr/bin/env python
# coding: utf-8
# Copyright 2015 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.
from __future__ import print_function
import argparse
import os
import pipes
import re
import subprocess
import sys
import tempfile
import textwrap
if sys.version_info[0] < 3:
input = raw_input
IS_WINDOWS = sys.platform.startswith('win')
def SubprocessCheckCall0Or1(args):
"""Like subprocss.check_call(), but allows a return code of 1.
Returns True if the subprocess exits with code 0, False if it exits with
code 1, and re-raises the subprocess.check_call() exception otherwise.
"""
try:
subprocess.check_call(args, shell=IS_WINDOWS)
except subprocess.CalledProcessError as e:
if e.returncode != 1:
raise
return False
return True
def GitMergeBaseIsAncestor(ancestor, descendant):
"""Determines whether |ancestor| is an ancestor of |descendant|.
"""
return SubprocessCheckCall0Or1(
['git', 'merge-base', '--is-ancestor', ancestor, descendant])
def main(args):
parser = argparse.ArgumentParser(
description='Update the in-tree copy of an imported project')
parser.add_argument(
'--repository',
default='https://chromium.googlesource.com/crashpad/crashpad',
help='The imported project\'s remote fetch URL',
metavar='URL')
parser.add_argument(
'--subtree',
default='third_party/crashpad/crashpad',
help='The imported project\'s location in this project\'s tree',
metavar='PATH')
parser.add_argument(
'--update-to',
default='FETCH_HEAD',
help='What to update the imported project to',
metavar='COMMITISH')
parser.add_argument(
'--fetch-ref',
default='HEAD',
help='The remote ref to fetch',
metavar='REF')
parser.add_argument(
'--readme',
help='The README.chromium file describing the imported project',
metavar='FILE',
dest='readme_path')
parser.add_argument(
'--exclude',
default=['codereview.settings'],
action='append',
help='Files to exclude from the imported copy',
metavar='PATH')
parsed = parser.parse_args(args)
original_head = (
subprocess.check_output(['git', 'rev-parse', 'HEAD'],
shell=IS_WINDOWS).rstrip())
# Read the README, because that’s what it’s for. Extract some things from
# it, and save it to be able to update it later.
readme_path = (parsed.readme_path or
os.path.join(os.path.dirname(__file__ or '.'),
'README.chromium'))
readme_content_old = open(readme_path, 'rb').read().decode('utf-8')
project_name_match = re.search(
r'^Name:\s+(.*)$', readme_content_old, re.MULTILINE)
project_name = project_name_match.group(1)
# Extract the original commit hash from the README.
revision_match = re.search(r'^Revision:\s+([0-9a-fA-F]{40})($|\s)',
readme_content_old,
re.MULTILINE)
revision_old = revision_match.group(1)
subprocess.check_call(['git', 'fetch', parsed.repository, parsed.fetch_ref],
shell=IS_WINDOWS)
# Make sure that parsed.update_to is an ancestor of FETCH_HEAD, and
# revision_old is an ancestor of parsed.update_to. This prevents the use of
# hashes that are known to git but that don’t make sense in the context of
# the update operation.
if not GitMergeBaseIsAncestor(parsed.update_to, 'FETCH_HEAD'):
raise Exception('update_to is not an ancestor of FETCH_HEAD',
parsed.update_to,
'FETCH_HEAD')
if not GitMergeBaseIsAncestor(revision_old, parsed.update_to):
raise Exception('revision_old is not an ancestor of update_to',
revision_old,
parsed.update_to)
# git-filter-branch needs a ref to update. It’s not enough to just tell it
# to operate on a range of commits ending at parsed.update_to, because
# parsed.update_to is a commit hash that can’t be updated to point to
# anything else.
subprocess.check_call(['git', 'update-ref', 'UPDATE_TO', parsed.update_to],
shell=IS_WINDOWS)
# Filter the range being updated over to exclude files that ought to be
# missing. This points UPDATE_TO to the rewritten (filtered) version.
# git-filter-branch insists on running from the top level of the working
# tree.
toplevel = subprocess.check_output(['git', 'rev-parse', '--show-toplevel'],
shell=IS_WINDOWS).rstrip()
subprocess.check_call(
['git',
'filter-branch',
'--force',
'--index-filter',
'git rm --cached --ignore-unmatch ' +
' '.join(pipes.quote(path) for path in parsed.exclude),
revision_old + '..UPDATE_TO'],
cwd=toplevel,
shell=IS_WINDOWS)
# git-filter-branch saved a copy of the original UPDATE_TO at
# original/UPDATE_TO, but this isn’t useful because it refers to the same
# thing as parsed.update_to, which is already known.
subprocess.check_call(
['git', 'update-ref', '-d', 'refs/original/UPDATE_TO'],
shell=IS_WINDOWS)
filtered_update_range = revision_old + '..UPDATE_TO'
unfiltered_update_range = revision_old + '..' + parsed.update_to
# This cherry-picks each change in the window from the filtered view of the
# upstream project into the current branch.
assisted_cherry_pick = False
try:
if not SubprocessCheckCall0Or1(['git',
'cherry-pick',
'--keep-redundant-commits',
'--strategy=subtree',
'-Xsubtree=' + parsed.subtree,
'-x',
filtered_update_range]):
assisted_cherry_pick = True
print("""
Please fix the errors above and run "git cherry-pick --continue".
Press Enter when "git cherry-pick" completes.
You may use a new shell for this, or ^Z if job control is available.
Press ^C to abort.
""", file=sys.stderr)
input()
except:
# ^C, signal, or something else.
print('Aborting...', file=sys.stderr)
subprocess.call(['git', 'cherry-pick', '--abort'], shell=IS_WINDOWS)
raise
# Get an abbreviated hash and subject line for each commit in the window,
# sorted in chronological order. Use the unfiltered view so that the commit
# hashes are recognizable.
log_lines = subprocess.check_output(
['git',
'-c',
'core.abbrev=12',
'log',
'--abbrev-commit',
'--pretty=oneline',
'--reverse',
unfiltered_update_range],
shell=IS_WINDOWS).decode('utf-8').splitlines(False)
if assisted_cherry_pick:
# If the user had to help, count the number of cherry-picked commits,
# expecting it to match.
cherry_picked_commits = int(subprocess.check_output(
['git', 'rev-list', '--count', original_head + '..HEAD'],
shell=IS_WINDOWS))
if cherry_picked_commits != len(log_lines):
print('Something smells fishy, aborting anyway...', file=sys.stderr)
subprocess.call(['git', 'cherry-pick', '--abort'], shell=IS_WINDOWS)
raise Exception('not all commits were cherry-picked',
len(log_lines),
cherry_picked_commits)
# Make a nice commit message. Start with the full commit hash.
revision_new = subprocess.check_output(
['git', 'rev-parse', parsed.update_to],
shell=IS_WINDOWS).decode('utf-8').rstrip()
new_message = u'Update ' + project_name + ' to ' + revision_new + '\n\n'
# Wrap everything to 72 characters, with a hanging indent.
wrapper = textwrap.TextWrapper(width=72, subsequent_indent = ' ' * 13)
for line in log_lines:
# Strip trailing periods from subjects.
if line.endswith('.'):
line = line[:-1]
# If any subjects have what look like commit hashes in them, truncate
# them to 12 characters.
line = re.sub(r'(\s)([0-9a-fA-F]{12})([0-9a-fA-F]{28})($|\s)',
r'\1\2\4',
line)
new_message += '\n'.join(wrapper.wrap(line)) + '\n'
# Update the README with the new hash.
readme_content_new = re.sub(
r'^(Revision:\s+)([0-9a-fA-F]{40})($|\s.*?$)',
r'\g<1>' + revision_new,
readme_content_old,
1,
re.MULTILINE)
# If the in-tree copy has no changes relative to the upstream, clear the
# “Local Modifications” section of the README.
has_local_modifications = True
if SubprocessCheckCall0Or1(['git',
'diff-tree',
'--quiet',
'UPDATE_TO',
'HEAD:' + parsed.subtree]):
has_local_modifications = False
if not parsed.exclude:
modifications = 'None.\n'
elif len(parsed.exclude) == 1:
modifications = (
' - %s has been excluded.\n' % parsed.exclude[0])
else:
modifications = (
' - The following files have been excluded:\n')
for excluded in sorted(parsed.exclude):
modifications += ' - ' + excluded + '\n'
readme_content_new = re.sub(r'\nLocal Modifications:\n.*$',
'\nLocal Modifications:\n' + modifications,
readme_content_new,
1,
re.DOTALL)
# The UPDATE_TO ref is no longer useful.
subprocess.check_call(['git', 'update-ref', '-d', 'UPDATE_TO'],
shell=IS_WINDOWS)
# This soft-reset causes all of the cherry-picks to show up as staged, which
# will have the effect of squashing them along with the README update when
# committed below.
subprocess.check_call(['git', 'reset', '--soft', original_head],
shell=IS_WINDOWS)
# Write the new README.
open(readme_path, 'wb').write(readme_content_new.encode('utf-8'))
# Commit everything.
subprocess.check_call(['git', 'add', readme_path], shell=IS_WINDOWS)
try:
commit_message_name = None
with tempfile.NamedTemporaryFile(mode='wb',
delete=False) as commit_message_f:
commit_message_name = commit_message_f.name
commit_message_f.write(new_message.encode('utf-8'))
subprocess.check_call(['git',
'commit', '--file=' + commit_message_name],
shell=IS_WINDOWS)
finally:
if commit_message_name:
os.unlink(commit_message_name)
if has_local_modifications:
print('Remember to check the Local Modifications section in ' +
readme_path, file=sys.stderr)
return 0
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))