1 # Copyright (c) 2011 The Chromium OS Authors.
3 # See file CREDITS for list of people who contributed to this
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.
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.
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,
32 def CountCommitsToBranch():
33 """Returns number of commits between HEAD and the tracking branch.
35 This looks back to the tracking branch and works out the number of commits
39 Number of patches that exist on top of the branch
41 pipe = [['git', 'log', '--no-color', '--oneline', '@{upstream}..'],
43 stdout = command.RunPipe(pipe, capture=True, oneline=True)
44 patch_count = int(stdout)
47 def CreatePatches(start, count, series):
48 """Create a series of patches from the top of the current branch.
50 The patch files are written to the current directory using
54 start: Commit to start from: 0=HEAD, 1=next one, etc.
55 count: number of commits to include
57 Filename of cover letter
58 List of filenames of patch files
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()
67 cmd += ['--subject-prefix=%s' % prefix]
68 cmd += ['HEAD~%d..HEAD~%d' % (start + count, start)]
70 stdout = command.RunList(cmd)
71 files = stdout.splitlines()
73 # We have an extra file if there is a cover letter
74 if series.get('cover'):
75 return files[0], files[1:]
79 def ApplyPatch(verbose, fname):
80 """Apply a patch with git am to test it
82 TODO: Convert these to use command, with stderr option
85 fname: filename of patch file to apply
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():
95 match = re_error.match(line)
97 print GetWarningMsg('warning', match.group(1), int(match.group(2)),
99 return pipe.returncode == 0, stdout
101 def ApplyPatches(verbose, args, start_point):
102 """Apply the patches with git am to make sure all is well
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
112 col = terminal.Color()
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()
119 str = 'Could not find current commit name'
120 print col.Color(col.RED, str)
123 old_head = stdout.splitlines()[0]
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()
131 str = 'Could not move to commit before patch series'
132 print col.Color(col.RED, str)
136 # Apply all the patches
138 ok, stdout = ApplyPatch(verbose, fname)
140 print col.Color(col.RED, 'git am returned errors for %s: will '
141 'skip this patch' % fname)
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...')
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()
158 print col.Color(col.RED, 'Could not move back to head commit')
160 return error_count == 0
162 def BuildEmailList(in_list, tag=None, alias=None):
163 """Build a list of email addresses based on an input list.
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.
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.
173 in_list: List of aliases/email addresses
174 tag: Text to put before each address
177 List of email addresses
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>']
193 quote = '"' if tag and tag[0] == '-' else ''
196 raw += LookupEmail(item, alias)
199 if not item in result:
202 return ['%s %s%s%s' % (tag, quote, email, quote) for email in result]
205 def EmailPatches(series, cover_fname, args, dry_run, cc_fname,
206 self_only=False, alias=None):
207 """Email a patch series.
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
218 Git command that was/would be run
221 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
222 >>> alias['john'] = ['j.bloggs@napier.co.nz']
223 >>> alias['mary'] = ['m.poppins@cloud.net']
224 >>> alias['boys'] = ['fred', ' john']
225 >>> alias['all'] = ['fred ', 'john', ' mary ']
226 >>> alias[os.getenv('USER')] = ['this-is-me@me.com']
227 >>> series = series.Series()
228 >>> series.to = ['fred']
229 >>> series.cc = ['mary']
230 >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, 'cc-fname', False, \
232 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
233 "m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2'
234 >>> EmailPatches(series, None, ['p1'], True, 'cc-fname', False, alias)
235 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
236 "m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" p1'
237 >>> series.cc = ['all']
238 >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, 'cc-fname', True, \
240 'git send-email --annotate --to "this-is-me@me.com" --cc-cmd "./patman \
241 --cc-cmd cc-fname" cover p1 p2'
242 >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, 'cc-fname', False, \
244 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
245 "f.bloggs@napier.co.nz" --cc "j.bloggs@napier.co.nz" --cc \
246 "m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2'
248 to = BuildEmailList(series.get('to'), '--to', alias)
250 print ("No recipient, please add something like this to a commit\n"
251 "Series-to: Fred Bloggs <f.blogs@napier.co.nz>")
253 cc = BuildEmailList(series.get('cc'), '--cc', alias)
255 to = BuildEmailList([os.getenv('USER')], '--to', alias)
257 cmd = ['git', 'send-email', '--annotate']
260 cmd += ['--cc-cmd', '"%s --cc-cmd %s"' % (sys.argv[0], cc_fname)]
262 cmd.append(cover_fname)
270 def LookupEmail(lookup_name, alias=None, level=0):
271 """If an email address is an alias, look it up and return the full name
273 TODO: Why not just use git's own alias feature?
276 lookup_name: Alias or email address to look up
280 list containing a list of email addresses
283 OSError if a recursive alias reference was found
284 ValueError if an alias was not found
287 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
288 >>> alias['john'] = ['j.bloggs@napier.co.nz']
289 >>> alias['mary'] = ['m.poppins@cloud.net']
290 >>> alias['boys'] = ['fred', ' john', 'f.bloggs@napier.co.nz']
291 >>> alias['all'] = ['fred ', 'john', ' mary ']
292 >>> alias['loop'] = ['other', 'john', ' mary ']
293 >>> alias['other'] = ['loop', 'john', ' mary ']
294 >>> LookupEmail('mary', alias)
295 ['m.poppins@cloud.net']
296 >>> LookupEmail('arthur.wellesley@howe.ro.uk', alias)
297 ['arthur.wellesley@howe.ro.uk']
298 >>> LookupEmail('boys', alias)
299 ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz']
300 >>> LookupEmail('all', alias)
301 ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
302 >>> LookupEmail('odd', alias)
303 Traceback (most recent call last):
305 ValueError: Alias 'odd' not found
306 >>> LookupEmail('loop', alias)
307 Traceback (most recent call last):
309 OSError: Recursive email alias at 'other'
312 alias = settings.alias
313 lookup_name = lookup_name.strip()
314 if '@' in lookup_name: # Perhaps a real email address
317 lookup_name = lookup_name.lower()
320 raise OSError, "Recursive email alias at '%s'" % lookup_name
324 if not lookup_name in alias:
325 raise ValueError, "Alias '%s' not found" % lookup_name
326 for item in alias[lookup_name]:
327 todo = LookupEmail(item, alias, level + 1)
328 for new_item in todo:
329 if not new_item in out_list:
330 out_list.append(new_item)
332 #print "No match for alias '%s'" % lookup_name
336 """Return name of top-level directory for this git repo.
339 Full path to git top-level directory
341 This test makes sure that we are running tests in the right subdir
343 >>> os.path.realpath(os.getcwd()) == \
344 os.path.join(GetTopLevel(), 'tools', 'scripts', 'patman')
347 return command.OutputOneLine('git', 'rev-parse', '--show-toplevel')
350 """Gets the name of the git alias file.
353 Filename of git alias file, or None if none
355 fname = command.OutputOneLine('git', 'config', 'sendemail.aliasesfile')
357 fname = os.path.join(GetTopLevel(), fname.strip())
360 def GetDefaultUserName():
361 """Gets the user.name from .gitconfig file.
364 User name found in .gitconfig file, or None if none
366 uname = command.OutputOneLine('git', 'config', '--global', 'user.name')
369 def GetDefaultUserEmail():
370 """Gets the user.email from the global .gitconfig file.
373 User's email found in .gitconfig file, or None if none
375 uemail = command.OutputOneLine('git', 'config', '--global', 'user.email')
379 """Set up git utils, by reading the alias files."""
382 # Check for a git alias file also
383 alias_fname = GetAliasFile()
385 settings.ReadGitAliases(alias_fname)
387 if __name__ == "__main__":