Joshua Peraza | 2fc3a43 | 2022-03-09 22:39:36 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
mark | adcc203 | 2015-12-14 15:27:26 | [diff] [blame] | 2 | # coding: utf-8 |
| 3 | |
Avi Drissman | b06ae61 | 2022-10-08 19:24:31 | [diff] [blame] | 4 | # Copyright 2015 The Chromium Authors |
mark | adcc203 | 2015-12-14 15:27:26 | [diff] [blame] | 5 | # Use of this source code is governed by a BSD-style license that can be |
| 6 | # found in the LICENSE file. |
| 7 | |
Mark Mentovai | 032fd5d | 2017-11-29 21:38:23 | [diff] [blame] | 8 | from __future__ import print_function |
| 9 | |
mark | adcc203 | 2015-12-14 15:27:26 | [diff] [blame] | 10 | import argparse |
| 11 | import os |
| 12 | import re |
Prashanth Swaminathan | 8dc44aae | 2023-06-12 21:06:36 | [diff] [blame] | 13 | import shlex |
mark | adcc203 | 2015-12-14 15:27:26 | [diff] [blame] | 14 | import subprocess |
| 15 | import sys |
scottmg | cf00460 | 2016-11-02 22:27:22 | [diff] [blame] | 16 | import tempfile |
mark | adcc203 | 2015-12-14 15:27:26 | [diff] [blame] | 17 | import textwrap |
| 18 | |
Mark Mentovai | 032fd5d | 2017-11-29 21:38:23 | [diff] [blame] | 19 | if sys.version_info[0] < 3: |
| 20 | input = raw_input |
| 21 | |
mark | adcc203 | 2015-12-14 15:27:26 | [diff] [blame] | 22 | |
scottmg | 609f6c2 | 2016-01-06 19:17:02 | [diff] [blame] | 23 | IS_WINDOWS = sys.platform.startswith('win') |
| 24 | |
| 25 | |
mark | adcc203 | 2015-12-14 15:27:26 | [diff] [blame] | 26 | def SubprocessCheckCall0Or1(args): |
| 27 | """Like subprocss.check_call(), but allows a return code of 1. |
| 28 | |
| 29 | Returns True if the subprocess exits with code 0, False if it exits with |
| 30 | code 1, and re-raises the subprocess.check_call() exception otherwise. |
| 31 | """ |
| 32 | try: |
scottmg | 609f6c2 | 2016-01-06 19:17:02 | [diff] [blame] | 33 | subprocess.check_call(args, shell=IS_WINDOWS) |
Mark Mentovai | 032fd5d | 2017-11-29 21:38:23 | [diff] [blame] | 34 | except subprocess.CalledProcessError as e: |
mark | adcc203 | 2015-12-14 15:27:26 | [diff] [blame] | 35 | if e.returncode != 1: |
| 36 | raise |
| 37 | return False |
| 38 | |
| 39 | return True |
| 40 | |
| 41 | |
| 42 | def GitMergeBaseIsAncestor(ancestor, descendant): |
| 43 | """Determines whether |ancestor| is an ancestor of |descendant|. |
| 44 | """ |
| 45 | return SubprocessCheckCall0Or1( |
| 46 | ['git', 'merge-base', '--is-ancestor', ancestor, descendant]) |
| 47 | |
| 48 | |
| 49 | def main(args): |
| 50 | parser = argparse.ArgumentParser( |
| 51 | description='Update the in-tree copy of an imported project') |
| 52 | parser.add_argument( |
| 53 | '--repository', |
| 54 | default='https://chromium.googlesource.com/crashpad/crashpad', |
| 55 | help='The imported project\'s remote fetch URL', |
| 56 | metavar='URL') |
| 57 | parser.add_argument( |
| 58 | '--subtree', |
| 59 | default='third_party/crashpad/crashpad', |
| 60 | help='The imported project\'s location in this project\'s tree', |
| 61 | metavar='PATH') |
| 62 | parser.add_argument( |
| 63 | '--update-to', |
| 64 | default='FETCH_HEAD', |
| 65 | help='What to update the imported project to', |
| 66 | metavar='COMMITISH') |
| 67 | parser.add_argument( |
| 68 | '--fetch-ref', |
| 69 | default='HEAD', |
| 70 | help='The remote ref to fetch', |
| 71 | metavar='REF') |
| 72 | parser.add_argument( |
| 73 | '--readme', |
| 74 | help='The README.chromium file describing the imported project', |
| 75 | metavar='FILE', |
| 76 | dest='readme_path') |
mark | 6af56c56 | 2017-03-03 18:08:09 | [diff] [blame] | 77 | parser.add_argument( |
| 78 | '--exclude', |
Joshua Peraza | 47771d5 | 2021-12-16 21:07:52 | [diff] [blame] | 79 | default=['codereview.settings', 'infra'], |
mark | 6af56c56 | 2017-03-03 18:08:09 | [diff] [blame] | 80 | action='append', |
| 81 | help='Files to exclude from the imported copy', |
| 82 | metavar='PATH') |
mark | adcc203 | 2015-12-14 15:27:26 | [diff] [blame] | 83 | parsed = parser.parse_args(args) |
| 84 | |
| 85 | original_head = ( |
scottmg | 609f6c2 | 2016-01-06 19:17:02 | [diff] [blame] | 86 | subprocess.check_output(['git', 'rev-parse', 'HEAD'], |
| 87 | shell=IS_WINDOWS).rstrip()) |
Bruce Dawson | 1396454 | 2022-02-13 00:43:41 | [diff] [blame] | 88 | original_head = original_head.decode('utf-8') |
mark | adcc203 | 2015-12-14 15:27:26 | [diff] [blame] | 89 | |
| 90 | # Read the README, because that’s what it’s for. Extract some things from |
| 91 | # it, and save it to be able to update it later. |
| 92 | readme_path = (parsed.readme_path or |
| 93 | os.path.join(os.path.dirname(__file__ or '.'), |
| 94 | 'README.chromium')) |
Mark Mentovai | 032fd5d | 2017-11-29 21:38:23 | [diff] [blame] | 95 | readme_content_old = open(readme_path, 'rb').read().decode('utf-8') |
mark | adcc203 | 2015-12-14 15:27:26 | [diff] [blame] | 96 | |
| 97 | project_name_match = re.search( |
| 98 | r'^Name:\s+(.*)$', readme_content_old, re.MULTILINE) |
| 99 | project_name = project_name_match.group(1) |
| 100 | |
| 101 | # Extract the original commit hash from the README. |
| 102 | revision_match = re.search(r'^Revision:\s+([0-9a-fA-F]{40})($|\s)', |
| 103 | readme_content_old, |
| 104 | re.MULTILINE) |
| 105 | revision_old = revision_match.group(1) |
| 106 | |
scottmg | 609f6c2 | 2016-01-06 19:17:02 | [diff] [blame] | 107 | subprocess.check_call(['git', 'fetch', parsed.repository, parsed.fetch_ref], |
| 108 | shell=IS_WINDOWS) |
mark | adcc203 | 2015-12-14 15:27:26 | [diff] [blame] | 109 | |
| 110 | # Make sure that parsed.update_to is an ancestor of FETCH_HEAD, and |
| 111 | # revision_old is an ancestor of parsed.update_to. This prevents the use of |
| 112 | # hashes that are known to git but that don’t make sense in the context of |
| 113 | # the update operation. |
| 114 | if not GitMergeBaseIsAncestor(parsed.update_to, 'FETCH_HEAD'): |
| 115 | raise Exception('update_to is not an ancestor of FETCH_HEAD', |
| 116 | parsed.update_to, |
| 117 | 'FETCH_HEAD') |
| 118 | if not GitMergeBaseIsAncestor(revision_old, parsed.update_to): |
| 119 | raise Exception('revision_old is not an ancestor of update_to', |
| 120 | revision_old, |
| 121 | parsed.update_to) |
| 122 | |
mark | 6af56c56 | 2017-03-03 18:08:09 | [diff] [blame] | 123 | # git-filter-branch needs a ref to update. It’s not enough to just tell it |
| 124 | # to operate on a range of commits ending at parsed.update_to, because |
| 125 | # parsed.update_to is a commit hash that can’t be updated to point to |
| 126 | # anything else. |
| 127 | subprocess.check_call(['git', 'update-ref', 'UPDATE_TO', parsed.update_to], |
| 128 | shell=IS_WINDOWS) |
mark | adcc203 | 2015-12-14 15:27:26 | [diff] [blame] | 129 | |
mark | 6af56c56 | 2017-03-03 18:08:09 | [diff] [blame] | 130 | # Filter the range being updated over to exclude files that ought to be |
| 131 | # missing. This points UPDATE_TO to the rewritten (filtered) version. |
| 132 | # git-filter-branch insists on running from the top level of the working |
| 133 | # tree. |
| 134 | toplevel = subprocess.check_output(['git', 'rev-parse', '--show-toplevel'], |
| 135 | shell=IS_WINDOWS).rstrip() |
| 136 | subprocess.check_call( |
| 137 | ['git', |
| 138 | 'filter-branch', |
| 139 | '--force', |
| 140 | '--index-filter', |
Joshua Peraza | 47771d5 | 2021-12-16 21:07:52 | [diff] [blame] | 141 | 'git rm -r --cached --ignore-unmatch ' + |
Prashanth Swaminathan | 8dc44aae | 2023-06-12 21:06:36 | [diff] [blame] | 142 | ' '.join(shlex.quote(path) for path in parsed.exclude), |
mark | 6af56c56 | 2017-03-03 18:08:09 | [diff] [blame] | 143 | revision_old + '..UPDATE_TO'], |
| 144 | cwd=toplevel, |
| 145 | shell=IS_WINDOWS) |
| 146 | |
| 147 | # git-filter-branch saved a copy of the original UPDATE_TO at |
| 148 | # original/UPDATE_TO, but this isn’t useful because it refers to the same |
| 149 | # thing as parsed.update_to, which is already known. |
| 150 | subprocess.check_call( |
| 151 | ['git', 'update-ref', '-d', 'refs/original/UPDATE_TO'], |
| 152 | shell=IS_WINDOWS) |
| 153 | |
| 154 | filtered_update_range = revision_old + '..UPDATE_TO' |
| 155 | unfiltered_update_range = revision_old + '..' + parsed.update_to |
| 156 | |
| 157 | # This cherry-picks each change in the window from the filtered view of the |
| 158 | # upstream project into the current branch. |
mark | adcc203 | 2015-12-14 15:27:26 | [diff] [blame] | 159 | assisted_cherry_pick = False |
| 160 | try: |
| 161 | if not SubprocessCheckCall0Or1(['git', |
| 162 | 'cherry-pick', |
| 163 | '--keep-redundant-commits', |
| 164 | '--strategy=subtree', |
| 165 | '-Xsubtree=' + parsed.subtree, |
| 166 | '-x', |
mark | 6af56c56 | 2017-03-03 18:08:09 | [diff] [blame] | 167 | filtered_update_range]): |
mark | adcc203 | 2015-12-14 15:27:26 | [diff] [blame] | 168 | assisted_cherry_pick = True |
Mark Mentovai | 032fd5d | 2017-11-29 21:38:23 | [diff] [blame] | 169 | print(""" |
mark | adcc203 | 2015-12-14 15:27:26 | [diff] [blame] | 170 | Please fix the errors above and run "git cherry-pick --continue". |
| 171 | Press Enter when "git cherry-pick" completes. |
| 172 | You may use a new shell for this, or ^Z if job control is available. |
| 173 | Press ^C to abort. |
Mark Mentovai | 032fd5d | 2017-11-29 21:38:23 | [diff] [blame] | 174 | """, file=sys.stderr) |
| 175 | input() |
mark | adcc203 | 2015-12-14 15:27:26 | [diff] [blame] | 176 | except: |
| 177 | # ^C, signal, or something else. |
Mark Mentovai | 032fd5d | 2017-11-29 21:38:23 | [diff] [blame] | 178 | print('Aborting...', file=sys.stderr) |
scottmg | 609f6c2 | 2016-01-06 19:17:02 | [diff] [blame] | 179 | subprocess.call(['git', 'cherry-pick', '--abort'], shell=IS_WINDOWS) |
mark | adcc203 | 2015-12-14 15:27:26 | [diff] [blame] | 180 | raise |
| 181 | |
| 182 | # Get an abbreviated hash and subject line for each commit in the window, |
mark | 6af56c56 | 2017-03-03 18:08:09 | [diff] [blame] | 183 | # sorted in chronological order. Use the unfiltered view so that the commit |
| 184 | # hashes are recognizable. |
Mark Mentovai | 032fd5d | 2017-11-29 21:38:23 | [diff] [blame] | 185 | log_lines = subprocess.check_output( |
| 186 | ['git', |
| 187 | '-c', |
| 188 | 'core.abbrev=12', |
| 189 | 'log', |
| 190 | '--abbrev-commit', |
| 191 | '--pretty=oneline', |
| 192 | '--reverse', |
| 193 | unfiltered_update_range], |
| 194 | shell=IS_WINDOWS).decode('utf-8').splitlines(False) |
mark | adcc203 | 2015-12-14 15:27:26 | [diff] [blame] | 195 | |
| 196 | if assisted_cherry_pick: |
| 197 | # If the user had to help, count the number of cherry-picked commits, |
| 198 | # expecting it to match. |
| 199 | cherry_picked_commits = int(subprocess.check_output( |
mark | 6af56c56 | 2017-03-03 18:08:09 | [diff] [blame] | 200 | ['git', 'rev-list', '--count', original_head + '..HEAD'], |
| 201 | shell=IS_WINDOWS)) |
mark | adcc203 | 2015-12-14 15:27:26 | [diff] [blame] | 202 | if cherry_picked_commits != len(log_lines): |
Mark Mentovai | 032fd5d | 2017-11-29 21:38:23 | [diff] [blame] | 203 | print('Something smells fishy, aborting anyway...', file=sys.stderr) |
scottmg | 609f6c2 | 2016-01-06 19:17:02 | [diff] [blame] | 204 | subprocess.call(['git', 'cherry-pick', '--abort'], shell=IS_WINDOWS) |
mark | adcc203 | 2015-12-14 15:27:26 | [diff] [blame] | 205 | raise Exception('not all commits were cherry-picked', |
| 206 | len(log_lines), |
| 207 | cherry_picked_commits) |
| 208 | |
| 209 | # Make a nice commit message. Start with the full commit hash. |
| 210 | revision_new = subprocess.check_output( |
Mark Mentovai | 032fd5d | 2017-11-29 21:38:23 | [diff] [blame] | 211 | ['git', 'rev-parse', parsed.update_to], |
| 212 | shell=IS_WINDOWS).decode('utf-8').rstrip() |
| 213 | new_message = u'Update ' + project_name + ' to ' + revision_new + '\n\n' |
mark | adcc203 | 2015-12-14 15:27:26 | [diff] [blame] | 214 | |
| 215 | # Wrap everything to 72 characters, with a hanging indent. |
| 216 | wrapper = textwrap.TextWrapper(width=72, subsequent_indent = ' ' * 13) |
| 217 | for line in log_lines: |
| 218 | # Strip trailing periods from subjects. |
| 219 | if line.endswith('.'): |
| 220 | line = line[:-1] |
| 221 | |
| 222 | # If any subjects have what look like commit hashes in them, truncate |
| 223 | # them to 12 characters. |
| 224 | line = re.sub(r'(\s)([0-9a-fA-F]{12})([0-9a-fA-F]{28})($|\s)', |
| 225 | r'\1\2\4', |
| 226 | line) |
| 227 | |
| 228 | new_message += '\n'.join(wrapper.wrap(line)) + '\n' |
| 229 | |
| 230 | # Update the README with the new hash. |
| 231 | readme_content_new = re.sub( |
| 232 | r'^(Revision:\s+)([0-9a-fA-F]{40})($|\s.*?$)', |
| 233 | r'\g<1>' + revision_new, |
| 234 | readme_content_old, |
| 235 | 1, |
| 236 | re.MULTILINE) |
| 237 | |
| 238 | # If the in-tree copy has no changes relative to the upstream, clear the |
| 239 | # “Local Modifications” section of the README. |
| 240 | has_local_modifications = True |
| 241 | if SubprocessCheckCall0Or1(['git', |
| 242 | 'diff-tree', |
| 243 | '--quiet', |
mark | 6af56c56 | 2017-03-03 18:08:09 | [diff] [blame] | 244 | 'UPDATE_TO', |
mark | adcc203 | 2015-12-14 15:27:26 | [diff] [blame] | 245 | 'HEAD:' + parsed.subtree]): |
| 246 | has_local_modifications = False |
| 247 | |
mark | 6af56c56 | 2017-03-03 18:08:09 | [diff] [blame] | 248 | if not parsed.exclude: |
| 249 | modifications = 'None.\n' |
| 250 | elif len(parsed.exclude) == 1: |
| 251 | modifications = ( |
| 252 | ' - %s has been excluded.\n' % parsed.exclude[0]) |
| 253 | else: |
| 254 | modifications = ( |
| 255 | ' - The following files have been excluded:\n') |
| 256 | for excluded in sorted(parsed.exclude): |
| 257 | modifications += ' - ' + excluded + '\n' |
mark | adcc203 | 2015-12-14 15:27:26 | [diff] [blame] | 258 | readme_content_new = re.sub(r'\nLocal Modifications:\n.*$', |
mark | 6af56c56 | 2017-03-03 18:08:09 | [diff] [blame] | 259 | '\nLocal Modifications:\n' + modifications, |
mark | adcc203 | 2015-12-14 15:27:26 | [diff] [blame] | 260 | readme_content_new, |
mark | 6af56c56 | 2017-03-03 18:08:09 | [diff] [blame] | 261 | 1, |
| 262 | re.DOTALL) |
mark | adcc203 | 2015-12-14 15:27:26 | [diff] [blame] | 263 | |
mark | 6af56c56 | 2017-03-03 18:08:09 | [diff] [blame] | 264 | # The UPDATE_TO ref is no longer useful. |
| 265 | subprocess.check_call(['git', 'update-ref', '-d', 'UPDATE_TO'], |
| 266 | shell=IS_WINDOWS) |
| 267 | |
| 268 | # This soft-reset causes all of the cherry-picks to show up as staged, which |
| 269 | # will have the effect of squashing them along with the README update when |
| 270 | # committed below. |
scottmg | 609f6c2 | 2016-01-06 19:17:02 | [diff] [blame] | 271 | subprocess.check_call(['git', 'reset', '--soft', original_head], |
| 272 | shell=IS_WINDOWS) |
mark | adcc203 | 2015-12-14 15:27:26 | [diff] [blame] | 273 | |
| 274 | # Write the new README. |
Mark Mentovai | 032fd5d | 2017-11-29 21:38:23 | [diff] [blame] | 275 | open(readme_path, 'wb').write(readme_content_new.encode('utf-8')) |
mark | adcc203 | 2015-12-14 15:27:26 | [diff] [blame] | 276 | |
| 277 | # Commit everything. |
scottmg | 609f6c2 | 2016-01-06 19:17:02 | [diff] [blame] | 278 | subprocess.check_call(['git', 'add', readme_path], shell=IS_WINDOWS) |
scottmg | cf00460 | 2016-11-02 22:27:22 | [diff] [blame] | 279 | |
| 280 | try: |
| 281 | commit_message_name = None |
Mark Mentovai | 032fd5d | 2017-11-29 21:38:23 | [diff] [blame] | 282 | with tempfile.NamedTemporaryFile(mode='wb', |
| 283 | delete=False) as commit_message_f: |
scottmg | cf00460 | 2016-11-02 22:27:22 | [diff] [blame] | 284 | commit_message_name = commit_message_f.name |
Mark Mentovai | 032fd5d | 2017-11-29 21:38:23 | [diff] [blame] | 285 | commit_message_f.write(new_message.encode('utf-8')) |
scottmg | cf00460 | 2016-11-02 22:27:22 | [diff] [blame] | 286 | subprocess.check_call(['git', |
| 287 | 'commit', '--file=' + commit_message_name], |
| 288 | shell=IS_WINDOWS) |
| 289 | finally: |
| 290 | if commit_message_name: |
| 291 | os.unlink(commit_message_name) |
mark | adcc203 | 2015-12-14 15:27:26 | [diff] [blame] | 292 | |
| 293 | if has_local_modifications: |
Mark Mentovai | 032fd5d | 2017-11-29 21:38:23 | [diff] [blame] | 294 | print('Remember to check the Local Modifications section in ' + |
| 295 | readme_path, file=sys.stderr) |
mark | adcc203 | 2015-12-14 15:27:26 | [diff] [blame] | 296 | |
| 297 | return 0 |
| 298 | |
| 299 | |
| 300 | if __name__ == '__main__': |
| 301 | sys.exit(main(sys.argv[1:])) |