| OLD | NEW |
| (Empty) | |
| 1 #!/usr/bin/env python |
| 2 # coding: utf-8 |
| 3 |
| 4 # Copyright 2015 The Chromium Authors. All rights reserved. |
| 5 # Use of this source code is governed by a BSD-style license that can be |
| 6 # found in the LICENSE file. |
| 7 |
| 8 import argparse |
| 9 import os |
| 10 import re |
| 11 import subprocess |
| 12 import sys |
| 13 import textwrap |
| 14 |
| 15 |
| 16 def SubprocessCheckCall0Or1(args): |
| 17 """Like subprocss.check_call(), but allows a return code of 1. |
| 18 |
| 19 Returns True if the subprocess exits with code 0, False if it exits with |
| 20 code 1, and re-raises the subprocess.check_call() exception otherwise. |
| 21 """ |
| 22 try: |
| 23 subprocess.check_call(args) |
| 24 except subprocess.CalledProcessError, e: |
| 25 if e.returncode != 1: |
| 26 raise |
| 27 return False |
| 28 |
| 29 return True |
| 30 |
| 31 |
| 32 def GitMergeBaseIsAncestor(ancestor, descendant): |
| 33 """Determines whether |ancestor| is an ancestor of |descendant|. |
| 34 """ |
| 35 return SubprocessCheckCall0Or1( |
| 36 ['git', 'merge-base', '--is-ancestor', ancestor, descendant]) |
| 37 |
| 38 |
| 39 def main(args): |
| 40 parser = argparse.ArgumentParser( |
| 41 description='Update the in-tree copy of an imported project') |
| 42 parser.add_argument( |
| 43 '--repository', |
| 44 default='https://chromium.googlesource.com/crashpad/crashpad', |
| 45 help='The imported project\'s remote fetch URL', |
| 46 metavar='URL') |
| 47 parser.add_argument( |
| 48 '--subtree', |
| 49 default='third_party/crashpad/crashpad', |
| 50 help='The imported project\'s location in this project\'s tree', |
| 51 metavar='PATH') |
| 52 parser.add_argument( |
| 53 '--update-to', |
| 54 default='FETCH_HEAD', |
| 55 help='What to update the imported project to', |
| 56 metavar='COMMITISH') |
| 57 parser.add_argument( |
| 58 '--fetch-ref', |
| 59 default='HEAD', |
| 60 help='The remote ref to fetch', |
| 61 metavar='REF') |
| 62 parser.add_argument( |
| 63 '--readme', |
| 64 help='The README.chromium file describing the imported project', |
| 65 metavar='FILE', |
| 66 dest='readme_path') |
| 67 parsed = parser.parse_args(args) |
| 68 |
| 69 original_head = ( |
| 70 subprocess.check_output(['git', 'rev-parse', 'HEAD']).rstrip()) |
| 71 |
| 72 # Read the README, because that’s what it’s for. Extract some things from |
| 73 # it, and save it to be able to update it later. |
| 74 readme_path = (parsed.readme_path or |
| 75 os.path.join(os.path.dirname(__file__ or '.'), |
| 76 'README.chromium')) |
| 77 readme_content_old = open(readme_path).read() |
| 78 |
| 79 project_name_match = re.search( |
| 80 r'^Name:\s+(.*)$', readme_content_old, re.MULTILINE) |
| 81 project_name = project_name_match.group(1) |
| 82 |
| 83 # Extract the original commit hash from the README. |
| 84 revision_match = re.search(r'^Revision:\s+([0-9a-fA-F]{40})($|\s)', |
| 85 readme_content_old, |
| 86 re.MULTILINE) |
| 87 revision_old = revision_match.group(1) |
| 88 |
| 89 subprocess.check_call(['git', 'fetch', parsed.repository, parsed.fetch_ref]) |
| 90 |
| 91 # Make sure that parsed.update_to is an ancestor of FETCH_HEAD, and |
| 92 # revision_old is an ancestor of parsed.update_to. This prevents the use of |
| 93 # hashes that are known to git but that don’t make sense in the context of |
| 94 # the update operation. |
| 95 if not GitMergeBaseIsAncestor(parsed.update_to, 'FETCH_HEAD'): |
| 96 raise Exception('update_to is not an ancestor of FETCH_HEAD', |
| 97 parsed.update_to, |
| 98 'FETCH_HEAD') |
| 99 if not GitMergeBaseIsAncestor(revision_old, parsed.update_to): |
| 100 raise Exception('revision_old is not an ancestor of update_to', |
| 101 revision_old, |
| 102 parsed.update_to) |
| 103 |
| 104 update_range = revision_old + '..' + parsed.update_to |
| 105 |
| 106 # This cherry-picks each change in the window from the upstream project into |
| 107 # the current branch. |
| 108 assisted_cherry_pick = False |
| 109 try: |
| 110 if not SubprocessCheckCall0Or1(['git', |
| 111 'cherry-pick', |
| 112 '--keep-redundant-commits', |
| 113 '--strategy=subtree', |
| 114 '-Xsubtree=' + parsed.subtree, |
| 115 '-x', |
| 116 update_range]): |
| 117 assisted_cherry_pick = True |
| 118 print >>sys.stderr, (""" |
| 119 Please fix the errors above and run "git cherry-pick --continue". |
| 120 Press Enter when "git cherry-pick" completes. |
| 121 You may use a new shell for this, or ^Z if job control is available. |
| 122 Press ^C to abort. |
| 123 """) |
| 124 raw_input() |
| 125 except: |
| 126 # ^C, signal, or something else. |
| 127 print >>sys.stderr, 'Aborting...' |
| 128 subprocess.call(['git', 'cherry-pick', '--abort']) |
| 129 raise |
| 130 |
| 131 # Get an abbreviated hash and subject line for each commit in the window, |
| 132 # sorted in chronological order. |
| 133 log_lines = subprocess.check_output(['git', |
| 134 '-c', |
| 135 'core.abbrev=12', |
| 136 'log', |
| 137 '--abbrev-commit', |
| 138 '--pretty=oneline', |
| 139 '--reverse', |
| 140 update_range]).splitlines(False) |
| 141 |
| 142 if assisted_cherry_pick: |
| 143 # If the user had to help, count the number of cherry-picked commits, |
| 144 # expecting it to match. |
| 145 cherry_picked_commits = int(subprocess.check_output( |
| 146 ['git', 'rev-list', '--count', original_head + '..HEAD'])) |
| 147 if cherry_picked_commits != len(log_lines): |
| 148 print >>sys.stderr, 'Something smells fishy, aborting anyway...' |
| 149 subprocess.call(['git', 'cherry-pick', '--abort']) |
| 150 raise Exception('not all commits were cherry-picked', |
| 151 len(log_lines), |
| 152 cherry_picked_commits) |
| 153 |
| 154 # Make a nice commit message. Start with the full commit hash. |
| 155 revision_new = subprocess.check_output( |
| 156 ['git', 'rev-parse', parsed.update_to]).rstrip() |
| 157 new_message = 'Update ' + project_name + ' to ' + revision_new + '\n\n' |
| 158 |
| 159 # Wrap everything to 72 characters, with a hanging indent. |
| 160 wrapper = textwrap.TextWrapper(width=72, subsequent_indent = ' ' * 13) |
| 161 for line in log_lines: |
| 162 # Strip trailing periods from subjects. |
| 163 if line.endswith('.'): |
| 164 line = line[:-1] |
| 165 |
| 166 # If any subjects have what look like commit hashes in them, truncate |
| 167 # them to 12 characters. |
| 168 line = re.sub(r'(\s)([0-9a-fA-F]{12})([0-9a-fA-F]{28})($|\s)', |
| 169 r'\1\2\4', |
| 170 line) |
| 171 |
| 172 new_message += '\n'.join(wrapper.wrap(line)) + '\n' |
| 173 |
| 174 # Update the README with the new hash. |
| 175 readme_content_new = re.sub( |
| 176 r'^(Revision:\s+)([0-9a-fA-F]{40})($|\s.*?$)', |
| 177 r'\g<1>' + revision_new, |
| 178 readme_content_old, |
| 179 1, |
| 180 re.MULTILINE) |
| 181 |
| 182 # If the in-tree copy has no changes relative to the upstream, clear the |
| 183 # “Local Modifications” section of the README. |
| 184 has_local_modifications = True |
| 185 if SubprocessCheckCall0Or1(['git', |
| 186 'diff-tree', |
| 187 '--quiet', |
| 188 parsed.update_to, |
| 189 'HEAD:' + parsed.subtree]): |
| 190 has_local_modifications = False |
| 191 |
| 192 readme_content_new = re.sub(r'\nLocal Modifications:\n.*$', |
| 193 '\nLocal Modifications:\nNone.', |
| 194 readme_content_new, |
| 195 1) |
| 196 |
| 197 # This soft-reset causes all of the cherry-picks to show up as staged, |
| 198 # which will have the effect of squashing them along with the README update |
| 199 # when committed below. |
| 200 subprocess.check_call(['git', 'reset', '--soft', original_head]) |
| 201 |
| 202 # Write the new README. |
| 203 open(readme_path, 'w').write(readme_content_new) |
| 204 |
| 205 # Commit everything. |
| 206 subprocess.check_call(['git', 'add', readme_path]) |
| 207 subprocess.check_call(['git', 'commit', '--message=' + new_message]) |
| 208 |
| 209 if has_local_modifications: |
| 210 print >>sys.stderr, ( |
| 211 'Remember to check the Local Modifications section in ' + |
| 212 readme_path) |
| 213 |
| 214 return 0 |
| 215 |
| 216 |
| 217 if __name__ == '__main__': |
| 218 sys.exit(main(sys.argv[1:])) |
| OLD | NEW |