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