Commit 757f64a89ba5bb04661b3f43444ca57fa6db1132
1 parent
75b3c3aa84
Exists in
v2017.01-smarct4x
and in
40 other branches
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 |