123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301 |
- #!/usr/bin/env python3
- # 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', 'infra'],
- 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())
- original_head = original_head.decode('utf-8')
- # 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 -r --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:]))
|