blob: ca3ba4a03e49a01e40522b5fdd03df5ef514fdbc [file] [log] [blame]
Simon Glass0d24de92012-01-14 15:12:45 +00001# 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 command
23import re
24import os
25import series
26import settings
27import subprocess
28import sys
29import terminal
30
31
32def CountCommitsToBranch():
33 """Returns number of commits between HEAD and the tracking branch.
34
35 This looks back to the tracking branch and works out the number of commits
36 since then.
37
38 Return:
39 Number of patches that exist on top of the branch
40 """
Albert ARIBAUD7f14f302012-10-15 09:55:50 +000041 pipe = [['git', 'log', '--no-color', '--oneline', '@{upstream}..'],
Simon Glass0d24de92012-01-14 15:12:45 +000042 ['wc', '-l']]
43 stdout = command.RunPipe(pipe, capture=True, oneline=True)
44 patch_count = int(stdout)
45 return patch_count
46
47def CreatePatches(start, count, series):
48 """Create a series of patches from the top of the current branch.
49
50 The patch files are written to the current directory using
51 git format-patch.
52
53 Args:
54 start: Commit to start from: 0=HEAD, 1=next one, etc.
55 count: number of commits to include
56 Return:
57 Filename of cover letter
58 List of filenames of patch files
59 """
60 if series.get('version'):
61 version = '%s ' % series['version']
62 cmd = ['git', 'format-patch', '-M', '--signoff']
63 if series.get('cover'):
64 cmd.append('--cover-letter')
65 prefix = series.GetPatchPrefix()
66 if prefix:
67 cmd += ['--subject-prefix=%s' % prefix]
68 cmd += ['HEAD~%d..HEAD~%d' % (start + count, start)]
69
70 stdout = command.RunList(cmd)
71 files = stdout.splitlines()
72
73 # We have an extra file if there is a cover letter
74 if series.get('cover'):
75 return files[0], files[1:]
76 else:
77 return None, files
78
79def ApplyPatch(verbose, fname):
80 """Apply a patch with git am to test it
81
82 TODO: Convert these to use command, with stderr option
83
84 Args:
85 fname: filename of patch file to apply
86 """
87 cmd = ['git', 'am', fname]
88 pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE,
89 stderr=subprocess.PIPE)
90 stdout, stderr = pipe.communicate()
91 re_error = re.compile('^error: patch failed: (.+):(\d+)')
92 for line in stderr.splitlines():
93 if verbose:
94 print line
95 match = re_error.match(line)
96 if match:
97 print GetWarningMsg('warning', match.group(1), int(match.group(2)),
98 'Patch failed')
99 return pipe.returncode == 0, stdout
100
101def ApplyPatches(verbose, args, start_point):
102 """Apply the patches with git am to make sure all is well
103
104 Args:
105 verbose: Print out 'git am' output verbatim
106 args: List of patch files to apply
107 start_point: Number of commits back from HEAD to start applying.
108 Normally this is len(args), but it can be larger if a start
109 offset was given.
110 """
111 error_count = 0
112 col = terminal.Color()
113
114 # Figure out our current position
115 cmd = ['git', 'name-rev', 'HEAD', '--name-only']
116 pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE)
117 stdout, stderr = pipe.communicate()
118 if pipe.returncode:
119 str = 'Could not find current commit name'
120 print col.Color(col.RED, str)
121 print stdout
122 return False
123 old_head = stdout.splitlines()[0]
124
125 # Checkout the required start point
126 cmd = ['git', 'checkout', 'HEAD~%d' % start_point]
127 pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE,
128 stderr=subprocess.PIPE)
129 stdout, stderr = pipe.communicate()
130 if pipe.returncode:
131 str = 'Could not move to commit before patch series'
132 print col.Color(col.RED, str)
133 print stdout, stderr
134 return False
135
136 # Apply all the patches
137 for fname in args:
138 ok, stdout = ApplyPatch(verbose, fname)
139 if not ok:
140 print col.Color(col.RED, 'git am returned errors for %s: will '
141 'skip this patch' % fname)
142 if verbose:
143 print stdout
144 error_count += 1
145 cmd = ['git', 'am', '--skip']
146 pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE)
147 stdout, stderr = pipe.communicate()
148 if pipe.returncode != 0:
149 print col.Color(col.RED, 'Unable to skip patch! Aborting...')
150 print stdout
151 break
152
153 # Return to our previous position
154 cmd = ['git', 'checkout', old_head]
155 pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
156 stdout, stderr = pipe.communicate()
157 if pipe.returncode:
158 print col.Color(col.RED, 'Could not move back to head commit')
159 print stdout, stderr
160 return error_count == 0
161
162def BuildEmailList(in_list, tag=None, alias=None):
163 """Build a list of email addresses based on an input list.
164
165 Takes a list of email addresses and aliases, and turns this into a list
166 of only email address, by resolving any aliases that are present.
167
168 If the tag is given, then each email address is prepended with this
169 tag and a space. If the tag starts with a minus sign (indicating a
170 command line parameter) then the email address is quoted.
171
172 Args:
173 in_list: List of aliases/email addresses
174 tag: Text to put before each address
175
176 Returns:
177 List of email addresses
178
179 >>> alias = {}
180 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
181 >>> alias['john'] = ['j.bloggs@napier.co.nz']
182 >>> alias['mary'] = ['Mary Poppins <m.poppins@cloud.net>']
183 >>> alias['boys'] = ['fred', ' john']
184 >>> alias['all'] = ['fred ', 'john', ' mary ']
185 >>> BuildEmailList(['john', 'mary'], None, alias)
186 ['j.bloggs@napier.co.nz', 'Mary Poppins <m.poppins@cloud.net>']
187 >>> BuildEmailList(['john', 'mary'], '--to', alias)
188 ['--to "j.bloggs@napier.co.nz"', \
189'--to "Mary Poppins <m.poppins@cloud.net>"']
190 >>> BuildEmailList(['john', 'mary'], 'Cc', alias)
191 ['Cc j.bloggs@napier.co.nz', 'Cc Mary Poppins <m.poppins@cloud.net>']
192 """
193 quote = '"' if tag and tag[0] == '-' else ''
194 raw = []
195 for item in in_list:
196 raw += LookupEmail(item, alias)
197 result = []
198 for item in raw:
199 if not item in result:
200 result.append(item)
201 if tag:
202 return ['%s %s%s%s' % (tag, quote, email, quote) for email in result]
203 return result
204
205def EmailPatches(series, cover_fname, args, dry_run, cc_fname,
206 self_only=False, alias=None):
207 """Email a patch series.
208
209 Args:
210 series: Series object containing destination info
211 cover_fname: filename of cover letter
212 args: list of filenames of patch files
213 dry_run: Just return the command that would be run
214 cc_fname: Filename of Cc file for per-commit Cc
215 self_only: True to just email to yourself as a test
216
217 Returns:
218 Git command that was/would be run
219
Doug Andersona9700482012-11-26 15:21:40 +0000220 # For the duration of this doctest pretend that we ran patman with ./patman
221 >>> _old_argv0 = sys.argv[0]
222 >>> sys.argv[0] = './patman'
223
Simon Glass0d24de92012-01-14 15:12:45 +0000224 >>> alias = {}
225 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
226 >>> alias['john'] = ['j.bloggs@napier.co.nz']
227 >>> alias['mary'] = ['m.poppins@cloud.net']
228 >>> alias['boys'] = ['fred', ' john']
229 >>> alias['all'] = ['fred ', 'john', ' mary ']
230 >>> alias[os.getenv('USER')] = ['this-is-me@me.com']
231 >>> series = series.Series()
232 >>> series.to = ['fred']
233 >>> series.cc = ['mary']
234 >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, 'cc-fname', False, \
235 alias)
236 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
237"m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2'
238 >>> EmailPatches(series, None, ['p1'], True, 'cc-fname', False, alias)
239 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
240"m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" p1'
241 >>> series.cc = ['all']
242 >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, 'cc-fname', True, \
243 alias)
244 'git send-email --annotate --to "this-is-me@me.com" --cc-cmd "./patman \
245--cc-cmd cc-fname" cover p1 p2'
246 >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, 'cc-fname', False, \
247 alias)
248 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
249"f.bloggs@napier.co.nz" --cc "j.bloggs@napier.co.nz" --cc \
250"m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2'
Doug Andersona9700482012-11-26 15:21:40 +0000251
252 # Restore argv[0] since we clobbered it.
253 >>> sys.argv[0] = _old_argv0
Simon Glass0d24de92012-01-14 15:12:45 +0000254 """
255 to = BuildEmailList(series.get('to'), '--to', alias)
256 if not to:
257 print ("No recipient, please add something like this to a commit\n"
258 "Series-to: Fred Bloggs <f.blogs@napier.co.nz>")
259 return
260 cc = BuildEmailList(series.get('cc'), '--cc', alias)
261 if self_only:
262 to = BuildEmailList([os.getenv('USER')], '--to', alias)
263 cc = []
264 cmd = ['git', 'send-email', '--annotate']
265 cmd += to
266 cmd += cc
267 cmd += ['--cc-cmd', '"%s --cc-cmd %s"' % (sys.argv[0], cc_fname)]
268 if cover_fname:
269 cmd.append(cover_fname)
270 cmd += args
271 str = ' '.join(cmd)
272 if not dry_run:
273 os.system(str)
274 return str
275
276
277def LookupEmail(lookup_name, alias=None, level=0):
278 """If an email address is an alias, look it up and return the full name
279
280 TODO: Why not just use git's own alias feature?
281
282 Args:
283 lookup_name: Alias or email address to look up
284
285 Returns:
286 tuple:
287 list containing a list of email addresses
288
289 Raises:
290 OSError if a recursive alias reference was found
291 ValueError if an alias was not found
292
293 >>> alias = {}
294 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
295 >>> alias['john'] = ['j.bloggs@napier.co.nz']
296 >>> alias['mary'] = ['m.poppins@cloud.net']
297 >>> alias['boys'] = ['fred', ' john', 'f.bloggs@napier.co.nz']
298 >>> alias['all'] = ['fred ', 'john', ' mary ']
299 >>> alias['loop'] = ['other', 'john', ' mary ']
300 >>> alias['other'] = ['loop', 'john', ' mary ']
301 >>> LookupEmail('mary', alias)
302 ['m.poppins@cloud.net']
303 >>> LookupEmail('arthur.wellesley@howe.ro.uk', alias)
304 ['arthur.wellesley@howe.ro.uk']
305 >>> LookupEmail('boys', alias)
306 ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz']
307 >>> LookupEmail('all', alias)
308 ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
309 >>> LookupEmail('odd', alias)
310 Traceback (most recent call last):
311 ...
312 ValueError: Alias 'odd' not found
313 >>> LookupEmail('loop', alias)
314 Traceback (most recent call last):
315 ...
316 OSError: Recursive email alias at 'other'
317 """
318 if not alias:
319 alias = settings.alias
320 lookup_name = lookup_name.strip()
321 if '@' in lookup_name: # Perhaps a real email address
322 return [lookup_name]
323
324 lookup_name = lookup_name.lower()
325
326 if level > 10:
327 raise OSError, "Recursive email alias at '%s'" % lookup_name
328
329 out_list = []
330 if lookup_name:
331 if not lookup_name in alias:
332 raise ValueError, "Alias '%s' not found" % lookup_name
333 for item in alias[lookup_name]:
334 todo = LookupEmail(item, alias, level + 1)
335 for new_item in todo:
336 if not new_item in out_list:
337 out_list.append(new_item)
338
339 #print "No match for alias '%s'" % lookup_name
340 return out_list
341
342def GetTopLevel():
343 """Return name of top-level directory for this git repo.
344
345 Returns:
346 Full path to git top-level directory
347
348 This test makes sure that we are running tests in the right subdir
349
Doug Andersona9700482012-11-26 15:21:40 +0000350 >>> os.path.realpath(os.path.dirname(__file__)) == \
351 os.path.join(GetTopLevel(), 'tools', 'patman')
Simon Glass0d24de92012-01-14 15:12:45 +0000352 True
353 """
354 return command.OutputOneLine('git', 'rev-parse', '--show-toplevel')
355
356def GetAliasFile():
357 """Gets the name of the git alias file.
358
359 Returns:
360 Filename of git alias file, or None if none
361 """
362 fname = command.OutputOneLine('git', 'config', 'sendemail.aliasesfile')
363 if fname:
364 fname = os.path.join(GetTopLevel(), fname.strip())
365 return fname
366
Vikram Narayanan87d65552012-05-23 09:01:06 +0000367def GetDefaultUserName():
368 """Gets the user.name from .gitconfig file.
369
370 Returns:
371 User name found in .gitconfig file, or None if none
372 """
373 uname = command.OutputOneLine('git', 'config', '--global', 'user.name')
374 return uname
375
376def GetDefaultUserEmail():
377 """Gets the user.email from the global .gitconfig file.
378
379 Returns:
380 User's email found in .gitconfig file, or None if none
381 """
382 uemail = command.OutputOneLine('git', 'config', '--global', 'user.email')
383 return uemail
384
Simon Glass0d24de92012-01-14 15:12:45 +0000385def Setup():
386 """Set up git utils, by reading the alias files."""
Simon Glass0d24de92012-01-14 15:12:45 +0000387 # Check for a git alias file also
388 alias_fname = GetAliasFile()
389 if alias_fname:
390 settings.ReadGitAliases(alias_fname)
391
392if __name__ == "__main__":
393 import doctest
394
395 doctest.testmod()