10d24de9dSSimon Glass# Copyright (c) 2011 The Chromium OS Authors. 20d24de9dSSimon Glass# 31a459660SWolfgang Denk# SPDX-License-Identifier: GPL-2.0+ 40d24de9dSSimon Glass# 50d24de9dSSimon Glass 6a920a17bSPaul Burtonfrom __future__ import print_function 7a920a17bSPaul Burton 831187255SDoug Andersonimport itertools 90d24de9dSSimon Glassimport os 100d24de9dSSimon Glass 1121a19d70SDoug Andersonimport get_maintainer 120d24de9dSSimon Glassimport gitutil 13*ad6e7aa6SChris Packhamimport settings 140d24de9dSSimon Glassimport terminal 150d24de9dSSimon Glass 160d24de9dSSimon Glass# Series-xxx tags that we understand 17fe2f8d9eSSimon Glassvalid_series = ['to', 'cc', 'version', 'changes', 'prefix', 'notes', 'name', 18d9917b0bSSimon Glass 'cover_cc', 'process_log'] 190d24de9dSSimon Glass 200d24de9dSSimon Glassclass Series(dict): 210d24de9dSSimon Glass """Holds information about a patch series, including all tags. 220d24de9dSSimon Glass 230d24de9dSSimon Glass Vars: 240d24de9dSSimon Glass cc: List of aliases/emails to Cc all patches to 250d24de9dSSimon Glass commits: List of Commit objects, one for each patch 260d24de9dSSimon Glass cover: List of lines in the cover letter 270d24de9dSSimon Glass notes: List of lines in the notes 280d24de9dSSimon Glass changes: (dict) List of changes for each version, The key is 290d24de9dSSimon Glass the integer version number 30f0b739f1SSimon Glass allow_overwrite: Allow tags to overwrite an existing tag 310d24de9dSSimon Glass """ 320d24de9dSSimon Glass def __init__(self): 330d24de9dSSimon Glass self.cc = [] 340d24de9dSSimon Glass self.to = [] 35fe2f8d9eSSimon Glass self.cover_cc = [] 360d24de9dSSimon Glass self.commits = [] 370d24de9dSSimon Glass self.cover = None 380d24de9dSSimon Glass self.notes = [] 390d24de9dSSimon Glass self.changes = {} 40f0b739f1SSimon Glass self.allow_overwrite = False 410d24de9dSSimon Glass 42d94566a1SDoug Anderson # Written in MakeCcFile() 43d94566a1SDoug Anderson # key: name of patch file 44d94566a1SDoug Anderson # value: list of email addresses 45d94566a1SDoug Anderson self._generated_cc = {} 46d94566a1SDoug Anderson 470d24de9dSSimon Glass # These make us more like a dictionary 480d24de9dSSimon Glass def __setattr__(self, name, value): 490d24de9dSSimon Glass self[name] = value 500d24de9dSSimon Glass 510d24de9dSSimon Glass def __getattr__(self, name): 520d24de9dSSimon Glass return self[name] 530d24de9dSSimon Glass 540d24de9dSSimon Glass def AddTag(self, commit, line, name, value): 550d24de9dSSimon Glass """Add a new Series-xxx tag along with its value. 560d24de9dSSimon Glass 570d24de9dSSimon Glass Args: 580d24de9dSSimon Glass line: Source line containing tag (useful for debug/error messages) 590d24de9dSSimon Glass name: Tag name (part after 'Series-') 600d24de9dSSimon Glass value: Tag value (part after 'Series-xxx: ') 610d24de9dSSimon Glass """ 620d24de9dSSimon Glass # If we already have it, then add to our list 63fe2f8d9eSSimon Glass name = name.replace('-', '_') 64f0b739f1SSimon Glass if name in self and not self.allow_overwrite: 650d24de9dSSimon Glass values = value.split(',') 660d24de9dSSimon Glass values = [str.strip() for str in values] 670d24de9dSSimon Glass if type(self[name]) != type([]): 680d24de9dSSimon Glass raise ValueError("In %s: line '%s': Cannot add another value " 690d24de9dSSimon Glass "'%s' to series '%s'" % 700d24de9dSSimon Glass (commit.hash, line, values, self[name])) 710d24de9dSSimon Glass self[name] += values 720d24de9dSSimon Glass 730d24de9dSSimon Glass # Otherwise just set the value 740d24de9dSSimon Glass elif name in valid_series: 75070b781bSAlbert ARIBAUD if name=="notes": 76070b781bSAlbert ARIBAUD self[name] = [value] 77070b781bSAlbert ARIBAUD else: 780d24de9dSSimon Glass self[name] = value 790d24de9dSSimon Glass else: 800d24de9dSSimon Glass raise ValueError("In %s: line '%s': Unknown 'Series-%s': valid " 81ef0e9de8SSimon Glass "options are %s" % (commit.hash, line, name, 820d24de9dSSimon Glass ', '.join(valid_series))) 830d24de9dSSimon Glass 840d24de9dSSimon Glass def AddCommit(self, commit): 850d24de9dSSimon Glass """Add a commit into our list of commits 860d24de9dSSimon Glass 870d24de9dSSimon Glass We create a list of tags in the commit subject also. 880d24de9dSSimon Glass 890d24de9dSSimon Glass Args: 900d24de9dSSimon Glass commit: Commit object to add 910d24de9dSSimon Glass """ 920d24de9dSSimon Glass commit.CheckTags() 930d24de9dSSimon Glass self.commits.append(commit) 940d24de9dSSimon Glass 950d24de9dSSimon Glass def ShowActions(self, args, cmd, process_tags): 960d24de9dSSimon Glass """Show what actions we will/would perform 970d24de9dSSimon Glass 980d24de9dSSimon Glass Args: 990d24de9dSSimon Glass args: List of patch files we created 1000d24de9dSSimon Glass cmd: The git command we would have run 1010d24de9dSSimon Glass process_tags: Process tags as if they were aliases 1020d24de9dSSimon Glass """ 1032181830fSPeter Tyser to_set = set(gitutil.BuildEmailList(self.to)); 1042181830fSPeter Tyser cc_set = set(gitutil.BuildEmailList(self.cc)); 1052181830fSPeter Tyser 1060d24de9dSSimon Glass col = terminal.Color() 107a920a17bSPaul Burton print('Dry run, so not doing much. But I would do this:') 108a920a17bSPaul Burton print() 109a920a17bSPaul Burton print('Send a total of %d patch%s with %scover letter.' % ( 1100d24de9dSSimon Glass len(args), '' if len(args) == 1 else 'es', 111a920a17bSPaul Burton self.get('cover') and 'a ' or 'no ')) 1120d24de9dSSimon Glass 1130d24de9dSSimon Glass # TODO: Colour the patches according to whether they passed checks 1140d24de9dSSimon Glass for upto in range(len(args)): 1150d24de9dSSimon Glass commit = self.commits[upto] 116a920a17bSPaul Burton print(col.Color(col.GREEN, ' %s' % args[upto])) 117d94566a1SDoug Anderson cc_list = list(self._generated_cc[commit.patch]) 1182181830fSPeter Tyser for email in set(cc_list) - to_set - cc_set: 1190d24de9dSSimon Glass if email == None: 1200d24de9dSSimon Glass email = col.Color(col.YELLOW, "<alias '%s' not found>" 1210d24de9dSSimon Glass % tag) 1220d24de9dSSimon Glass if email: 1236f8abf76SSimon Glass print(' Cc: ', email) 1240d24de9dSSimon Glass print 1252181830fSPeter Tyser for item in to_set: 126a920a17bSPaul Burton print('To:\t ', item) 1272181830fSPeter Tyser for item in cc_set - to_set: 128a920a17bSPaul Burton print('Cc:\t ', item) 129a920a17bSPaul Burton print('Version: ', self.get('version')) 130a920a17bSPaul Burton print('Prefix:\t ', self.get('prefix')) 1310d24de9dSSimon Glass if self.cover: 132a920a17bSPaul Burton print('Cover: %d lines' % len(self.cover)) 133fe2f8d9eSSimon Glass cover_cc = gitutil.BuildEmailList(self.get('cover_cc', '')) 134fe2f8d9eSSimon Glass all_ccs = itertools.chain(cover_cc, *self._generated_cc.values()) 1352181830fSPeter Tyser for email in set(all_ccs) - to_set - cc_set: 136a920a17bSPaul Burton print(' Cc: ', email) 1370d24de9dSSimon Glass if cmd: 138a920a17bSPaul Burton print('Git command: %s' % cmd) 1390d24de9dSSimon Glass 1400d24de9dSSimon Glass def MakeChangeLog(self, commit): 1410d24de9dSSimon Glass """Create a list of changes for each version. 1420d24de9dSSimon Glass 1430d24de9dSSimon Glass Return: 1440d24de9dSSimon Glass The change log as a list of strings, one per line 1450d24de9dSSimon Glass 14627e97600SSimon Glass Changes in v4: 147244e6f97SOtavio Salvador - Jog the dial back closer to the widget 148244e6f97SOtavio Salvador 14927e97600SSimon Glass Changes in v3: None 15027e97600SSimon Glass Changes in v2: 1510d24de9dSSimon Glass - Fix the widget 1520d24de9dSSimon Glass - Jog the dial 1530d24de9dSSimon Glass 1540d24de9dSSimon Glass etc. 1550d24de9dSSimon Glass """ 1560d24de9dSSimon Glass final = [] 157645b271aSSimon Glass process_it = self.get('process_log', '').split(',') 158645b271aSSimon Glass process_it = [item.strip() for item in process_it] 1590d24de9dSSimon Glass need_blank = False 160244e6f97SOtavio Salvador for change in sorted(self.changes, reverse=True): 1610d24de9dSSimon Glass out = [] 1620d24de9dSSimon Glass for this_commit, text in self.changes[change]: 1630d24de9dSSimon Glass if commit and this_commit != commit: 1640d24de9dSSimon Glass continue 165645b271aSSimon Glass if 'uniq' not in process_it or text not in out: 1660d24de9dSSimon Glass out.append(text) 16727e97600SSimon Glass line = 'Changes in v%d:' % change 16827e97600SSimon Glass have_changes = len(out) > 0 169645b271aSSimon Glass if 'sort' in process_it: 170645b271aSSimon Glass out = sorted(out) 17127e97600SSimon Glass if have_changes: 17227e97600SSimon Glass out.insert(0, line) 17327e97600SSimon Glass else: 17427e97600SSimon Glass out = [line + ' None'] 1750d24de9dSSimon Glass if need_blank: 17627e97600SSimon Glass out.insert(0, '') 1770d24de9dSSimon Glass final += out 17827e97600SSimon Glass need_blank = have_changes 1790d24de9dSSimon Glass if self.changes: 1800d24de9dSSimon Glass final.append('') 1810d24de9dSSimon Glass return final 1820d24de9dSSimon Glass 1830d24de9dSSimon Glass def DoChecks(self): 1840d24de9dSSimon Glass """Check that each version has a change log 1850d24de9dSSimon Glass 1860d24de9dSSimon Glass Print an error if something is wrong. 1870d24de9dSSimon Glass """ 1880d24de9dSSimon Glass col = terminal.Color() 1890d24de9dSSimon Glass if self.get('version'): 1900d24de9dSSimon Glass changes_copy = dict(self.changes) 191d5f81d8aSOtavio Salvador for version in range(1, int(self.version) + 1): 1920d24de9dSSimon Glass if self.changes.get(version): 1930d24de9dSSimon Glass del changes_copy[version] 1940d24de9dSSimon Glass else: 195d5f81d8aSOtavio Salvador if version > 1: 1960d24de9dSSimon Glass str = 'Change log missing for v%d' % version 197a920a17bSPaul Burton print(col.Color(col.RED, str)) 1980d24de9dSSimon Glass for version in changes_copy: 1990d24de9dSSimon Glass str = 'Change log for unknown version v%d' % version 200a920a17bSPaul Burton print(col.Color(col.RED, str)) 2010d24de9dSSimon Glass elif self.changes: 2020d24de9dSSimon Glass str = 'Change log exists, but no version is set' 203a920a17bSPaul Burton print(col.Color(col.RED, str)) 2040d24de9dSSimon Glass 205983a2749SSimon Glass def MakeCcFile(self, process_tags, cover_fname, raise_on_error, 206983a2749SSimon Glass add_maintainers): 2070d24de9dSSimon Glass """Make a cc file for us to use for per-commit Cc automation 2080d24de9dSSimon Glass 209d94566a1SDoug Anderson Also stores in self._generated_cc to make ShowActions() faster. 210d94566a1SDoug Anderson 2110d24de9dSSimon Glass Args: 2120d24de9dSSimon Glass process_tags: Process tags as if they were aliases 21331187255SDoug Anderson cover_fname: If non-None the name of the cover letter. 214a1318f7cSSimon Glass raise_on_error: True to raise an error when an alias fails to match, 215a1318f7cSSimon Glass False to just print a message. 2161f487f85SSimon Glass add_maintainers: Either: 2171f487f85SSimon Glass True/False to call the get_maintainers to CC maintainers 2181f487f85SSimon Glass List of maintainers to include (for testing) 2190d24de9dSSimon Glass Return: 2200d24de9dSSimon Glass Filename of temp file created 2210d24de9dSSimon Glass """ 222*ad6e7aa6SChris Packham col = terminal.Color() 2230d24de9dSSimon Glass # Look for commit tags (of the form 'xxx:' at the start of the subject) 2240d24de9dSSimon Glass fname = '/tmp/patman.%d' % os.getpid() 2250d24de9dSSimon Glass fd = open(fname, 'w') 22631187255SDoug Anderson all_ccs = [] 2270d24de9dSSimon Glass for commit in self.commits: 228a44f4fb7SSimon Glass cc = [] 2290d24de9dSSimon Glass if process_tags: 230a44f4fb7SSimon Glass cc += gitutil.BuildEmailList(commit.tags, 231a1318f7cSSimon Glass raise_on_error=raise_on_error) 232a44f4fb7SSimon Glass cc += gitutil.BuildEmailList(commit.cc_list, 233a1318f7cSSimon Glass raise_on_error=raise_on_error) 234a44f4fb7SSimon Glass if type(add_maintainers) == type(cc): 235a44f4fb7SSimon Glass cc += add_maintainers 2361f487f85SSimon Glass elif add_maintainers: 237a44f4fb7SSimon Glass cc += get_maintainer.GetMaintainer(commit.patch) 238*ad6e7aa6SChris Packham for x in set(cc) & set(settings.bounces): 239*ad6e7aa6SChris Packham print(col.Color(col.YELLOW, 'Skipping "%s"' % x)) 240*ad6e7aa6SChris Packham cc = set(cc) - set(settings.bounces) 241a44f4fb7SSimon Glass cc = [m.encode('utf-8') if type(m) != str else m for m in cc] 242a44f4fb7SSimon Glass all_ccs += cc 243a44f4fb7SSimon Glass print(commit.patch, ', '.join(set(cc)), file=fd) 244a44f4fb7SSimon Glass self._generated_cc[commit.patch] = cc 2450d24de9dSSimon Glass 24631187255SDoug Anderson if cover_fname: 247fe2f8d9eSSimon Glass cover_cc = gitutil.BuildEmailList(self.get('cover_cc', '')) 2486f8abf76SSimon Glass cover_cc = [m.encode('utf-8') if type(m) != str else m 2496f8abf76SSimon Glass for m in cover_cc] 2506f8abf76SSimon Glass cc_list = ', '.join([x.decode('utf-8') 2516f8abf76SSimon Glass for x in set(cover_cc + all_ccs)]) 252f11a0af7SChris Packham print(cover_fname, cc_list.encode('utf-8'), file=fd) 25331187255SDoug Anderson 2540d24de9dSSimon Glass fd.close() 2550d24de9dSSimon Glass return fname 2560d24de9dSSimon Glass 2570d24de9dSSimon Glass def AddChange(self, version, commit, info): 2580d24de9dSSimon Glass """Add a new change line to a version. 2590d24de9dSSimon Glass 2600d24de9dSSimon Glass This will later appear in the change log. 2610d24de9dSSimon Glass 2620d24de9dSSimon Glass Args: 2630d24de9dSSimon Glass version: version number to add change list to 2640d24de9dSSimon Glass info: change line for this version 2650d24de9dSSimon Glass """ 2660d24de9dSSimon Glass if not self.changes.get(version): 2670d24de9dSSimon Glass self.changes[version] = [] 2680d24de9dSSimon Glass self.changes[version].append([commit, info]) 2690d24de9dSSimon Glass 2700d24de9dSSimon Glass def GetPatchPrefix(self): 2710d24de9dSSimon Glass """Get the patch version string 2720d24de9dSSimon Glass 2730d24de9dSSimon Glass Return: 2740d24de9dSSimon Glass Patch string, like 'RFC PATCH v5' or just 'PATCH' 2750d24de9dSSimon Glass """ 2763871cd85SWu, Josh git_prefix = gitutil.GetDefaultSubjectPrefix() 2773871cd85SWu, Josh if git_prefix: 2783871cd85SWu, Josh git_prefix = '%s][' % git_prefix 2793871cd85SWu, Josh else: 2803871cd85SWu, Josh git_prefix = '' 2813871cd85SWu, Josh 2820d24de9dSSimon Glass version = '' 2830d24de9dSSimon Glass if self.get('version'): 2840d24de9dSSimon Glass version = ' v%s' % self['version'] 2850d24de9dSSimon Glass 2860d24de9dSSimon Glass # Get patch name prefix 2870d24de9dSSimon Glass prefix = '' 2880d24de9dSSimon Glass if self.get('prefix'): 2890d24de9dSSimon Glass prefix = '%s ' % self['prefix'] 2903871cd85SWu, Josh return '%s%sPATCH%s' % (git_prefix, prefix, version) 291