cl split: Refactor splitting datastructure

This is a preparatory CL which refactors some internal logic to make
writing future CLs easier. In particular, it changes from treating a
CL as "a list of files with the same reviewer" to treating them as a
triple of (reviewers, files, directories). It does so using a dataclass
to make it easy to work with.

It also adds an EmitWarning convenience function, which will be more
heavily used in future CLs.

Bug: 389069356
Change-Id: I842d59ba69f5db5e3745fa39832794cdc1eb222e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/tools/depot_tools/+/6190430
Commit-Queue: Devon Loehr <dloehr@google.com>
Reviewed-by: Josip Sokcevic <sokcevic@chromium.org>
changes/30/6190430/6
Devon Loehr 3 months ago committed by LUCI CQ
parent 682a6e1194
commit f99834cd29

@ -5,10 +5,12 @@
"""Splits a branch into smaller branches and uploads CLs.""" """Splits a branch into smaller branches and uploads CLs."""
import collections import collections
import dataclasses
import os import os
import re import re
import subprocess2 import subprocess2
import sys import sys
from typing import List, Set, Tuple, Dict, Any
import gclient_utils import gclient_utils
import git_footers import git_footers
@ -27,10 +29,62 @@ CL_SPLIT_FORCE_LIMIT = 10
# will be listed. # will be listed.
CL_SPLIT_TOP_REVIEWERS = 5 CL_SPLIT_TOP_REVIEWERS = 5
def EmitWarning(msg: str):
print("Warning: ", msg)
FilesAndOwnersDirectory = collections.namedtuple("FilesAndOwnersDirectory", FilesAndOwnersDirectory = collections.namedtuple("FilesAndOwnersDirectory",
"files owners_directories") "files owners_directories")
@dataclasses.dataclass
class CLInfo:
"""
Data structure representing a single CL. The script will split the large CL
into a list of these.
Fields:
- reviewers: the reviewers the CL will be sent to.
- files: a list of <action>, <file> pairs in the CL.
Has the same format as `git status`.
- directories: a string representing the directories containing the files
in this CL. This is only used for replacing $directory in
the user-provided CL description.
"""
# Have to use default_factory because lists are mutable
reviewers: Set[str] = dataclasses.field(default_factory=set)
files: List[Tuple[str, str]] = dataclasses.field(default_factory=list)
# This is only used for formatting in the CL description, so it just
# has to be convertible to string.
directories: Any = ""
def FormatForPrinting(self) -> str:
"""
Format the CLInfo for printing to a file in a human-readable format.
"""
# Don't quote the reviewer emails in the output
reviewers_str = ", ".join(self.reviewers)
lines = [
f"Reviewers: [{reviewers_str}]", f"Directories: {self.directories}"
] + [f"{action}, {file}" for (action, file) in self.files]
return "\n".join(lines)
def CLInfoFromFilesAndOwnersDirectoriesDict(
d: Dict[Tuple[str], FilesAndOwnersDirectory]) -> List[CLInfo]:
"""
Transform a dictionary mapping reviewer tuples to FilesAndOwnersDirectories
into a list of CLInfo
"""
cl_infos = []
for (reviewers, fod) in d.items():
cl_infos.append(
CLInfo(set(reviewers), fod.files, fod.owners_directories))
return cl_infos
def EnsureInGitRepository(): def EnsureInGitRepository():
"""Throws an exception if the current directory is not a git repository.""" """Throws an exception if the current directory is not a git repository."""
git.run('rev-parse') git.run('rev-parse')
@ -230,7 +284,7 @@ def LoadDescription(description_file, dry_run):
return gclient_utils.FileRead(description_file) return gclient_utils.FileRead(description_file)
def PrintSummary(files_split_by_reviewers, refactor_branch): def PrintSummary(cl_infos, refactor_branch):
"""Print a brief summary of the splitting so the user """Print a brief summary of the splitting so the user
can review it before uploading. can review it before uploading.
@ -238,16 +292,17 @@ def PrintSummary(files_split_by_reviewers, refactor_branch):
files_split_by_reviewers: A dictionary mapping reviewer tuples files_split_by_reviewers: A dictionary mapping reviewer tuples
to the files and directories assigned to them. to the files and directories assigned to them.
""" """
for info in cl_infos:
print(f'Reviewers: {info.reviewers}, files: {len(info.files)}, ',
f'directories: {info.directories}')
for reviewers, file_info in files_split_by_reviewers.items(): num_cls = len(cl_infos)
print(f'Reviewers: {reviewers}, files: {len(file_info.files)}, '
f'directories: {file_info.owners_directories}')
num_cls = len(files_split_by_reviewers)
print(f'\nWill split branch {refactor_branch} into {num_cls} CLs. ' print(f'\nWill split branch {refactor_branch} into {num_cls} CLs. '
'Please quickly review them before proceeding.\n') 'Please quickly review them before proceeding.\n')
if (num_cls > CL_SPLIT_FORCE_LIMIT): if (num_cls > CL_SPLIT_FORCE_LIMIT):
print('Warning: Uploading this many CLs may potentially ' EmitWarning(
'Uploading this many CLs may potentially '
'reach the limit of concurrent runs, imposed on you by the ' 'reach the limit of concurrent runs, imposed on you by the '
'build infrastructure. Your runs may be throttled as a ' 'build infrastructure. Your runs may be throttled as a '
'result.\n\nPlease email infra-dev@chromium.org if you ' 'result.\n\nPlease email infra-dev@chromium.org if you '
@ -311,30 +366,30 @@ def SplitCl(description_file, comment_file, changelist, cmd_upload, dry_run,
files_split_by_reviewers = SelectReviewersForFiles( files_split_by_reviewers = SelectReviewersForFiles(
cl, author, files, max_depth) cl, author, files, max_depth)
cl_infos = CLInfoFromFilesAndOwnersDirectoriesDict(
files_split_by_reviewers)
if not dry_run: if not dry_run:
PrintSummary(files_split_by_reviewers, refactor_branch) PrintSummary(cl_infos, refactor_branch)
answer = gclient_utils.AskForData('Proceed? (y/N):') answer = gclient_utils.AskForData('Proceed? (y/N):')
if answer.lower() != 'y': if answer.lower() != 'y':
return 0 return 0
cls_per_reviewer = collections.defaultdict(int) cls_per_reviewer = collections.defaultdict(int)
for cl_index, (reviewers, cl_info) in \ for cl_index, cl_info in enumerate(cl_infos, 1):
enumerate(files_split_by_reviewers.items(), 1):
# Convert reviewers from tuple to set. # Convert reviewers from tuple to set.
reviewer_set = set(reviewers)
if dry_run: if dry_run:
file_paths = [f for _, f in cl_info.files] file_paths = [f for _, f in cl_info.files]
PrintClInfo(cl_index, len(files_split_by_reviewers), PrintClInfo(cl_index, len(cl_infos), cl_info.directories,
cl_info.owners_directories, file_paths, description, file_paths, description, cl_info.reviewers,
reviewer_set, cq_dry_run, enable_auto_submit, topic) cq_dry_run, enable_auto_submit, topic)
else: else:
UploadCl(refactor_branch, refactor_branch_upstream, UploadCl(refactor_branch, refactor_branch_upstream,
cl_info.owners_directories, cl_info.files, description, cl_info.directories, cl_info.files, description,
comment, reviewer_set, changelist, cmd_upload, comment, cl_info.reviewers, changelist, cmd_upload,
cq_dry_run, enable_auto_submit, topic, repository_root) cq_dry_run, enable_auto_submit, topic, repository_root)
for reviewer in reviewers: for reviewer in cl_info.reviewers:
cls_per_reviewer[reviewer] += 1 cls_per_reviewer[reviewer] += 1
# List the top reviewers that will be sent the most CLs as a result of # List the top reviewers that will be sent the most CLs as a result of

Loading…
Cancel
Save