xref: /openbmc/u-boot/tools/patman/series.py (revision c9aa831e)
1# Copyright (c) 2011 The Chromium OS Authors.
2#
3# See file CREDITS for list of people who contributed to this
4# project.
5#
6# This program is free software; you can redistribute it and/or
7# modify it under the terms of the GNU General Public License as
8# published by the Free Software Foundation; either version 2 of
9# the License, or (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program; if not, write to the Free Software
18# Foundation, Inc., 59 Temple Place, Suite 330, Boston,
19# MA 02111-1307 USA
20#
21
22import itertools
23import os
24
25import get_maintainer
26import gitutil
27import terminal
28
29# Series-xxx tags that we understand
30valid_series = ['to', 'cc', 'version', 'changes', 'prefix', 'notes', 'name'];
31
32class Series(dict):
33    """Holds information about a patch series, including all tags.
34
35    Vars:
36        cc: List of aliases/emails to Cc all patches to
37        commits: List of Commit objects, one for each patch
38        cover: List of lines in the cover letter
39        notes: List of lines in the notes
40        changes: (dict) List of changes for each version, The key is
41            the integer version number
42    """
43    def __init__(self):
44        self.cc = []
45        self.to = []
46        self.commits = []
47        self.cover = None
48        self.notes = []
49        self.changes = {}
50
51        # Written in MakeCcFile()
52        #  key: name of patch file
53        #  value: list of email addresses
54        self._generated_cc = {}
55
56    # These make us more like a dictionary
57    def __setattr__(self, name, value):
58        self[name] = value
59
60    def __getattr__(self, name):
61        return self[name]
62
63    def AddTag(self, commit, line, name, value):
64        """Add a new Series-xxx tag along with its value.
65
66        Args:
67            line: Source line containing tag (useful for debug/error messages)
68            name: Tag name (part after 'Series-')
69            value: Tag value (part after 'Series-xxx: ')
70        """
71        # If we already have it, then add to our list
72        if name in self:
73            values = value.split(',')
74            values = [str.strip() for str in values]
75            if type(self[name]) != type([]):
76                raise ValueError("In %s: line '%s': Cannot add another value "
77                        "'%s' to series '%s'" %
78                            (commit.hash, line, values, self[name]))
79            self[name] += values
80
81        # Otherwise just set the value
82        elif name in valid_series:
83            self[name] = value
84        else:
85            raise ValueError("In %s: line '%s': Unknown 'Series-%s': valid "
86                        "options are %s" % (commit.hash, line, name,
87                            ', '.join(valid_series)))
88
89    def AddCommit(self, commit):
90        """Add a commit into our list of commits
91
92        We create a list of tags in the commit subject also.
93
94        Args:
95            commit: Commit object to add
96        """
97        commit.CheckTags()
98        self.commits.append(commit)
99
100    def ShowActions(self, args, cmd, process_tags):
101        """Show what actions we will/would perform
102
103        Args:
104            args: List of patch files we created
105            cmd: The git command we would have run
106            process_tags: Process tags as if they were aliases
107        """
108        col = terminal.Color()
109        print 'Dry run, so not doing much. But I would do this:'
110        print
111        print 'Send a total of %d patch%s with %scover letter.' % (
112                len(args), '' if len(args) == 1 else 'es',
113                self.get('cover') and 'a ' or 'no ')
114
115        # TODO: Colour the patches according to whether they passed checks
116        for upto in range(len(args)):
117            commit = self.commits[upto]
118            print col.Color(col.GREEN, '   %s' % args[upto])
119            cc_list = list(self._generated_cc[commit.patch])
120
121            # Skip items in To list
122            if 'to' in self:
123                try:
124                    map(cc_list.remove, gitutil.BuildEmailList(self.to))
125                except ValueError:
126                    pass
127
128            for email in cc_list:
129                if email == None:
130                    email = col.Color(col.YELLOW, "<alias '%s' not found>"
131                            % tag)
132                if email:
133                    print '      Cc: ',email
134        print
135        for item in gitutil.BuildEmailList(self.get('to', '<none>')):
136            print 'To:\t ', item
137        for item in gitutil.BuildEmailList(self.cc):
138            print 'Cc:\t ', item
139        print 'Version: ', self.get('version')
140        print 'Prefix:\t ', self.get('prefix')
141        if self.cover:
142            print 'Cover: %d lines' % len(self.cover)
143            all_ccs = itertools.chain(*self._generated_cc.values())
144            for email in set(all_ccs):
145                    print '      Cc: ',email
146        if cmd:
147            print 'Git command: %s' % cmd
148
149    def MakeChangeLog(self, commit):
150        """Create a list of changes for each version.
151
152        Return:
153            The change log as a list of strings, one per line
154
155            Changes in v4:
156            - Jog the dial back closer to the widget
157
158            Changes in v3: None
159            Changes in v2:
160            - Fix the widget
161            - Jog the dial
162
163            etc.
164        """
165        final = []
166        need_blank = False
167        for change in sorted(self.changes, reverse=True):
168            out = []
169            for this_commit, text in self.changes[change]:
170                if commit and this_commit != commit:
171                    continue
172                out.append(text)
173            line = 'Changes in v%d:' % change
174            have_changes = len(out) > 0
175            if have_changes:
176                out.insert(0, line)
177            else:
178                out = [line + ' None']
179            if need_blank:
180                out.insert(0, '')
181            final += out
182            need_blank = have_changes
183        if self.changes:
184            final.append('')
185        return final
186
187    def DoChecks(self):
188        """Check that each version has a change log
189
190        Print an error if something is wrong.
191        """
192        col = terminal.Color()
193        if self.get('version'):
194            changes_copy = dict(self.changes)
195            for version in range(1, int(self.version) + 1):
196                if self.changes.get(version):
197                    del changes_copy[version]
198                else:
199                    if version > 1:
200                        str = 'Change log missing for v%d' % version
201                        print col.Color(col.RED, str)
202            for version in changes_copy:
203                str = 'Change log for unknown version v%d' % version
204                print col.Color(col.RED, str)
205        elif self.changes:
206            str = 'Change log exists, but no version is set'
207            print col.Color(col.RED, str)
208
209    def MakeCcFile(self, process_tags, cover_fname):
210        """Make a cc file for us to use for per-commit Cc automation
211
212        Also stores in self._generated_cc to make ShowActions() faster.
213
214        Args:
215            process_tags: Process tags as if they were aliases
216            cover_fname: If non-None the name of the cover letter.
217        Return:
218            Filename of temp file created
219        """
220        # Look for commit tags (of the form 'xxx:' at the start of the subject)
221        fname = '/tmp/patman.%d' % os.getpid()
222        fd = open(fname, 'w')
223        all_ccs = []
224        for commit in self.commits:
225            list = []
226            if process_tags:
227                list += gitutil.BuildEmailList(commit.tags)
228            list += gitutil.BuildEmailList(commit.cc_list)
229            list += get_maintainer.GetMaintainer(commit.patch)
230            all_ccs += list
231            print >>fd, commit.patch, ', '.join(list)
232            self._generated_cc[commit.patch] = list
233
234        if cover_fname:
235            print >>fd, cover_fname, ', '.join(set(all_ccs))
236
237        fd.close()
238        return fname
239
240    def AddChange(self, version, commit, info):
241        """Add a new change line to a version.
242
243        This will later appear in the change log.
244
245        Args:
246            version: version number to add change list to
247            info: change line for this version
248        """
249        if not self.changes.get(version):
250            self.changes[version] = []
251        self.changes[version].append([commit, info])
252
253    def GetPatchPrefix(self):
254        """Get the patch version string
255
256        Return:
257            Patch string, like 'RFC PATCH v5' or just 'PATCH'
258        """
259        version = ''
260        if self.get('version'):
261            version = ' v%s' % self['version']
262
263        # Get patch name prefix
264        prefix = ''
265        if self.get('prefix'):
266            prefix = '%s ' % self['prefix']
267        return '%sPATCH%s' % (prefix, version)
268