Commit 757f64a89ba5bb04661b3f43444ca57fa6db1132

Authored by Simon Glass
1 parent 75b3c3aa84

patman: Deal with 'git apply' failures correctly

This sort of failure is rare, but the code to deal with it is wrong.
Fix it.

Signed-off-by: Simon Glass <sjg@chromium.org>

Showing 1 changed file with 4 additions and 2 deletions Inline Diff

tools/patman/gitutil.py
1 # Copyright (c) 2011 The Chromium OS Authors. 1 # Copyright (c) 2011 The Chromium OS Authors.
2 # 2 #
3 # SPDX-License-Identifier: GPL-2.0+ 3 # SPDX-License-Identifier: GPL-2.0+
4 # 4 #
5 5
6 import command 6 import command
7 import re 7 import re
8 import os 8 import os
9 import series 9 import series
10 import subprocess 10 import subprocess
11 import sys 11 import sys
12 import terminal 12 import terminal
13 13
14 import checkpatch
14 import settings 15 import settings
15 16
16 17
17 def CountCommitsToBranch(): 18 def CountCommitsToBranch():
18 """Returns number of commits between HEAD and the tracking branch. 19 """Returns number of commits between HEAD and the tracking branch.
19 20
20 This looks back to the tracking branch and works out the number of commits 21 This looks back to the tracking branch and works out the number of commits
21 since then. 22 since then.
22 23
23 Return: 24 Return:
24 Number of patches that exist on top of the branch 25 Number of patches that exist on top of the branch
25 """ 26 """
26 pipe = [['git', 'log', '--no-color', '--oneline', '--no-decorate', 27 pipe = [['git', 'log', '--no-color', '--oneline', '--no-decorate',
27 '@{upstream}..'], 28 '@{upstream}..'],
28 ['wc', '-l']] 29 ['wc', '-l']]
29 stdout = command.RunPipe(pipe, capture=True, oneline=True).stdout 30 stdout = command.RunPipe(pipe, capture=True, oneline=True).stdout
30 patch_count = int(stdout) 31 patch_count = int(stdout)
31 return patch_count 32 return patch_count
32 33
33 def GetUpstream(git_dir, branch): 34 def GetUpstream(git_dir, branch):
34 """Returns the name of the upstream for a branch 35 """Returns the name of the upstream for a branch
35 36
36 Args: 37 Args:
37 git_dir: Git directory containing repo 38 git_dir: Git directory containing repo
38 branch: Name of branch 39 branch: Name of branch
39 40
40 Returns: 41 Returns:
41 Name of upstream branch (e.g. 'upstream/master') or None if none 42 Name of upstream branch (e.g. 'upstream/master') or None if none
42 """ 43 """
43 try: 44 try:
44 remote = command.OutputOneLine('git', '--git-dir', git_dir, 'config', 45 remote = command.OutputOneLine('git', '--git-dir', git_dir, 'config',
45 'branch.%s.remote' % branch) 46 'branch.%s.remote' % branch)
46 merge = command.OutputOneLine('git', '--git-dir', git_dir, 'config', 47 merge = command.OutputOneLine('git', '--git-dir', git_dir, 'config',
47 'branch.%s.merge' % branch) 48 'branch.%s.merge' % branch)
48 except: 49 except:
49 return None 50 return None
50 51
51 if remote == '.': 52 if remote == '.':
52 return merge 53 return merge
53 elif remote and merge: 54 elif remote and merge:
54 leaf = merge.split('/')[-1] 55 leaf = merge.split('/')[-1]
55 return '%s/%s' % (remote, leaf) 56 return '%s/%s' % (remote, leaf)
56 else: 57 else:
57 raise ValueError, ("Cannot determine upstream branch for branch " 58 raise ValueError, ("Cannot determine upstream branch for branch "
58 "'%s' remote='%s', merge='%s'" % (branch, remote, merge)) 59 "'%s' remote='%s', merge='%s'" % (branch, remote, merge))
59 60
60 61
61 def GetRangeInBranch(git_dir, branch, include_upstream=False): 62 def GetRangeInBranch(git_dir, branch, include_upstream=False):
62 """Returns an expression for the commits in the given branch. 63 """Returns an expression for the commits in the given branch.
63 64
64 Args: 65 Args:
65 git_dir: Directory containing git repo 66 git_dir: Directory containing git repo
66 branch: Name of branch 67 branch: Name of branch
67 Return: 68 Return:
68 Expression in the form 'upstream..branch' which can be used to 69 Expression in the form 'upstream..branch' which can be used to
69 access the commits. If the branch does not exist, returns None. 70 access the commits. If the branch does not exist, returns None.
70 """ 71 """
71 upstream = GetUpstream(git_dir, branch) 72 upstream = GetUpstream(git_dir, branch)
72 if not upstream: 73 if not upstream:
73 return None 74 return None
74 return '%s%s..%s' % (upstream, '~' if include_upstream else '', branch) 75 return '%s%s..%s' % (upstream, '~' if include_upstream else '', branch)
75 76
76 def CountCommitsInBranch(git_dir, branch, include_upstream=False): 77 def CountCommitsInBranch(git_dir, branch, include_upstream=False):
77 """Returns the number of commits in the given branch. 78 """Returns the number of commits in the given branch.
78 79
79 Args: 80 Args:
80 git_dir: Directory containing git repo 81 git_dir: Directory containing git repo
81 branch: Name of branch 82 branch: Name of branch
82 Return: 83 Return:
83 Number of patches that exist on top of the branch, or None if the 84 Number of patches that exist on top of the branch, or None if the
84 branch does not exist. 85 branch does not exist.
85 """ 86 """
86 range_expr = GetRangeInBranch(git_dir, branch, include_upstream) 87 range_expr = GetRangeInBranch(git_dir, branch, include_upstream)
87 if not range_expr: 88 if not range_expr:
88 return None 89 return None
89 pipe = [['git', '--git-dir', git_dir, 'log', '--oneline', '--no-decorate', 90 pipe = [['git', '--git-dir', git_dir, 'log', '--oneline', '--no-decorate',
90 range_expr], 91 range_expr],
91 ['wc', '-l']] 92 ['wc', '-l']]
92 result = command.RunPipe(pipe, capture=True, oneline=True) 93 result = command.RunPipe(pipe, capture=True, oneline=True)
93 patch_count = int(result.stdout) 94 patch_count = int(result.stdout)
94 return patch_count 95 return patch_count
95 96
96 def CountCommits(commit_range): 97 def CountCommits(commit_range):
97 """Returns the number of commits in the given range. 98 """Returns the number of commits in the given range.
98 99
99 Args: 100 Args:
100 commit_range: Range of commits to count (e.g. 'HEAD..base') 101 commit_range: Range of commits to count (e.g. 'HEAD..base')
101 Return: 102 Return:
102 Number of patches that exist on top of the branch 103 Number of patches that exist on top of the branch
103 """ 104 """
104 pipe = [['git', 'log', '--oneline', '--no-decorate', commit_range], 105 pipe = [['git', 'log', '--oneline', '--no-decorate', commit_range],
105 ['wc', '-l']] 106 ['wc', '-l']]
106 stdout = command.RunPipe(pipe, capture=True, oneline=True).stdout 107 stdout = command.RunPipe(pipe, capture=True, oneline=True).stdout
107 patch_count = int(stdout) 108 patch_count = int(stdout)
108 return patch_count 109 return patch_count
109 110
110 def Checkout(commit_hash, git_dir=None, work_tree=None, force=False): 111 def Checkout(commit_hash, git_dir=None, work_tree=None, force=False):
111 """Checkout the selected commit for this build 112 """Checkout the selected commit for this build
112 113
113 Args: 114 Args:
114 commit_hash: Commit hash to check out 115 commit_hash: Commit hash to check out
115 """ 116 """
116 pipe = ['git'] 117 pipe = ['git']
117 if git_dir: 118 if git_dir:
118 pipe.extend(['--git-dir', git_dir]) 119 pipe.extend(['--git-dir', git_dir])
119 if work_tree: 120 if work_tree:
120 pipe.extend(['--work-tree', work_tree]) 121 pipe.extend(['--work-tree', work_tree])
121 pipe.append('checkout') 122 pipe.append('checkout')
122 if force: 123 if force:
123 pipe.append('-f') 124 pipe.append('-f')
124 pipe.append(commit_hash) 125 pipe.append(commit_hash)
125 result = command.RunPipe([pipe], capture=True, raise_on_error=False) 126 result = command.RunPipe([pipe], capture=True, raise_on_error=False)
126 if result.return_code != 0: 127 if result.return_code != 0:
127 raise OSError, 'git checkout (%s): %s' % (pipe, result.stderr) 128 raise OSError, 'git checkout (%s): %s' % (pipe, result.stderr)
128 129
129 def Clone(git_dir, output_dir): 130 def Clone(git_dir, output_dir):
130 """Checkout the selected commit for this build 131 """Checkout the selected commit for this build
131 132
132 Args: 133 Args:
133 commit_hash: Commit hash to check out 134 commit_hash: Commit hash to check out
134 """ 135 """
135 pipe = ['git', 'clone', git_dir, '.'] 136 pipe = ['git', 'clone', git_dir, '.']
136 result = command.RunPipe([pipe], capture=True, cwd=output_dir) 137 result = command.RunPipe([pipe], capture=True, cwd=output_dir)
137 if result.return_code != 0: 138 if result.return_code != 0:
138 raise OSError, 'git clone: %s' % result.stderr 139 raise OSError, 'git clone: %s' % result.stderr
139 140
140 def Fetch(git_dir=None, work_tree=None): 141 def Fetch(git_dir=None, work_tree=None):
141 """Fetch from the origin repo 142 """Fetch from the origin repo
142 143
143 Args: 144 Args:
144 commit_hash: Commit hash to check out 145 commit_hash: Commit hash to check out
145 """ 146 """
146 pipe = ['git'] 147 pipe = ['git']
147 if git_dir: 148 if git_dir:
148 pipe.extend(['--git-dir', git_dir]) 149 pipe.extend(['--git-dir', git_dir])
149 if work_tree: 150 if work_tree:
150 pipe.extend(['--work-tree', work_tree]) 151 pipe.extend(['--work-tree', work_tree])
151 pipe.append('fetch') 152 pipe.append('fetch')
152 result = command.RunPipe([pipe], capture=True) 153 result = command.RunPipe([pipe], capture=True)
153 if result.return_code != 0: 154 if result.return_code != 0:
154 raise OSError, 'git fetch: %s' % result.stderr 155 raise OSError, 'git fetch: %s' % result.stderr
155 156
156 def CreatePatches(start, count, series): 157 def CreatePatches(start, count, series):
157 """Create a series of patches from the top of the current branch. 158 """Create a series of patches from the top of the current branch.
158 159
159 The patch files are written to the current directory using 160 The patch files are written to the current directory using
160 git format-patch. 161 git format-patch.
161 162
162 Args: 163 Args:
163 start: Commit to start from: 0=HEAD, 1=next one, etc. 164 start: Commit to start from: 0=HEAD, 1=next one, etc.
164 count: number of commits to include 165 count: number of commits to include
165 Return: 166 Return:
166 Filename of cover letter 167 Filename of cover letter
167 List of filenames of patch files 168 List of filenames of patch files
168 """ 169 """
169 if series.get('version'): 170 if series.get('version'):
170 version = '%s ' % series['version'] 171 version = '%s ' % series['version']
171 cmd = ['git', 'format-patch', '-M', '--signoff'] 172 cmd = ['git', 'format-patch', '-M', '--signoff']
172 if series.get('cover'): 173 if series.get('cover'):
173 cmd.append('--cover-letter') 174 cmd.append('--cover-letter')
174 prefix = series.GetPatchPrefix() 175 prefix = series.GetPatchPrefix()
175 if prefix: 176 if prefix:
176 cmd += ['--subject-prefix=%s' % prefix] 177 cmd += ['--subject-prefix=%s' % prefix]
177 cmd += ['HEAD~%d..HEAD~%d' % (start + count, start)] 178 cmd += ['HEAD~%d..HEAD~%d' % (start + count, start)]
178 179
179 stdout = command.RunList(cmd) 180 stdout = command.RunList(cmd)
180 files = stdout.splitlines() 181 files = stdout.splitlines()
181 182
182 # We have an extra file if there is a cover letter 183 # We have an extra file if there is a cover letter
183 if series.get('cover'): 184 if series.get('cover'):
184 return files[0], files[1:] 185 return files[0], files[1:]
185 else: 186 else:
186 return None, files 187 return None, files
187 188
188 def ApplyPatch(verbose, fname): 189 def ApplyPatch(verbose, fname):
189 """Apply a patch with git am to test it 190 """Apply a patch with git am to test it
190 191
191 TODO: Convert these to use command, with stderr option 192 TODO: Convert these to use command, with stderr option
192 193
193 Args: 194 Args:
194 fname: filename of patch file to apply 195 fname: filename of patch file to apply
195 """ 196 """
197 col = terminal.Color()
196 cmd = ['git', 'am', fname] 198 cmd = ['git', 'am', fname]
197 pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE, 199 pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE,
198 stderr=subprocess.PIPE) 200 stderr=subprocess.PIPE)
199 stdout, stderr = pipe.communicate() 201 stdout, stderr = pipe.communicate()
200 re_error = re.compile('^error: patch failed: (.+):(\d+)') 202 re_error = re.compile('^error: patch failed: (.+):(\d+)')
201 for line in stderr.splitlines(): 203 for line in stderr.splitlines():
202 if verbose: 204 if verbose:
203 print line 205 print line
204 match = re_error.match(line) 206 match = re_error.match(line)
205 if match: 207 if match:
206 print GetWarningMsg('warning', match.group(1), int(match.group(2)), 208 print checkpatch.GetWarningMsg(col, 'warning', match.group(1),
207 'Patch failed') 209 int(match.group(2)), 'Patch failed')
208 return pipe.returncode == 0, stdout 210 return pipe.returncode == 0, stdout
209 211
210 def ApplyPatches(verbose, args, start_point): 212 def ApplyPatches(verbose, args, start_point):
211 """Apply the patches with git am to make sure all is well 213 """Apply the patches with git am to make sure all is well
212 214
213 Args: 215 Args:
214 verbose: Print out 'git am' output verbatim 216 verbose: Print out 'git am' output verbatim
215 args: List of patch files to apply 217 args: List of patch files to apply
216 start_point: Number of commits back from HEAD to start applying. 218 start_point: Number of commits back from HEAD to start applying.
217 Normally this is len(args), but it can be larger if a start 219 Normally this is len(args), but it can be larger if a start
218 offset was given. 220 offset was given.
219 """ 221 """
220 error_count = 0 222 error_count = 0
221 col = terminal.Color() 223 col = terminal.Color()
222 224
223 # Figure out our current position 225 # Figure out our current position
224 cmd = ['git', 'name-rev', 'HEAD', '--name-only'] 226 cmd = ['git', 'name-rev', 'HEAD', '--name-only']
225 pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE) 227 pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE)
226 stdout, stderr = pipe.communicate() 228 stdout, stderr = pipe.communicate()
227 if pipe.returncode: 229 if pipe.returncode:
228 str = 'Could not find current commit name' 230 str = 'Could not find current commit name'
229 print col.Color(col.RED, str) 231 print col.Color(col.RED, str)
230 print stdout 232 print stdout
231 return False 233 return False
232 old_head = stdout.splitlines()[0] 234 old_head = stdout.splitlines()[0]
233 235
234 # Checkout the required start point 236 # Checkout the required start point
235 cmd = ['git', 'checkout', 'HEAD~%d' % start_point] 237 cmd = ['git', 'checkout', 'HEAD~%d' % start_point]
236 pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE, 238 pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE,
237 stderr=subprocess.PIPE) 239 stderr=subprocess.PIPE)
238 stdout, stderr = pipe.communicate() 240 stdout, stderr = pipe.communicate()
239 if pipe.returncode: 241 if pipe.returncode:
240 str = 'Could not move to commit before patch series' 242 str = 'Could not move to commit before patch series'
241 print col.Color(col.RED, str) 243 print col.Color(col.RED, str)
242 print stdout, stderr 244 print stdout, stderr
243 return False 245 return False
244 246
245 # Apply all the patches 247 # Apply all the patches
246 for fname in args: 248 for fname in args:
247 ok, stdout = ApplyPatch(verbose, fname) 249 ok, stdout = ApplyPatch(verbose, fname)
248 if not ok: 250 if not ok:
249 print col.Color(col.RED, 'git am returned errors for %s: will ' 251 print col.Color(col.RED, 'git am returned errors for %s: will '
250 'skip this patch' % fname) 252 'skip this patch' % fname)
251 if verbose: 253 if verbose:
252 print stdout 254 print stdout
253 error_count += 1 255 error_count += 1
254 cmd = ['git', 'am', '--skip'] 256 cmd = ['git', 'am', '--skip']
255 pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE) 257 pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE)
256 stdout, stderr = pipe.communicate() 258 stdout, stderr = pipe.communicate()
257 if pipe.returncode != 0: 259 if pipe.returncode != 0:
258 print col.Color(col.RED, 'Unable to skip patch! Aborting...') 260 print col.Color(col.RED, 'Unable to skip patch! Aborting...')
259 print stdout 261 print stdout
260 break 262 break
261 263
262 # Return to our previous position 264 # Return to our previous position
263 cmd = ['git', 'checkout', old_head] 265 cmd = ['git', 'checkout', old_head]
264 pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 266 pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
265 stdout, stderr = pipe.communicate() 267 stdout, stderr = pipe.communicate()
266 if pipe.returncode: 268 if pipe.returncode:
267 print col.Color(col.RED, 'Could not move back to head commit') 269 print col.Color(col.RED, 'Could not move back to head commit')
268 print stdout, stderr 270 print stdout, stderr
269 return error_count == 0 271 return error_count == 0
270 272
271 def BuildEmailList(in_list, tag=None, alias=None, raise_on_error=True): 273 def BuildEmailList(in_list, tag=None, alias=None, raise_on_error=True):
272 """Build a list of email addresses based on an input list. 274 """Build a list of email addresses based on an input list.
273 275
274 Takes a list of email addresses and aliases, and turns this into a list 276 Takes a list of email addresses and aliases, and turns this into a list
275 of only email address, by resolving any aliases that are present. 277 of only email address, by resolving any aliases that are present.
276 278
277 If the tag is given, then each email address is prepended with this 279 If the tag is given, then each email address is prepended with this
278 tag and a space. If the tag starts with a minus sign (indicating a 280 tag and a space. If the tag starts with a minus sign (indicating a
279 command line parameter) then the email address is quoted. 281 command line parameter) then the email address is quoted.
280 282
281 Args: 283 Args:
282 in_list: List of aliases/email addresses 284 in_list: List of aliases/email addresses
283 tag: Text to put before each address 285 tag: Text to put before each address
284 alias: Alias dictionary 286 alias: Alias dictionary
285 raise_on_error: True to raise an error when an alias fails to match, 287 raise_on_error: True to raise an error when an alias fails to match,
286 False to just print a message. 288 False to just print a message.
287 289
288 Returns: 290 Returns:
289 List of email addresses 291 List of email addresses
290 292
291 >>> alias = {} 293 >>> alias = {}
292 >>> alias['fred'] = ['f.bloggs@napier.co.nz'] 294 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
293 >>> alias['john'] = ['j.bloggs@napier.co.nz'] 295 >>> alias['john'] = ['j.bloggs@napier.co.nz']
294 >>> alias['mary'] = ['Mary Poppins <m.poppins@cloud.net>'] 296 >>> alias['mary'] = ['Mary Poppins <m.poppins@cloud.net>']
295 >>> alias['boys'] = ['fred', ' john'] 297 >>> alias['boys'] = ['fred', ' john']
296 >>> alias['all'] = ['fred ', 'john', ' mary '] 298 >>> alias['all'] = ['fred ', 'john', ' mary ']
297 >>> BuildEmailList(['john', 'mary'], None, alias) 299 >>> BuildEmailList(['john', 'mary'], None, alias)
298 ['j.bloggs@napier.co.nz', 'Mary Poppins <m.poppins@cloud.net>'] 300 ['j.bloggs@napier.co.nz', 'Mary Poppins <m.poppins@cloud.net>']
299 >>> BuildEmailList(['john', 'mary'], '--to', alias) 301 >>> BuildEmailList(['john', 'mary'], '--to', alias)
300 ['--to "j.bloggs@napier.co.nz"', \ 302 ['--to "j.bloggs@napier.co.nz"', \
301 '--to "Mary Poppins <m.poppins@cloud.net>"'] 303 '--to "Mary Poppins <m.poppins@cloud.net>"']
302 >>> BuildEmailList(['john', 'mary'], 'Cc', alias) 304 >>> BuildEmailList(['john', 'mary'], 'Cc', alias)
303 ['Cc j.bloggs@napier.co.nz', 'Cc Mary Poppins <m.poppins@cloud.net>'] 305 ['Cc j.bloggs@napier.co.nz', 'Cc Mary Poppins <m.poppins@cloud.net>']
304 """ 306 """
305 quote = '"' if tag and tag[0] == '-' else '' 307 quote = '"' if tag and tag[0] == '-' else ''
306 raw = [] 308 raw = []
307 for item in in_list: 309 for item in in_list:
308 raw += LookupEmail(item, alias, raise_on_error=raise_on_error) 310 raw += LookupEmail(item, alias, raise_on_error=raise_on_error)
309 result = [] 311 result = []
310 for item in raw: 312 for item in raw:
311 if not item in result: 313 if not item in result:
312 result.append(item) 314 result.append(item)
313 if tag: 315 if tag:
314 return ['%s %s%s%s' % (tag, quote, email, quote) for email in result] 316 return ['%s %s%s%s' % (tag, quote, email, quote) for email in result]
315 return result 317 return result
316 318
317 def EmailPatches(series, cover_fname, args, dry_run, raise_on_error, cc_fname, 319 def EmailPatches(series, cover_fname, args, dry_run, raise_on_error, cc_fname,
318 self_only=False, alias=None, in_reply_to=None): 320 self_only=False, alias=None, in_reply_to=None):
319 """Email a patch series. 321 """Email a patch series.
320 322
321 Args: 323 Args:
322 series: Series object containing destination info 324 series: Series object containing destination info
323 cover_fname: filename of cover letter 325 cover_fname: filename of cover letter
324 args: list of filenames of patch files 326 args: list of filenames of patch files
325 dry_run: Just return the command that would be run 327 dry_run: Just return the command that would be run
326 raise_on_error: True to raise an error when an alias fails to match, 328 raise_on_error: True to raise an error when an alias fails to match,
327 False to just print a message. 329 False to just print a message.
328 cc_fname: Filename of Cc file for per-commit Cc 330 cc_fname: Filename of Cc file for per-commit Cc
329 self_only: True to just email to yourself as a test 331 self_only: True to just email to yourself as a test
330 in_reply_to: If set we'll pass this to git as --in-reply-to. 332 in_reply_to: If set we'll pass this to git as --in-reply-to.
331 Should be a message ID that this is in reply to. 333 Should be a message ID that this is in reply to.
332 334
333 Returns: 335 Returns:
334 Git command that was/would be run 336 Git command that was/would be run
335 337
336 # For the duration of this doctest pretend that we ran patman with ./patman 338 # For the duration of this doctest pretend that we ran patman with ./patman
337 >>> _old_argv0 = sys.argv[0] 339 >>> _old_argv0 = sys.argv[0]
338 >>> sys.argv[0] = './patman' 340 >>> sys.argv[0] = './patman'
339 341
340 >>> alias = {} 342 >>> alias = {}
341 >>> alias['fred'] = ['f.bloggs@napier.co.nz'] 343 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
342 >>> alias['john'] = ['j.bloggs@napier.co.nz'] 344 >>> alias['john'] = ['j.bloggs@napier.co.nz']
343 >>> alias['mary'] = ['m.poppins@cloud.net'] 345 >>> alias['mary'] = ['m.poppins@cloud.net']
344 >>> alias['boys'] = ['fred', ' john'] 346 >>> alias['boys'] = ['fred', ' john']
345 >>> alias['all'] = ['fred ', 'john', ' mary '] 347 >>> alias['all'] = ['fred ', 'john', ' mary ']
346 >>> alias[os.getenv('USER')] = ['this-is-me@me.com'] 348 >>> alias[os.getenv('USER')] = ['this-is-me@me.com']
347 >>> series = series.Series() 349 >>> series = series.Series()
348 >>> series.to = ['fred'] 350 >>> series.to = ['fred']
349 >>> series.cc = ['mary'] 351 >>> series.cc = ['mary']
350 >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \ 352 >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
351 False, alias) 353 False, alias)
352 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \ 354 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
353 "m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2' 355 "m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2'
354 >>> EmailPatches(series, None, ['p1'], True, True, 'cc-fname', False, \ 356 >>> EmailPatches(series, None, ['p1'], True, True, 'cc-fname', False, \
355 alias) 357 alias)
356 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \ 358 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
357 "m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" p1' 359 "m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" p1'
358 >>> series.cc = ['all'] 360 >>> series.cc = ['all']
359 >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \ 361 >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
360 True, alias) 362 True, alias)
361 'git send-email --annotate --to "this-is-me@me.com" --cc-cmd "./patman \ 363 'git send-email --annotate --to "this-is-me@me.com" --cc-cmd "./patman \
362 --cc-cmd cc-fname" cover p1 p2' 364 --cc-cmd cc-fname" cover p1 p2'
363 >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \ 365 >>> EmailPatches(series, 'cover', ['p1', 'p2'], True, True, 'cc-fname', \
364 False, alias) 366 False, alias)
365 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \ 367 'git send-email --annotate --to "f.bloggs@napier.co.nz" --cc \
366 "f.bloggs@napier.co.nz" --cc "j.bloggs@napier.co.nz" --cc \ 368 "f.bloggs@napier.co.nz" --cc "j.bloggs@napier.co.nz" --cc \
367 "m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2' 369 "m.poppins@cloud.net" --cc-cmd "./patman --cc-cmd cc-fname" cover p1 p2'
368 370
369 # Restore argv[0] since we clobbered it. 371 # Restore argv[0] since we clobbered it.
370 >>> sys.argv[0] = _old_argv0 372 >>> sys.argv[0] = _old_argv0
371 """ 373 """
372 to = BuildEmailList(series.get('to'), '--to', alias, raise_on_error) 374 to = BuildEmailList(series.get('to'), '--to', alias, raise_on_error)
373 if not to: 375 if not to:
374 print ("No recipient, please add something like this to a commit\n" 376 print ("No recipient, please add something like this to a commit\n"
375 "Series-to: Fred Bloggs <f.blogs@napier.co.nz>") 377 "Series-to: Fred Bloggs <f.blogs@napier.co.nz>")
376 return 378 return
377 cc = BuildEmailList(series.get('cc'), '--cc', alias, raise_on_error) 379 cc = BuildEmailList(series.get('cc'), '--cc', alias, raise_on_error)
378 if self_only: 380 if self_only:
379 to = BuildEmailList([os.getenv('USER')], '--to', alias, raise_on_error) 381 to = BuildEmailList([os.getenv('USER')], '--to', alias, raise_on_error)
380 cc = [] 382 cc = []
381 cmd = ['git', 'send-email', '--annotate'] 383 cmd = ['git', 'send-email', '--annotate']
382 if in_reply_to: 384 if in_reply_to:
383 cmd.append('--in-reply-to="%s"' % in_reply_to) 385 cmd.append('--in-reply-to="%s"' % in_reply_to)
384 386
385 cmd += to 387 cmd += to
386 cmd += cc 388 cmd += cc
387 cmd += ['--cc-cmd', '"%s --cc-cmd %s"' % (sys.argv[0], cc_fname)] 389 cmd += ['--cc-cmd', '"%s --cc-cmd %s"' % (sys.argv[0], cc_fname)]
388 if cover_fname: 390 if cover_fname:
389 cmd.append(cover_fname) 391 cmd.append(cover_fname)
390 cmd += args 392 cmd += args
391 str = ' '.join(cmd) 393 str = ' '.join(cmd)
392 if not dry_run: 394 if not dry_run:
393 os.system(str) 395 os.system(str)
394 return str 396 return str
395 397
396 398
397 def LookupEmail(lookup_name, alias=None, raise_on_error=True, level=0): 399 def LookupEmail(lookup_name, alias=None, raise_on_error=True, level=0):
398 """If an email address is an alias, look it up and return the full name 400 """If an email address is an alias, look it up and return the full name
399 401
400 TODO: Why not just use git's own alias feature? 402 TODO: Why not just use git's own alias feature?
401 403
402 Args: 404 Args:
403 lookup_name: Alias or email address to look up 405 lookup_name: Alias or email address to look up
404 alias: Dictionary containing aliases (None to use settings default) 406 alias: Dictionary containing aliases (None to use settings default)
405 raise_on_error: True to raise an error when an alias fails to match, 407 raise_on_error: True to raise an error when an alias fails to match,
406 False to just print a message. 408 False to just print a message.
407 409
408 Returns: 410 Returns:
409 tuple: 411 tuple:
410 list containing a list of email addresses 412 list containing a list of email addresses
411 413
412 Raises: 414 Raises:
413 OSError if a recursive alias reference was found 415 OSError if a recursive alias reference was found
414 ValueError if an alias was not found 416 ValueError if an alias was not found
415 417
416 >>> alias = {} 418 >>> alias = {}
417 >>> alias['fred'] = ['f.bloggs@napier.co.nz'] 419 >>> alias['fred'] = ['f.bloggs@napier.co.nz']
418 >>> alias['john'] = ['j.bloggs@napier.co.nz'] 420 >>> alias['john'] = ['j.bloggs@napier.co.nz']
419 >>> alias['mary'] = ['m.poppins@cloud.net'] 421 >>> alias['mary'] = ['m.poppins@cloud.net']
420 >>> alias['boys'] = ['fred', ' john', 'f.bloggs@napier.co.nz'] 422 >>> alias['boys'] = ['fred', ' john', 'f.bloggs@napier.co.nz']
421 >>> alias['all'] = ['fred ', 'john', ' mary '] 423 >>> alias['all'] = ['fred ', 'john', ' mary ']
422 >>> alias['loop'] = ['other', 'john', ' mary '] 424 >>> alias['loop'] = ['other', 'john', ' mary ']
423 >>> alias['other'] = ['loop', 'john', ' mary '] 425 >>> alias['other'] = ['loop', 'john', ' mary ']
424 >>> LookupEmail('mary', alias) 426 >>> LookupEmail('mary', alias)
425 ['m.poppins@cloud.net'] 427 ['m.poppins@cloud.net']
426 >>> LookupEmail('arthur.wellesley@howe.ro.uk', alias) 428 >>> LookupEmail('arthur.wellesley@howe.ro.uk', alias)
427 ['arthur.wellesley@howe.ro.uk'] 429 ['arthur.wellesley@howe.ro.uk']
428 >>> LookupEmail('boys', alias) 430 >>> LookupEmail('boys', alias)
429 ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz'] 431 ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz']
430 >>> LookupEmail('all', alias) 432 >>> LookupEmail('all', alias)
431 ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz', 'm.poppins@cloud.net'] 433 ['f.bloggs@napier.co.nz', 'j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
432 >>> LookupEmail('odd', alias) 434 >>> LookupEmail('odd', alias)
433 Traceback (most recent call last): 435 Traceback (most recent call last):
434 ... 436 ...
435 ValueError: Alias 'odd' not found 437 ValueError: Alias 'odd' not found
436 >>> LookupEmail('loop', alias) 438 >>> LookupEmail('loop', alias)
437 Traceback (most recent call last): 439 Traceback (most recent call last):
438 ... 440 ...
439 OSError: Recursive email alias at 'other' 441 OSError: Recursive email alias at 'other'
440 >>> LookupEmail('odd', alias, raise_on_error=False) 442 >>> LookupEmail('odd', alias, raise_on_error=False)
441 \033[1;31mAlias 'odd' not found\033[0m 443 \033[1;31mAlias 'odd' not found\033[0m
442 [] 444 []
443 >>> # In this case the loop part will effectively be ignored. 445 >>> # In this case the loop part will effectively be ignored.
444 >>> LookupEmail('loop', alias, raise_on_error=False) 446 >>> LookupEmail('loop', alias, raise_on_error=False)
445 \033[1;31mRecursive email alias at 'other'\033[0m 447 \033[1;31mRecursive email alias at 'other'\033[0m
446 \033[1;31mRecursive email alias at 'john'\033[0m 448 \033[1;31mRecursive email alias at 'john'\033[0m
447 \033[1;31mRecursive email alias at 'mary'\033[0m 449 \033[1;31mRecursive email alias at 'mary'\033[0m
448 ['j.bloggs@napier.co.nz', 'm.poppins@cloud.net'] 450 ['j.bloggs@napier.co.nz', 'm.poppins@cloud.net']
449 """ 451 """
450 if not alias: 452 if not alias:
451 alias = settings.alias 453 alias = settings.alias
452 lookup_name = lookup_name.strip() 454 lookup_name = lookup_name.strip()
453 if '@' in lookup_name: # Perhaps a real email address 455 if '@' in lookup_name: # Perhaps a real email address
454 return [lookup_name] 456 return [lookup_name]
455 457
456 lookup_name = lookup_name.lower() 458 lookup_name = lookup_name.lower()
457 col = terminal.Color() 459 col = terminal.Color()
458 460
459 out_list = [] 461 out_list = []
460 if level > 10: 462 if level > 10:
461 msg = "Recursive email alias at '%s'" % lookup_name 463 msg = "Recursive email alias at '%s'" % lookup_name
462 if raise_on_error: 464 if raise_on_error:
463 raise OSError, msg 465 raise OSError, msg
464 else: 466 else:
465 print col.Color(col.RED, msg) 467 print col.Color(col.RED, msg)
466 return out_list 468 return out_list
467 469
468 if lookup_name: 470 if lookup_name:
469 if not lookup_name in alias: 471 if not lookup_name in alias:
470 msg = "Alias '%s' not found" % lookup_name 472 msg = "Alias '%s' not found" % lookup_name
471 if raise_on_error: 473 if raise_on_error:
472 raise ValueError, msg 474 raise ValueError, msg
473 else: 475 else:
474 print col.Color(col.RED, msg) 476 print col.Color(col.RED, msg)
475 return out_list 477 return out_list
476 for item in alias[lookup_name]: 478 for item in alias[lookup_name]:
477 todo = LookupEmail(item, alias, raise_on_error, level + 1) 479 todo = LookupEmail(item, alias, raise_on_error, level + 1)
478 for new_item in todo: 480 for new_item in todo:
479 if not new_item in out_list: 481 if not new_item in out_list:
480 out_list.append(new_item) 482 out_list.append(new_item)
481 483
482 #print "No match for alias '%s'" % lookup_name 484 #print "No match for alias '%s'" % lookup_name
483 return out_list 485 return out_list
484 486
485 def GetTopLevel(): 487 def GetTopLevel():
486 """Return name of top-level directory for this git repo. 488 """Return name of top-level directory for this git repo.
487 489
488 Returns: 490 Returns:
489 Full path to git top-level directory 491 Full path to git top-level directory
490 492
491 This test makes sure that we are running tests in the right subdir 493 This test makes sure that we are running tests in the right subdir
492 494
493 >>> os.path.realpath(os.path.dirname(__file__)) == \ 495 >>> os.path.realpath(os.path.dirname(__file__)) == \
494 os.path.join(GetTopLevel(), 'tools', 'patman') 496 os.path.join(GetTopLevel(), 'tools', 'patman')
495 True 497 True
496 """ 498 """
497 return command.OutputOneLine('git', 'rev-parse', '--show-toplevel') 499 return command.OutputOneLine('git', 'rev-parse', '--show-toplevel')
498 500
499 def GetAliasFile(): 501 def GetAliasFile():
500 """Gets the name of the git alias file. 502 """Gets the name of the git alias file.
501 503
502 Returns: 504 Returns:
503 Filename of git alias file, or None if none 505 Filename of git alias file, or None if none
504 """ 506 """
505 fname = command.OutputOneLine('git', 'config', 'sendemail.aliasesfile', 507 fname = command.OutputOneLine('git', 'config', 'sendemail.aliasesfile',
506 raise_on_error=False) 508 raise_on_error=False)
507 if fname: 509 if fname:
508 fname = os.path.join(GetTopLevel(), fname.strip()) 510 fname = os.path.join(GetTopLevel(), fname.strip())
509 return fname 511 return fname
510 512
511 def GetDefaultUserName(): 513 def GetDefaultUserName():
512 """Gets the user.name from .gitconfig file. 514 """Gets the user.name from .gitconfig file.
513 515
514 Returns: 516 Returns:
515 User name found in .gitconfig file, or None if none 517 User name found in .gitconfig file, or None if none
516 """ 518 """
517 uname = command.OutputOneLine('git', 'config', '--global', 'user.name') 519 uname = command.OutputOneLine('git', 'config', '--global', 'user.name')
518 return uname 520 return uname
519 521
520 def GetDefaultUserEmail(): 522 def GetDefaultUserEmail():
521 """Gets the user.email from the global .gitconfig file. 523 """Gets the user.email from the global .gitconfig file.
522 524
523 Returns: 525 Returns:
524 User's email found in .gitconfig file, or None if none 526 User's email found in .gitconfig file, or None if none
525 """ 527 """
526 uemail = command.OutputOneLine('git', 'config', '--global', 'user.email') 528 uemail = command.OutputOneLine('git', 'config', '--global', 'user.email')
527 return uemail 529 return uemail
528 530
529 def Setup(): 531 def Setup():
530 """Set up git utils, by reading the alias files.""" 532 """Set up git utils, by reading the alias files."""
531 # Check for a git alias file also 533 # Check for a git alias file also
532 alias_fname = GetAliasFile() 534 alias_fname = GetAliasFile()
533 if alias_fname: 535 if alias_fname:
534 settings.ReadGitAliases(alias_fname) 536 settings.ReadGitAliases(alias_fname)
535 537
536 def GetHead(): 538 def GetHead():
537 """Get the hash of the current HEAD 539 """Get the hash of the current HEAD
538 540
539 Returns: 541 Returns:
540 Hash of HEAD 542 Hash of HEAD
541 """ 543 """
542 return command.OutputOneLine('git', 'show', '-s', '--pretty=format:%H') 544 return command.OutputOneLine('git', 'show', '-s', '--pretty=format:%H')
543 545
544 if __name__ == "__main__": 546 if __name__ == "__main__":
545 import doctest 547 import doctest
546 548
547 doctest.testmod() 549 doctest.testmod()
548 550