Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 1 | #!/usr/bin/env python |
| 2 | # Copyright 2017 The Chromium Authors. All rights reserved. |
| 3 | # Use of this source code is governed by a BSD-style license that can be |
| 4 | # found in the LICENSE file. |
| 5 | |
| 6 | """Splits a branch into smaller branches and uploads CLs.""" |
| 7 | |
Raul Tambre | 80ee78e | 2019-05-06 22:41:05 +0000 | [diff] [blame] | 8 | from __future__ import print_function |
| 9 | |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 10 | import collections |
| 11 | import os |
| 12 | import re |
| 13 | import subprocess2 |
| 14 | import sys |
| 15 | import tempfile |
| 16 | |
Edward Lemur | 1773f37 | 2020-02-22 00:27:14 +0000 | [diff] [blame] | 17 | import gclient_utils |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 18 | import git_footers |
| 19 | import owners |
| 20 | import owners_finder |
Edward Lesmes | 17ffd98 | 2020-03-31 17:33:16 +0000 | [diff] [blame^] | 21 | import scm |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 22 | |
| 23 | import git_common as git |
| 24 | |
| 25 | |
Stephen Martinis | f53f82c | 2018-09-07 20:58:05 +0000 | [diff] [blame] | 26 | # If a call to `git cl split` will generate more than this number of CLs, the |
| 27 | # command will prompt the user to make sure they know what they're doing. Large |
| 28 | # numbers of CLs generated by `git cl split` have caused infrastructure issues |
| 29 | # in the past. |
| 30 | CL_SPLIT_FORCE_LIMIT = 10 |
| 31 | |
| 32 | |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 33 | def ReadFile(file_path): |
| 34 | """Returns the content of |file_path|.""" |
| 35 | with open(file_path) as f: |
| 36 | content = f.read() |
| 37 | return content |
| 38 | |
| 39 | |
| 40 | def EnsureInGitRepository(): |
| 41 | """Throws an exception if the current directory is not a git repository.""" |
| 42 | git.run('rev-parse') |
| 43 | |
| 44 | |
Edward Lemur | ac5c55f | 2020-02-29 00:17:16 +0000 | [diff] [blame] | 45 | def CreateBranchForDirectory(prefix, directory, upstream): |
| 46 | """Creates a branch named |prefix| + "_" + |directory| + "_split". |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 47 | |
| 48 | Return false if the branch already exists. |upstream| is used as upstream for |
| 49 | the created branch. |
| 50 | """ |
| 51 | existing_branches = set(git.branches(use_limit = False)) |
Edward Lemur | ac5c55f | 2020-02-29 00:17:16 +0000 | [diff] [blame] | 52 | branch_name = prefix + '_' + directory + '_split' |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 53 | if branch_name in existing_branches: |
| 54 | return False |
| 55 | git.run('checkout', '-t', upstream, '-b', branch_name) |
| 56 | return True |
| 57 | |
| 58 | |
Edward Lemur | ac5c55f | 2020-02-29 00:17:16 +0000 | [diff] [blame] | 59 | def FormatDescriptionOrComment(txt, directory): |
| 60 | """Replaces $directory with |directory| in |txt|.""" |
| 61 | return txt.replace('$directory', '/' + directory) |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 62 | |
| 63 | |
| 64 | def AddUploadedByGitClSplitToDescription(description): |
| 65 | """Adds a 'This CL was uploaded by git cl split.' line to |description|. |
| 66 | |
| 67 | The line is added before footers, or at the end of |description| if it has no |
| 68 | footers. |
| 69 | """ |
| 70 | split_footers = git_footers.split_footers(description) |
| 71 | lines = split_footers[0] |
| 72 | if not lines[-1] or lines[-1].isspace(): |
| 73 | lines = lines + [''] |
| 74 | lines = lines + ['This CL was uploaded by git cl split.'] |
| 75 | if split_footers[1]: |
| 76 | lines += [''] + split_footers[1] |
| 77 | return '\n'.join(lines) |
| 78 | |
| 79 | |
Edward Lemur | ac5c55f | 2020-02-29 00:17:16 +0000 | [diff] [blame] | 80 | def UploadCl(refactor_branch, refactor_branch_upstream, directory, files, |
| 81 | description, comment, reviewers, changelist, cmd_upload, |
Edward Lemur | 2c62b33 | 2020-03-12 22:12:33 +0000 | [diff] [blame] | 82 | cq_dry_run, enable_auto_submit, repository_root): |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 83 | """Uploads a CL with all changes to |files| in |refactor_branch|. |
| 84 | |
| 85 | Args: |
| 86 | refactor_branch: Name of the branch that contains the changes to upload. |
| 87 | refactor_branch_upstream: Name of the upstream of |refactor_branch|. |
| 88 | directory: Path to the directory that contains the OWNERS file for which |
| 89 | to upload a CL. |
| 90 | files: List of AffectedFile instances to include in the uploaded CL. |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 91 | description: Description of the uploaded CL. |
| 92 | comment: Comment to post on the uploaded CL. |
Edward Lemur | ac5c55f | 2020-02-29 00:17:16 +0000 | [diff] [blame] | 93 | reviewers: A set of reviewers for the CL. |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 94 | changelist: The Changelist class. |
| 95 | cmd_upload: The function associated with the git cl upload command. |
Stephen Martinis | cb32668 | 2018-08-29 21:06:30 +0000 | [diff] [blame] | 96 | cq_dry_run: If CL uploads should also do a cq dry run. |
Takuto Ikuta | 51eca59 | 2019-02-14 19:40:52 +0000 | [diff] [blame] | 97 | enable_auto_submit: If CL uploads should also enable auto submit. |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 98 | """ |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 99 | # Create a branch. |
Edward Lemur | ac5c55f | 2020-02-29 00:17:16 +0000 | [diff] [blame] | 100 | if not CreateBranchForDirectory( |
| 101 | refactor_branch, directory, refactor_branch_upstream): |
| 102 | print('Skipping ' + directory + ' for which a branch already exists.') |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 103 | return |
| 104 | |
| 105 | # Checkout all changes to files in |files|. |
Edward Lemur | 2c62b33 | 2020-03-12 22:12:33 +0000 | [diff] [blame] | 106 | deleted_files = [] |
| 107 | modified_files = [] |
| 108 | for action, f in files: |
| 109 | abspath = os.path.abspath(os.path.join(repository_root, f)) |
| 110 | if action == 'D': |
| 111 | deleted_files.append(abspath) |
| 112 | else: |
| 113 | modified_files.append(abspath) |
| 114 | |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 115 | if deleted_files: |
| 116 | git.run(*['rm'] + deleted_files) |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 117 | if modified_files: |
| 118 | git.run(*['checkout', refactor_branch, '--'] + modified_files) |
| 119 | |
| 120 | # Commit changes. The temporary file is created with delete=False so that it |
| 121 | # can be deleted manually after git has read it rather than automatically |
| 122 | # when it is closed. |
Edward Lemur | 1773f37 | 2020-02-22 00:27:14 +0000 | [diff] [blame] | 123 | with gclient_utils.temporary_file() as tmp_file: |
| 124 | gclient_utils.FileWrite( |
Edward Lemur | ac5c55f | 2020-02-29 00:17:16 +0000 | [diff] [blame] | 125 | tmp_file, FormatDescriptionOrComment(description, directory)) |
Edward Lemur | 1773f37 | 2020-02-22 00:27:14 +0000 | [diff] [blame] | 126 | git.run('commit', '-F', tmp_file) |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 127 | |
| 128 | # Upload a CL. |
Edward Lemur | ac5c55f | 2020-02-29 00:17:16 +0000 | [diff] [blame] | 129 | upload_args = ['-f', '-r', ','.join(reviewers)] |
Stephen Martinis | cb32668 | 2018-08-29 21:06:30 +0000 | [diff] [blame] | 130 | if cq_dry_run: |
| 131 | upload_args.append('--cq-dry-run') |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 132 | if not comment: |
Aaron Gable | e5adf61 | 2017-07-14 10:43:58 -0700 | [diff] [blame] | 133 | upload_args.append('--send-mail') |
Takuto Ikuta | 51eca59 | 2019-02-14 19:40:52 +0000 | [diff] [blame] | 134 | if enable_auto_submit: |
| 135 | upload_args.append('--enable-auto-submit') |
Raul Tambre | 80ee78e | 2019-05-06 22:41:05 +0000 | [diff] [blame] | 136 | print('Uploading CL for ' + directory + '.') |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 137 | cmd_upload(upload_args) |
| 138 | if comment: |
Edward Lemur | ac5c55f | 2020-02-29 00:17:16 +0000 | [diff] [blame] | 139 | changelist().AddComment(FormatDescriptionOrComment(comment, directory), |
| 140 | publish=True) |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 141 | |
| 142 | |
Edward Lemur | ac5c55f | 2020-02-29 00:17:16 +0000 | [diff] [blame] | 143 | def GetFilesSplitByOwners(owners_database, files): |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 144 | """Returns a map of files split by OWNERS file. |
| 145 | |
| 146 | Returns: |
| 147 | A map where keys are paths to directories containing an OWNERS file and |
| 148 | values are lists of files sharing an OWNERS file. |
| 149 | """ |
Edward Lemur | ac5c55f | 2020-02-29 00:17:16 +0000 | [diff] [blame] | 150 | files_split_by_owners = collections.defaultdict(list) |
Edward Lesmes | 17ffd98 | 2020-03-31 17:33:16 +0000 | [diff] [blame^] | 151 | for action, path in files: |
| 152 | enclosing_dir = owners_database.enclosing_dir_with_owners(path) |
| 153 | files_split_by_owners[enclosing_dir].append((action, path)) |
Edward Lemur | ac5c55f | 2020-02-29 00:17:16 +0000 | [diff] [blame] | 154 | return files_split_by_owners |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 155 | |
| 156 | |
Chris Watkins | ba28e46 | 2017-12-13 11:22:17 +1100 | [diff] [blame] | 157 | def PrintClInfo(cl_index, num_cls, directory, file_paths, description, |
Edward Lemur | ac5c55f | 2020-02-29 00:17:16 +0000 | [diff] [blame] | 158 | reviewers): |
Chris Watkins | ba28e46 | 2017-12-13 11:22:17 +1100 | [diff] [blame] | 159 | """Prints info about a CL. |
| 160 | |
| 161 | Args: |
| 162 | cl_index: The index of this CL in the list of CLs to upload. |
| 163 | num_cls: The total number of CLs that will be uploaded. |
| 164 | directory: Path to the directory that contains the OWNERS file for which |
| 165 | to upload a CL. |
| 166 | file_paths: A list of files in this CL. |
| 167 | description: The CL description. |
Edward Lemur | ac5c55f | 2020-02-29 00:17:16 +0000 | [diff] [blame] | 168 | reviewers: A set of reviewers for this CL. |
Chris Watkins | ba28e46 | 2017-12-13 11:22:17 +1100 | [diff] [blame] | 169 | """ |
Edward Lemur | ac5c55f | 2020-02-29 00:17:16 +0000 | [diff] [blame] | 170 | description_lines = FormatDescriptionOrComment(description, |
| 171 | directory).splitlines() |
Chris Watkins | ba28e46 | 2017-12-13 11:22:17 +1100 | [diff] [blame] | 172 | indented_description = '\n'.join([' ' + l for l in description_lines]) |
| 173 | |
Raul Tambre | 80ee78e | 2019-05-06 22:41:05 +0000 | [diff] [blame] | 174 | print('CL {}/{}'.format(cl_index, num_cls)) |
| 175 | print('Path: {}'.format(directory)) |
Edward Lemur | ac5c55f | 2020-02-29 00:17:16 +0000 | [diff] [blame] | 176 | print('Reviewers: {}'.format(', '.join(reviewers))) |
Raul Tambre | 80ee78e | 2019-05-06 22:41:05 +0000 | [diff] [blame] | 177 | print('\n' + indented_description + '\n') |
| 178 | print('\n'.join(file_paths)) |
| 179 | print() |
Chris Watkins | ba28e46 | 2017-12-13 11:22:17 +1100 | [diff] [blame] | 180 | |
| 181 | |
Stephen Martinis | cb32668 | 2018-08-29 21:06:30 +0000 | [diff] [blame] | 182 | def SplitCl(description_file, comment_file, changelist, cmd_upload, dry_run, |
Edward Lemur | 2c62b33 | 2020-03-12 22:12:33 +0000 | [diff] [blame] | 183 | cq_dry_run, enable_auto_submit, repository_root): |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 184 | """"Splits a branch into smaller branches and uploads CLs. |
| 185 | |
| 186 | Args: |
| 187 | description_file: File containing the description of uploaded CLs. |
| 188 | comment_file: File containing the comment of uploaded CLs. |
| 189 | changelist: The Changelist class. |
| 190 | cmd_upload: The function associated with the git cl upload command. |
Chris Watkins | ba28e46 | 2017-12-13 11:22:17 +1100 | [diff] [blame] | 191 | dry_run: Whether this is a dry run (no branches or CLs created). |
Stephen Martinis | cb32668 | 2018-08-29 21:06:30 +0000 | [diff] [blame] | 192 | cq_dry_run: If CL uploads should also do a cq dry run. |
Takuto Ikuta | 51eca59 | 2019-02-14 19:40:52 +0000 | [diff] [blame] | 193 | enable_auto_submit: If CL uploads should also enable auto submit. |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 194 | |
| 195 | Returns: |
| 196 | 0 in case of success. 1 in case of error. |
| 197 | """ |
| 198 | description = AddUploadedByGitClSplitToDescription(ReadFile(description_file)) |
| 199 | comment = ReadFile(comment_file) if comment_file else None |
| 200 | |
| 201 | try: |
Chris Watkins | ba28e46 | 2017-12-13 11:22:17 +1100 | [diff] [blame] | 202 | EnsureInGitRepository() |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 203 | |
| 204 | cl = changelist() |
Edward Lemur | 2c62b33 | 2020-03-12 22:12:33 +0000 | [diff] [blame] | 205 | upstream = cl.GetCommonAncestorWithUpstream() |
| 206 | files = [ |
| 207 | (action.strip(), f) |
| 208 | for action, f in scm.GIT.CaptureStatus(repository_root, upstream) |
| 209 | ] |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 210 | |
| 211 | if not files: |
Raul Tambre | 80ee78e | 2019-05-06 22:41:05 +0000 | [diff] [blame] | 212 | print('Cannot split an empty CL.') |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 213 | return 1 |
| 214 | |
| 215 | author = git.run('config', 'user.email').strip() or None |
| 216 | refactor_branch = git.current_branch() |
Gabriel Charette | 09baacd | 2017-11-09 13:30:41 -0500 | [diff] [blame] | 217 | assert refactor_branch, "Can't run from detached branch." |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 218 | refactor_branch_upstream = git.upstream(refactor_branch) |
Gabriel Charette | 09baacd | 2017-11-09 13:30:41 -0500 | [diff] [blame] | 219 | assert refactor_branch_upstream, \ |
| 220 | "Branch %s must have an upstream." % refactor_branch |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 221 | |
Edward Lemur | 2c62b33 | 2020-03-12 22:12:33 +0000 | [diff] [blame] | 222 | owners_database = owners.Database(repository_root, open, os.path) |
| 223 | owners_database.load_data_needed_for([f for _, f in files]) |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 224 | |
Edward Lemur | ac5c55f | 2020-02-29 00:17:16 +0000 | [diff] [blame] | 225 | files_split_by_owners = GetFilesSplitByOwners(owners_database, files) |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 226 | |
Edward Lemur | ac5c55f | 2020-02-29 00:17:16 +0000 | [diff] [blame] | 227 | num_cls = len(files_split_by_owners) |
| 228 | print('Will split current branch (' + refactor_branch + ') into ' + |
| 229 | str(num_cls) + ' CLs.\n') |
Stephen Martinis | f53f82c | 2018-09-07 20:58:05 +0000 | [diff] [blame] | 230 | if cq_dry_run and num_cls > CL_SPLIT_FORCE_LIMIT: |
Raul Tambre | 80ee78e | 2019-05-06 22:41:05 +0000 | [diff] [blame] | 231 | print( |
Stephen Martinis | cb32668 | 2018-08-29 21:06:30 +0000 | [diff] [blame] | 232 | 'This will generate "%r" CLs. This many CLs can potentially generate' |
| 233 | ' too much load on the build infrastructure. Please email' |
| 234 | ' infra-dev@chromium.org to ensure that this won\'t break anything.' |
| 235 | ' The infra team reserves the right to cancel your jobs if they are' |
Raul Tambre | 80ee78e | 2019-05-06 22:41:05 +0000 | [diff] [blame] | 236 | ' overloading the CQ.' % num_cls) |
Edward Lesmes | ae3586b | 2020-03-23 21:21:14 +0000 | [diff] [blame] | 237 | answer = gclient_utils.AskForData('Proceed? (y/n):') |
Stephen Martinis | cb32668 | 2018-08-29 21:06:30 +0000 | [diff] [blame] | 238 | if answer.lower() != 'y': |
| 239 | return 0 |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 240 | |
Edward Lemur | ac5c55f | 2020-02-29 00:17:16 +0000 | [diff] [blame] | 241 | for cl_index, (directory, files) in \ |
| 242 | enumerate(files_split_by_owners.items(), 1): |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 243 | # Use '/' as a path separator in the branch name and the CL description |
| 244 | # and comment. |
Edward Lemur | ac5c55f | 2020-02-29 00:17:16 +0000 | [diff] [blame] | 245 | directory = directory.replace(os.path.sep, '/') |
Edward Lemur | 2c62b33 | 2020-03-12 22:12:33 +0000 | [diff] [blame] | 246 | file_paths = [f for _, f in files] |
Edward Lemur | ac5c55f | 2020-02-29 00:17:16 +0000 | [diff] [blame] | 247 | reviewers = owners_database.reviewers_for(file_paths, author) |
Chris Watkins | ba28e46 | 2017-12-13 11:22:17 +1100 | [diff] [blame] | 248 | |
| 249 | if dry_run: |
| 250 | PrintClInfo(cl_index, num_cls, directory, file_paths, description, |
Edward Lemur | ac5c55f | 2020-02-29 00:17:16 +0000 | [diff] [blame] | 251 | reviewers) |
Chris Watkins | ba28e46 | 2017-12-13 11:22:17 +1100 | [diff] [blame] | 252 | else: |
Edward Lemur | ac5c55f | 2020-02-29 00:17:16 +0000 | [diff] [blame] | 253 | UploadCl(refactor_branch, refactor_branch_upstream, directory, files, |
| 254 | description, comment, reviewers, changelist, cmd_upload, |
Edward Lemur | 2c62b33 | 2020-03-12 22:12:33 +0000 | [diff] [blame] | 255 | cq_dry_run, enable_auto_submit, repository_root) |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 256 | |
| 257 | # Go back to the original branch. |
| 258 | git.run('checkout', refactor_branch) |
| 259 | |
| 260 | except subprocess2.CalledProcessError as cpe: |
| 261 | sys.stderr.write(cpe.stderr) |
| 262 | return 1 |
| 263 | return 0 |