1*4882a593Smuzhiyun# Copyright (c) 2011 The Chromium OS Authors. 2*4882a593Smuzhiyun# 3*4882a593Smuzhiyun# SPDX-License-Identifier: GPL-2.0+ 4*4882a593Smuzhiyun# 5*4882a593Smuzhiyun 6*4882a593Smuzhiyunfrom __future__ import print_function 7*4882a593Smuzhiyun 8*4882a593Smuzhiyunimport itertools 9*4882a593Smuzhiyunimport os 10*4882a593Smuzhiyun 11*4882a593Smuzhiyunimport get_maintainer 12*4882a593Smuzhiyunimport gitutil 13*4882a593Smuzhiyunimport settings 14*4882a593Smuzhiyunimport terminal 15*4882a593Smuzhiyun 16*4882a593Smuzhiyun# Series-xxx tags that we understand 17*4882a593Smuzhiyunvalid_series = ['to', 'cc', 'version', 'changes', 'prefix', 'notes', 'name', 18*4882a593Smuzhiyun 'cover_cc', 'process_log'] 19*4882a593Smuzhiyun 20*4882a593Smuzhiyunclass Series(dict): 21*4882a593Smuzhiyun """Holds information about a patch series, including all tags. 22*4882a593Smuzhiyun 23*4882a593Smuzhiyun Vars: 24*4882a593Smuzhiyun cc: List of aliases/emails to Cc all patches to 25*4882a593Smuzhiyun commits: List of Commit objects, one for each patch 26*4882a593Smuzhiyun cover: List of lines in the cover letter 27*4882a593Smuzhiyun notes: List of lines in the notes 28*4882a593Smuzhiyun changes: (dict) List of changes for each version, The key is 29*4882a593Smuzhiyun the integer version number 30*4882a593Smuzhiyun allow_overwrite: Allow tags to overwrite an existing tag 31*4882a593Smuzhiyun """ 32*4882a593Smuzhiyun def __init__(self): 33*4882a593Smuzhiyun self.cc = [] 34*4882a593Smuzhiyun self.to = [] 35*4882a593Smuzhiyun self.cover_cc = [] 36*4882a593Smuzhiyun self.commits = [] 37*4882a593Smuzhiyun self.cover = None 38*4882a593Smuzhiyun self.notes = [] 39*4882a593Smuzhiyun self.changes = {} 40*4882a593Smuzhiyun self.allow_overwrite = False 41*4882a593Smuzhiyun 42*4882a593Smuzhiyun # Written in MakeCcFile() 43*4882a593Smuzhiyun # key: name of patch file 44*4882a593Smuzhiyun # value: list of email addresses 45*4882a593Smuzhiyun self._generated_cc = {} 46*4882a593Smuzhiyun 47*4882a593Smuzhiyun # These make us more like a dictionary 48*4882a593Smuzhiyun def __setattr__(self, name, value): 49*4882a593Smuzhiyun self[name] = value 50*4882a593Smuzhiyun 51*4882a593Smuzhiyun def __getattr__(self, name): 52*4882a593Smuzhiyun return self[name] 53*4882a593Smuzhiyun 54*4882a593Smuzhiyun def AddTag(self, commit, line, name, value): 55*4882a593Smuzhiyun """Add a new Series-xxx tag along with its value. 56*4882a593Smuzhiyun 57*4882a593Smuzhiyun Args: 58*4882a593Smuzhiyun line: Source line containing tag (useful for debug/error messages) 59*4882a593Smuzhiyun name: Tag name (part after 'Series-') 60*4882a593Smuzhiyun value: Tag value (part after 'Series-xxx: ') 61*4882a593Smuzhiyun """ 62*4882a593Smuzhiyun # If we already have it, then add to our list 63*4882a593Smuzhiyun name = name.replace('-', '_') 64*4882a593Smuzhiyun if name in self and not self.allow_overwrite: 65*4882a593Smuzhiyun values = value.split(',') 66*4882a593Smuzhiyun values = [str.strip() for str in values] 67*4882a593Smuzhiyun if type(self[name]) != type([]): 68*4882a593Smuzhiyun raise ValueError("In %s: line '%s': Cannot add another value " 69*4882a593Smuzhiyun "'%s' to series '%s'" % 70*4882a593Smuzhiyun (commit.hash, line, values, self[name])) 71*4882a593Smuzhiyun self[name] += values 72*4882a593Smuzhiyun 73*4882a593Smuzhiyun # Otherwise just set the value 74*4882a593Smuzhiyun elif name in valid_series: 75*4882a593Smuzhiyun if name=="notes": 76*4882a593Smuzhiyun self[name] = [value] 77*4882a593Smuzhiyun else: 78*4882a593Smuzhiyun self[name] = value 79*4882a593Smuzhiyun else: 80*4882a593Smuzhiyun raise ValueError("In %s: line '%s': Unknown 'Series-%s': valid " 81*4882a593Smuzhiyun "options are %s" % (commit.hash, line, name, 82*4882a593Smuzhiyun ', '.join(valid_series))) 83*4882a593Smuzhiyun 84*4882a593Smuzhiyun def AddCommit(self, commit): 85*4882a593Smuzhiyun """Add a commit into our list of commits 86*4882a593Smuzhiyun 87*4882a593Smuzhiyun We create a list of tags in the commit subject also. 88*4882a593Smuzhiyun 89*4882a593Smuzhiyun Args: 90*4882a593Smuzhiyun commit: Commit object to add 91*4882a593Smuzhiyun """ 92*4882a593Smuzhiyun commit.CheckTags() 93*4882a593Smuzhiyun self.commits.append(commit) 94*4882a593Smuzhiyun 95*4882a593Smuzhiyun def ShowActions(self, args, cmd, process_tags): 96*4882a593Smuzhiyun """Show what actions we will/would perform 97*4882a593Smuzhiyun 98*4882a593Smuzhiyun Args: 99*4882a593Smuzhiyun args: List of patch files we created 100*4882a593Smuzhiyun cmd: The git command we would have run 101*4882a593Smuzhiyun process_tags: Process tags as if they were aliases 102*4882a593Smuzhiyun """ 103*4882a593Smuzhiyun to_set = set(gitutil.BuildEmailList(self.to)); 104*4882a593Smuzhiyun cc_set = set(gitutil.BuildEmailList(self.cc)); 105*4882a593Smuzhiyun 106*4882a593Smuzhiyun col = terminal.Color() 107*4882a593Smuzhiyun print('Dry run, so not doing much. But I would do this:') 108*4882a593Smuzhiyun print() 109*4882a593Smuzhiyun print('Send a total of %d patch%s with %scover letter.' % ( 110*4882a593Smuzhiyun len(args), '' if len(args) == 1 else 'es', 111*4882a593Smuzhiyun self.get('cover') and 'a ' or 'no ')) 112*4882a593Smuzhiyun 113*4882a593Smuzhiyun # TODO: Colour the patches according to whether they passed checks 114*4882a593Smuzhiyun for upto in range(len(args)): 115*4882a593Smuzhiyun commit = self.commits[upto] 116*4882a593Smuzhiyun print(col.Color(col.GREEN, ' %s' % args[upto])) 117*4882a593Smuzhiyun cc_list = list(self._generated_cc[commit.patch]) 118*4882a593Smuzhiyun for email in set(cc_list) - to_set - cc_set: 119*4882a593Smuzhiyun if email == None: 120*4882a593Smuzhiyun email = col.Color(col.YELLOW, "<alias '%s' not found>" 121*4882a593Smuzhiyun % tag) 122*4882a593Smuzhiyun if email: 123*4882a593Smuzhiyun print(' Cc: ', email) 124*4882a593Smuzhiyun print 125*4882a593Smuzhiyun for item in to_set: 126*4882a593Smuzhiyun print('To:\t ', item) 127*4882a593Smuzhiyun for item in cc_set - to_set: 128*4882a593Smuzhiyun print('Cc:\t ', item) 129*4882a593Smuzhiyun print('Version: ', self.get('version')) 130*4882a593Smuzhiyun print('Prefix:\t ', self.get('prefix')) 131*4882a593Smuzhiyun if self.cover: 132*4882a593Smuzhiyun print('Cover: %d lines' % len(self.cover)) 133*4882a593Smuzhiyun cover_cc = gitutil.BuildEmailList(self.get('cover_cc', '')) 134*4882a593Smuzhiyun all_ccs = itertools.chain(cover_cc, *self._generated_cc.values()) 135*4882a593Smuzhiyun for email in set(all_ccs) - to_set - cc_set: 136*4882a593Smuzhiyun print(' Cc: ', email) 137*4882a593Smuzhiyun if cmd: 138*4882a593Smuzhiyun print('Git command: %s' % cmd) 139*4882a593Smuzhiyun 140*4882a593Smuzhiyun def MakeChangeLog(self, commit): 141*4882a593Smuzhiyun """Create a list of changes for each version. 142*4882a593Smuzhiyun 143*4882a593Smuzhiyun Return: 144*4882a593Smuzhiyun The change log as a list of strings, one per line 145*4882a593Smuzhiyun 146*4882a593Smuzhiyun Changes in v4: 147*4882a593Smuzhiyun - Jog the dial back closer to the widget 148*4882a593Smuzhiyun 149*4882a593Smuzhiyun Changes in v3: None 150*4882a593Smuzhiyun Changes in v2: 151*4882a593Smuzhiyun - Fix the widget 152*4882a593Smuzhiyun - Jog the dial 153*4882a593Smuzhiyun 154*4882a593Smuzhiyun etc. 155*4882a593Smuzhiyun """ 156*4882a593Smuzhiyun final = [] 157*4882a593Smuzhiyun process_it = self.get('process_log', '').split(',') 158*4882a593Smuzhiyun process_it = [item.strip() for item in process_it] 159*4882a593Smuzhiyun need_blank = False 160*4882a593Smuzhiyun for change in sorted(self.changes, reverse=True): 161*4882a593Smuzhiyun out = [] 162*4882a593Smuzhiyun for this_commit, text in self.changes[change]: 163*4882a593Smuzhiyun if commit and this_commit != commit: 164*4882a593Smuzhiyun continue 165*4882a593Smuzhiyun if 'uniq' not in process_it or text not in out: 166*4882a593Smuzhiyun out.append(text) 167*4882a593Smuzhiyun line = 'Changes in v%d:' % change 168*4882a593Smuzhiyun have_changes = len(out) > 0 169*4882a593Smuzhiyun if 'sort' in process_it: 170*4882a593Smuzhiyun out = sorted(out) 171*4882a593Smuzhiyun if have_changes: 172*4882a593Smuzhiyun out.insert(0, line) 173*4882a593Smuzhiyun else: 174*4882a593Smuzhiyun out = [line + ' None'] 175*4882a593Smuzhiyun if need_blank: 176*4882a593Smuzhiyun out.insert(0, '') 177*4882a593Smuzhiyun final += out 178*4882a593Smuzhiyun need_blank = have_changes 179*4882a593Smuzhiyun if self.changes: 180*4882a593Smuzhiyun final.append('') 181*4882a593Smuzhiyun return final 182*4882a593Smuzhiyun 183*4882a593Smuzhiyun def DoChecks(self): 184*4882a593Smuzhiyun """Check that each version has a change log 185*4882a593Smuzhiyun 186*4882a593Smuzhiyun Print an error if something is wrong. 187*4882a593Smuzhiyun """ 188*4882a593Smuzhiyun col = terminal.Color() 189*4882a593Smuzhiyun if self.get('version'): 190*4882a593Smuzhiyun changes_copy = dict(self.changes) 191*4882a593Smuzhiyun for version in range(1, int(self.version) + 1): 192*4882a593Smuzhiyun if self.changes.get(version): 193*4882a593Smuzhiyun del changes_copy[version] 194*4882a593Smuzhiyun else: 195*4882a593Smuzhiyun if version > 1: 196*4882a593Smuzhiyun str = 'Change log missing for v%d' % version 197*4882a593Smuzhiyun print(col.Color(col.RED, str)) 198*4882a593Smuzhiyun for version in changes_copy: 199*4882a593Smuzhiyun str = 'Change log for unknown version v%d' % version 200*4882a593Smuzhiyun print(col.Color(col.RED, str)) 201*4882a593Smuzhiyun elif self.changes: 202*4882a593Smuzhiyun str = 'Change log exists, but no version is set' 203*4882a593Smuzhiyun print(col.Color(col.RED, str)) 204*4882a593Smuzhiyun 205*4882a593Smuzhiyun def MakeCcFile(self, process_tags, cover_fname, raise_on_error, 206*4882a593Smuzhiyun add_maintainers): 207*4882a593Smuzhiyun """Make a cc file for us to use for per-commit Cc automation 208*4882a593Smuzhiyun 209*4882a593Smuzhiyun Also stores in self._generated_cc to make ShowActions() faster. 210*4882a593Smuzhiyun 211*4882a593Smuzhiyun Args: 212*4882a593Smuzhiyun process_tags: Process tags as if they were aliases 213*4882a593Smuzhiyun cover_fname: If non-None the name of the cover letter. 214*4882a593Smuzhiyun raise_on_error: True to raise an error when an alias fails to match, 215*4882a593Smuzhiyun False to just print a message. 216*4882a593Smuzhiyun add_maintainers: Either: 217*4882a593Smuzhiyun True/False to call the get_maintainers to CC maintainers 218*4882a593Smuzhiyun List of maintainers to include (for testing) 219*4882a593Smuzhiyun Return: 220*4882a593Smuzhiyun Filename of temp file created 221*4882a593Smuzhiyun """ 222*4882a593Smuzhiyun col = terminal.Color() 223*4882a593Smuzhiyun # Look for commit tags (of the form 'xxx:' at the start of the subject) 224*4882a593Smuzhiyun fname = '/tmp/patman.%d' % os.getpid() 225*4882a593Smuzhiyun fd = open(fname, 'w') 226*4882a593Smuzhiyun all_ccs = [] 227*4882a593Smuzhiyun for commit in self.commits: 228*4882a593Smuzhiyun cc = [] 229*4882a593Smuzhiyun if process_tags: 230*4882a593Smuzhiyun cc += gitutil.BuildEmailList(commit.tags, 231*4882a593Smuzhiyun raise_on_error=raise_on_error) 232*4882a593Smuzhiyun cc += gitutil.BuildEmailList(commit.cc_list, 233*4882a593Smuzhiyun raise_on_error=raise_on_error) 234*4882a593Smuzhiyun if type(add_maintainers) == type(cc): 235*4882a593Smuzhiyun cc += add_maintainers 236*4882a593Smuzhiyun elif add_maintainers: 237*4882a593Smuzhiyun cc += get_maintainer.GetMaintainer(commit.patch) 238*4882a593Smuzhiyun for x in set(cc) & set(settings.bounces): 239*4882a593Smuzhiyun print(col.Color(col.YELLOW, 'Skipping "%s"' % x)) 240*4882a593Smuzhiyun cc = set(cc) - set(settings.bounces) 241*4882a593Smuzhiyun cc = [m.encode('utf-8') if type(m) != str else m for m in cc] 242*4882a593Smuzhiyun all_ccs += cc 243*4882a593Smuzhiyun print(commit.patch, ', '.join(set(cc)), file=fd) 244*4882a593Smuzhiyun self._generated_cc[commit.patch] = cc 245*4882a593Smuzhiyun 246*4882a593Smuzhiyun if cover_fname: 247*4882a593Smuzhiyun cover_cc = gitutil.BuildEmailList(self.get('cover_cc', '')) 248*4882a593Smuzhiyun cover_cc = [m.encode('utf-8') if type(m) != str else m 249*4882a593Smuzhiyun for m in cover_cc] 250*4882a593Smuzhiyun cc_list = ', '.join([x.decode('utf-8') 251*4882a593Smuzhiyun for x in set(cover_cc + all_ccs)]) 252*4882a593Smuzhiyun print(cover_fname, cc_list.encode('utf-8'), file=fd) 253*4882a593Smuzhiyun 254*4882a593Smuzhiyun fd.close() 255*4882a593Smuzhiyun return fname 256*4882a593Smuzhiyun 257*4882a593Smuzhiyun def AddChange(self, version, commit, info): 258*4882a593Smuzhiyun """Add a new change line to a version. 259*4882a593Smuzhiyun 260*4882a593Smuzhiyun This will later appear in the change log. 261*4882a593Smuzhiyun 262*4882a593Smuzhiyun Args: 263*4882a593Smuzhiyun version: version number to add change list to 264*4882a593Smuzhiyun info: change line for this version 265*4882a593Smuzhiyun """ 266*4882a593Smuzhiyun if not self.changes.get(version): 267*4882a593Smuzhiyun self.changes[version] = [] 268*4882a593Smuzhiyun self.changes[version].append([commit, info]) 269*4882a593Smuzhiyun 270*4882a593Smuzhiyun def GetPatchPrefix(self): 271*4882a593Smuzhiyun """Get the patch version string 272*4882a593Smuzhiyun 273*4882a593Smuzhiyun Return: 274*4882a593Smuzhiyun Patch string, like 'RFC PATCH v5' or just 'PATCH' 275*4882a593Smuzhiyun """ 276*4882a593Smuzhiyun git_prefix = gitutil.GetDefaultSubjectPrefix() 277*4882a593Smuzhiyun if git_prefix: 278*4882a593Smuzhiyun git_prefix = '%s][' % git_prefix 279*4882a593Smuzhiyun else: 280*4882a593Smuzhiyun git_prefix = '' 281*4882a593Smuzhiyun 282*4882a593Smuzhiyun version = '' 283*4882a593Smuzhiyun if self.get('version'): 284*4882a593Smuzhiyun version = ' v%s' % self['version'] 285*4882a593Smuzhiyun 286*4882a593Smuzhiyun # Get patch name prefix 287*4882a593Smuzhiyun prefix = '' 288*4882a593Smuzhiyun if self.get('prefix'): 289*4882a593Smuzhiyun prefix = '%s ' % self['prefix'] 290*4882a593Smuzhiyun return '%s%sPATCH%s' % (git_prefix, prefix, version) 291