Josip Sokcevic | 4de5dea | 2022-03-23 21:15:14 +0000 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 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. |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 5 | """Splits a branch into smaller branches and uploads CLs.""" |
| 6 | |
| 7 | import collections |
| 8 | import os |
| 9 | import re |
| 10 | import subprocess2 |
| 11 | import sys |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 12 | |
Edward Lemur | 1773f37 | 2020-02-22 00:27:14 +0000 | [diff] [blame] | 13 | import gclient_utils |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 14 | import git_footers |
Josip Sokcevic | 7958e30 | 2023-03-01 23:02:21 +0000 | [diff] [blame] | 15 | import scm |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 16 | |
| 17 | import git_common as git |
| 18 | |
Stephen Martinis | f53f82c | 2018-09-07 20:58:05 +0000 | [diff] [blame] | 19 | # If a call to `git cl split` will generate more than this number of CLs, the |
| 20 | # command will prompt the user to make sure they know what they're doing. Large |
| 21 | # numbers of CLs generated by `git cl split` have caused infrastructure issues |
| 22 | # in the past. |
| 23 | CL_SPLIT_FORCE_LIMIT = 10 |
| 24 | |
Anne Redulla | b550995 | 2023-07-27 01:27:02 +0000 | [diff] [blame] | 25 | # The maximum number of top reviewers to list. `git cl split` may send many CLs |
| 26 | # to a single reviewer, so the top reviewers with the most CLs sent to them |
| 27 | # will be listed. |
| 28 | CL_SPLIT_TOP_REVIEWERS = 5 |
| 29 | |
Peter Kotwicz | 70d971a | 2023-08-01 22:26:14 +0000 | [diff] [blame] | 30 | FilesAndOwnersDirectory = collections.namedtuple("FilesAndOwnersDirectory", |
| 31 | "files owners_directories") |
| 32 | |
Stephen Martinis | f53f82c | 2018-09-07 20:58:05 +0000 | [diff] [blame] | 33 | |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 34 | def EnsureInGitRepository(): |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 35 | """Throws an exception if the current directory is not a git repository.""" |
| 36 | git.run('rev-parse') |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 37 | |
| 38 | |
Peter Kotwicz | 70d971a | 2023-08-01 22:26:14 +0000 | [diff] [blame] | 39 | def CreateBranchForDirectories(prefix, directories, upstream): |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 40 | """Creates a branch named |prefix| + "_" + |directories[0]| + "_split". |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 41 | |
| 42 | Return false if the branch already exists. |upstream| is used as upstream for |
| 43 | the created branch. |
| 44 | """ |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 45 | existing_branches = set(git.branches(use_limit=False)) |
| 46 | branch_name = prefix + '_' + directories[0] + '_split' |
| 47 | if branch_name in existing_branches: |
| 48 | return False |
| 49 | git.run('checkout', '-t', upstream, '-b', branch_name) |
| 50 | return True |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 51 | |
| 52 | |
Peter Kotwicz | 70d971a | 2023-08-01 22:26:14 +0000 | [diff] [blame] | 53 | def FormatDirectoriesForPrinting(directories, prefix=None): |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 54 | """Formats directory list for printing |
Peter Kotwicz | 70d971a | 2023-08-01 22:26:14 +0000 | [diff] [blame] | 55 | |
| 56 | Uses dedicated format for single-item list.""" |
| 57 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 58 | prefixed = directories |
| 59 | if prefix: |
| 60 | prefixed = [(prefix + d) for d in directories] |
Peter Kotwicz | 70d971a | 2023-08-01 22:26:14 +0000 | [diff] [blame] | 61 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 62 | return str(prefixed) if len(prefixed) > 1 else str(prefixed[0]) |
Peter Kotwicz | 70d971a | 2023-08-01 22:26:14 +0000 | [diff] [blame] | 63 | |
| 64 | |
| 65 | def FormatDescriptionOrComment(txt, directories): |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 66 | """Replaces $directory with |directories| in |txt|.""" |
| 67 | to_insert = FormatDirectoriesForPrinting(directories, prefix='/') |
| 68 | return txt.replace('$directory', to_insert) |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 69 | |
| 70 | |
| 71 | def AddUploadedByGitClSplitToDescription(description): |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 72 | """Adds a 'This CL was uploaded by git cl split.' line to |description|. |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 73 | |
| 74 | The line is added before footers, or at the end of |description| if it has no |
| 75 | footers. |
| 76 | """ |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 77 | split_footers = git_footers.split_footers(description) |
| 78 | lines = split_footers[0] |
| 79 | if lines[-1] and not lines[-1].isspace(): |
| 80 | lines = lines + [''] |
| 81 | lines = lines + ['This CL was uploaded by git cl split.'] |
| 82 | if split_footers[1]: |
| 83 | lines += [''] + split_footers[1] |
| 84 | return '\n'.join(lines) |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 85 | |
| 86 | |
Peter Kotwicz | 70d971a | 2023-08-01 22:26:14 +0000 | [diff] [blame] | 87 | def UploadCl(refactor_branch, refactor_branch_upstream, directories, files, |
Edward Lemur | ac5c55f | 2020-02-29 00:17:16 +0000 | [diff] [blame] | 88 | description, comment, reviewers, changelist, cmd_upload, |
Rachael Newitt | 03e4912 | 2023-06-28 21:39:21 +0000 | [diff] [blame] | 89 | cq_dry_run, enable_auto_submit, topic, repository_root): |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 90 | """Uploads a CL with all changes to |files| in |refactor_branch|. |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 91 | |
| 92 | Args: |
| 93 | refactor_branch: Name of the branch that contains the changes to upload. |
| 94 | refactor_branch_upstream: Name of the upstream of |refactor_branch|. |
Peter Kotwicz | 70d971a | 2023-08-01 22:26:14 +0000 | [diff] [blame] | 95 | directories: Paths to the directories that contain the OWNERS files for |
| 96 | which to upload a CL. |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 97 | files: List of AffectedFile instances to include in the uploaded CL. |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 98 | description: Description of the uploaded CL. |
| 99 | comment: Comment to post on the uploaded CL. |
Edward Lemur | ac5c55f | 2020-02-29 00:17:16 +0000 | [diff] [blame] | 100 | reviewers: A set of reviewers for the CL. |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 101 | changelist: The Changelist class. |
| 102 | cmd_upload: The function associated with the git cl upload command. |
Stephen Martinis | cb32668 | 2018-08-29 21:06:30 +0000 | [diff] [blame] | 103 | 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] | 104 | enable_auto_submit: If CL uploads should also enable auto submit. |
Rachael Newitt | 03e4912 | 2023-06-28 21:39:21 +0000 | [diff] [blame] | 105 | topic: Topic to associate with uploaded CLs. |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 106 | """ |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 107 | # Create a branch. |
| 108 | if not CreateBranchForDirectories(refactor_branch, directories, |
| 109 | refactor_branch_upstream): |
| 110 | print('Skipping ' + FormatDirectoriesForPrinting(directories) + |
| 111 | ' for which a branch already exists.') |
| 112 | return |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 113 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 114 | # Checkout all changes to files in |files|. |
| 115 | deleted_files = [] |
| 116 | modified_files = [] |
| 117 | for action, f in files: |
| 118 | abspath = os.path.abspath(os.path.join(repository_root, f)) |
| 119 | if action == 'D': |
| 120 | deleted_files.append(abspath) |
| 121 | else: |
| 122 | modified_files.append(abspath) |
Edward Lemur | 2c62b33 | 2020-03-12 22:12:33 +0000 | [diff] [blame] | 123 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 124 | if deleted_files: |
| 125 | git.run(*['rm'] + deleted_files) |
| 126 | if modified_files: |
| 127 | git.run(*['checkout', refactor_branch, '--'] + modified_files) |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 128 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 129 | # Commit changes. The temporary file is created with delete=False so that it |
| 130 | # can be deleted manually after git has read it rather than automatically |
| 131 | # when it is closed. |
| 132 | with gclient_utils.temporary_file() as tmp_file: |
| 133 | gclient_utils.FileWrite( |
| 134 | tmp_file, FormatDescriptionOrComment(description, directories)) |
| 135 | git.run('commit', '-F', tmp_file) |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 136 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 137 | # Upload a CL. |
| 138 | upload_args = ['-f'] |
| 139 | if reviewers: |
| 140 | upload_args.extend(['-r', ','.join(sorted(reviewers))]) |
| 141 | if cq_dry_run: |
| 142 | upload_args.append('--cq-dry-run') |
| 143 | if not comment: |
| 144 | upload_args.append('--send-mail') |
| 145 | if enable_auto_submit: |
| 146 | upload_args.append('--enable-auto-submit') |
| 147 | if topic: |
| 148 | upload_args.append('--topic={}'.format(topic)) |
| 149 | print('Uploading CL for ' + FormatDirectoriesForPrinting(directories) + |
| 150 | '...') |
Olivier Li | 0614591 | 2021-05-12 23:59:24 +0000 | [diff] [blame] | 151 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 152 | ret = cmd_upload(upload_args) |
| 153 | if ret != 0: |
| 154 | print('Uploading failed.') |
| 155 | print('Note: git cl split has built-in resume capabilities.') |
| 156 | print('Delete ' + git.current_branch() + |
| 157 | ' then run git cl split again to resume uploading.') |
Olivier Li | 0614591 | 2021-05-12 23:59:24 +0000 | [diff] [blame] | 158 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 159 | if comment: |
| 160 | changelist().AddComment(FormatDescriptionOrComment( |
| 161 | comment, directories), |
| 162 | publish=True) |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 163 | |
| 164 | |
Daniel Cheng | 403c44e | 2022-10-05 22:24:58 +0000 | [diff] [blame] | 165 | def GetFilesSplitByOwners(files, max_depth): |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 166 | """Returns a map of files split by OWNERS file. |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 167 | |
| 168 | Returns: |
| 169 | A map where keys are paths to directories containing an OWNERS file and |
| 170 | values are lists of files sharing an OWNERS file. |
| 171 | """ |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 172 | files_split_by_owners = {} |
| 173 | for action, path in files: |
| 174 | # normpath() is important to normalize separators here, in prepration |
| 175 | # for str.split() before. It would be nicer to use something like |
| 176 | # pathlib here but alas... |
| 177 | dir_with_owners = os.path.normpath(os.path.dirname(path)) |
| 178 | if max_depth >= 1: |
| 179 | dir_with_owners = os.path.join( |
| 180 | *dir_with_owners.split(os.path.sep)[:max_depth]) |
| 181 | # Find the closest parent directory with an OWNERS file. |
| 182 | while (dir_with_owners not in files_split_by_owners |
| 183 | and not os.path.isfile(os.path.join(dir_with_owners, 'OWNERS'))): |
| 184 | dir_with_owners = os.path.dirname(dir_with_owners) |
| 185 | files_split_by_owners.setdefault(dir_with_owners, []).append( |
| 186 | (action, path)) |
| 187 | return files_split_by_owners |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 188 | |
| 189 | |
Peter Kotwicz | 70d971a | 2023-08-01 22:26:14 +0000 | [diff] [blame] | 190 | def PrintClInfo(cl_index, num_cls, directories, file_paths, description, |
Anne Redulla | 072d06e | 2023-07-06 23:12:16 +0000 | [diff] [blame] | 191 | reviewers, enable_auto_submit, topic): |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 192 | """Prints info about a CL. |
Chris Watkins | ba28e46 | 2017-12-13 11:22:17 +1100 | [diff] [blame] | 193 | |
| 194 | Args: |
| 195 | cl_index: The index of this CL in the list of CLs to upload. |
| 196 | num_cls: The total number of CLs that will be uploaded. |
Peter Kotwicz | 70d971a | 2023-08-01 22:26:14 +0000 | [diff] [blame] | 197 | directories: Paths to directories that contains the OWNERS files for which |
Chris Watkins | ba28e46 | 2017-12-13 11:22:17 +1100 | [diff] [blame] | 198 | to upload a CL. |
| 199 | file_paths: A list of files in this CL. |
| 200 | description: The CL description. |
Edward Lemur | ac5c55f | 2020-02-29 00:17:16 +0000 | [diff] [blame] | 201 | reviewers: A set of reviewers for this CL. |
Anne Redulla | 072d06e | 2023-07-06 23:12:16 +0000 | [diff] [blame] | 202 | enable_auto_submit: If the CL should also have auto submit enabled. |
Rachael Newitt | 03e4912 | 2023-06-28 21:39:21 +0000 | [diff] [blame] | 203 | topic: Topic to set for this CL. |
Chris Watkins | ba28e46 | 2017-12-13 11:22:17 +1100 | [diff] [blame] | 204 | """ |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 205 | description_lines = FormatDescriptionOrComment(description, |
| 206 | directories).splitlines() |
| 207 | indented_description = '\n'.join([' ' + l for l in description_lines]) |
Chris Watkins | ba28e46 | 2017-12-13 11:22:17 +1100 | [diff] [blame] | 208 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 209 | print('CL {}/{}'.format(cl_index, num_cls)) |
| 210 | print('Paths: {}'.format(FormatDirectoriesForPrinting(directories))) |
| 211 | print('Reviewers: {}'.format(', '.join(reviewers))) |
| 212 | print('Auto-Submit: {}'.format(enable_auto_submit)) |
| 213 | print('Topic: {}'.format(topic)) |
| 214 | print('\n' + indented_description + '\n') |
| 215 | print('\n'.join(file_paths)) |
| 216 | print() |
Chris Watkins | ba28e46 | 2017-12-13 11:22:17 +1100 | [diff] [blame] | 217 | |
| 218 | |
Stephen Martinis | cb32668 | 2018-08-29 21:06:30 +0000 | [diff] [blame] | 219 | def SplitCl(description_file, comment_file, changelist, cmd_upload, dry_run, |
Rachael Newitt | 03e4912 | 2023-06-28 21:39:21 +0000 | [diff] [blame] | 220 | cq_dry_run, enable_auto_submit, max_depth, topic, repository_root): |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 221 | """"Splits a branch into smaller branches and uploads CLs. |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 222 | |
| 223 | Args: |
| 224 | description_file: File containing the description of uploaded CLs. |
| 225 | comment_file: File containing the comment of uploaded CLs. |
| 226 | changelist: The Changelist class. |
| 227 | cmd_upload: The function associated with the git cl upload command. |
Chris Watkins | ba28e46 | 2017-12-13 11:22:17 +1100 | [diff] [blame] | 228 | 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] | 229 | 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] | 230 | enable_auto_submit: If CL uploads should also enable auto submit. |
Daniel Cheng | 403c44e | 2022-10-05 22:24:58 +0000 | [diff] [blame] | 231 | max_depth: The maximum directory depth to search for OWNERS files. A value |
| 232 | less than 1 means no limit. |
Rachael Newitt | 03e4912 | 2023-06-28 21:39:21 +0000 | [diff] [blame] | 233 | topic: Topic to associate with split CLs. |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 234 | |
| 235 | Returns: |
| 236 | 0 in case of success. 1 in case of error. |
| 237 | """ |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 238 | description = AddUploadedByGitClSplitToDescription( |
| 239 | gclient_utils.FileRead(description_file)) |
| 240 | comment = gclient_utils.FileRead(comment_file) if comment_file else None |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 241 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 242 | try: |
| 243 | EnsureInGitRepository() |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 244 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 245 | cl = changelist() |
| 246 | upstream = cl.GetCommonAncestorWithUpstream() |
| 247 | files = [ |
| 248 | (action.strip(), f) |
| 249 | for action, f in scm.GIT.CaptureStatus(repository_root, upstream) |
| 250 | ] |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 251 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 252 | if not files: |
| 253 | print('Cannot split an empty CL.') |
| 254 | return 1 |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 255 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 256 | author = git.run('config', 'user.email').strip() or None |
| 257 | refactor_branch = git.current_branch() |
| 258 | assert refactor_branch, "Can't run from detached branch." |
| 259 | refactor_branch_upstream = git.upstream(refactor_branch) |
| 260 | assert refactor_branch_upstream, \ |
| 261 | "Branch %s must have an upstream." % refactor_branch |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 262 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 263 | if not CheckDescriptionBugLink(description): |
| 264 | return 0 |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 265 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 266 | files_split_by_reviewers = SelectReviewersForFiles( |
| 267 | cl, author, files, max_depth) |
Peter Kotwicz | 70d971a | 2023-08-01 22:26:14 +0000 | [diff] [blame] | 268 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 269 | num_cls = len(files_split_by_reviewers) |
| 270 | print('Will split current branch (' + refactor_branch + ') into ' + |
| 271 | str(num_cls) + ' CLs.\n') |
| 272 | if cq_dry_run and num_cls > CL_SPLIT_FORCE_LIMIT: |
| 273 | print( |
| 274 | 'This will generate "%r" CLs. This many CLs can potentially' |
| 275 | ' generate too much load on the build infrastructure. Please' |
| 276 | ' email infra-dev@chromium.org to ensure that this won\'t break' |
| 277 | ' anything. The infra team reserves the right to cancel your' |
| 278 | ' jobs if they are overloading the CQ.' % num_cls) |
| 279 | answer = gclient_utils.AskForData('Proceed? (y/n):') |
| 280 | if answer.lower() != 'y': |
| 281 | return 0 |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 282 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 283 | cls_per_reviewer = collections.defaultdict(int) |
| 284 | for cl_index, (reviewers, cl_info) in \ |
| 285 | enumerate(files_split_by_reviewers.items(), 1): |
| 286 | # Convert reviewers from tuple to set. |
| 287 | reviewer_set = set(reviewers) |
| 288 | if dry_run: |
| 289 | file_paths = [f for _, f in cl_info.files] |
| 290 | PrintClInfo(cl_index, num_cls, cl_info.owners_directories, |
| 291 | file_paths, description, reviewer_set, |
| 292 | enable_auto_submit, topic) |
| 293 | else: |
| 294 | UploadCl(refactor_branch, refactor_branch_upstream, |
| 295 | cl_info.owners_directories, cl_info.files, description, |
| 296 | comment, reviewer_set, changelist, cmd_upload, |
| 297 | cq_dry_run, enable_auto_submit, topic, repository_root) |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 298 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 299 | for reviewer in reviewers: |
| 300 | cls_per_reviewer[reviewer] += 1 |
Anne Redulla | b550995 | 2023-07-27 01:27:02 +0000 | [diff] [blame] | 301 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 302 | # List the top reviewers that will be sent the most CLs as a result of |
| 303 | # the split. |
| 304 | reviewer_rankings = sorted(cls_per_reviewer.items(), |
| 305 | key=lambda item: item[1], |
| 306 | reverse=True) |
| 307 | print('The top reviewers are:') |
| 308 | for reviewer, count in reviewer_rankings[:CL_SPLIT_TOP_REVIEWERS]: |
| 309 | print(f' {reviewer}: {count} CLs') |
Anne Redulla | b550995 | 2023-07-27 01:27:02 +0000 | [diff] [blame] | 310 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 311 | # Go back to the original branch. |
| 312 | git.run('checkout', refactor_branch) |
Francois Doray | d42c681 | 2017-05-30 15:10:20 -0400 | [diff] [blame] | 313 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 314 | except subprocess2.CalledProcessError as cpe: |
| 315 | sys.stderr.write(cpe.stderr) |
| 316 | return 1 |
| 317 | return 0 |
Peter Kotwicz | 70d971a | 2023-08-01 22:26:14 +0000 | [diff] [blame] | 318 | |
| 319 | |
Peter Kotwicz | caeef7b | 2023-08-24 02:34:52 +0000 | [diff] [blame] | 320 | def CheckDescriptionBugLink(description): |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 321 | """Verifies that the description contains a bug link. |
Peter Kotwicz | caeef7b | 2023-08-24 02:34:52 +0000 | [diff] [blame] | 322 | |
| 323 | Examples: |
| 324 | Bug: 123 |
| 325 | Bug: chromium:456 |
| 326 | |
| 327 | Prompts user if the description does not contain a bug link. |
| 328 | """ |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 329 | bug_pattern = re.compile(r"^Bug:\s*(?:[a-zA-Z]+:)?[0-9]+", re.MULTILINE) |
| 330 | matches = re.findall(bug_pattern, description) |
| 331 | answer = 'y' |
| 332 | if not matches: |
| 333 | answer = gclient_utils.AskForData( |
| 334 | 'Description does not include a bug link. Proceed? (y/n):') |
| 335 | return answer.lower() == 'y' |
Peter Kotwicz | caeef7b | 2023-08-24 02:34:52 +0000 | [diff] [blame] | 336 | |
| 337 | |
Peter Kotwicz | 70d971a | 2023-08-01 22:26:14 +0000 | [diff] [blame] | 338 | def SelectReviewersForFiles(cl, author, files, max_depth): |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 339 | """Selects reviewers for passed-in files |
Peter Kotwicz | 70d971a | 2023-08-01 22:26:14 +0000 | [diff] [blame] | 340 | |
| 341 | Args: |
| 342 | cl: Changelist class instance |
| 343 | author: Email of person running 'git cl split' |
| 344 | files: List of files |
| 345 | max_depth: The maximum directory depth to search for OWNERS files. A value |
| 346 | less than 1 means no limit. |
| 347 | """ |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 348 | info_split_by_owners = GetFilesSplitByOwners(files, max_depth) |
Peter Kotwicz | 70d971a | 2023-08-01 22:26:14 +0000 | [diff] [blame] | 349 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 350 | info_split_by_reviewers = {} |
Peter Kotwicz | 70d971a | 2023-08-01 22:26:14 +0000 | [diff] [blame] | 351 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 352 | for (directory, split_files) in info_split_by_owners.items(): |
| 353 | # Use '/' as a path separator in the branch name and the CL description |
| 354 | # and comment. |
| 355 | directory = directory.replace(os.path.sep, '/') |
| 356 | file_paths = [f for _, f in split_files] |
| 357 | # Convert reviewers list to tuple in order to use reviewers as key to |
| 358 | # dictionary. |
| 359 | reviewers = tuple( |
| 360 | cl.owners_client.SuggestOwners( |
| 361 | file_paths, exclude=[author, cl.owners_client.EVERYONE])) |
Peter Kotwicz | 70d971a | 2023-08-01 22:26:14 +0000 | [diff] [blame] | 362 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 363 | if not reviewers in info_split_by_reviewers: |
| 364 | info_split_by_reviewers[reviewers] = FilesAndOwnersDirectory([], []) |
| 365 | info_split_by_reviewers[reviewers].files.extend(split_files) |
| 366 | info_split_by_reviewers[reviewers].owners_directories.append(directory) |
Peter Kotwicz | 70d971a | 2023-08-01 22:26:14 +0000 | [diff] [blame] | 367 | |
Mike Frysinger | 124bb8e | 2023-09-06 05:48:55 +0000 | [diff] [blame] | 368 | return info_split_by_reviewers |