Commit | Line | Data |
---|---|---|
0d2cd1e4 CM |
1 | __copyright__ = """ |
2 | Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com> | |
3 | ||
4 | This program is free software; you can redistribute it and/or modify | |
5 | it under the terms of the GNU General Public License version 2 as | |
6 | published by the Free Software Foundation. | |
7 | ||
8 | This program is distributed in the hope that it will be useful, | |
9 | but WITHOUT ANY WARRANTY; without even the implied warranty of | |
10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
11 | GNU General Public License for more details. | |
12 | ||
13 | You should have received a copy of the GNU General Public License | |
14 | along with this program; if not, write to the Free Software | |
15 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA | |
16 | """ | |
17 | ||
6ef533bc | 18 | import sys, os, re, email |
2ac5a14c | 19 | from email.Header import decode_header, make_header |
99c52915 | 20 | from mailbox import UnixMailbox |
457c3093 | 21 | from StringIO import StringIO |
0d2cd1e4 CM |
22 | from optparse import OptionParser, make_option |
23 | ||
24 | from stgit.commands.common import * | |
25 | from stgit.utils import * | |
26 | from stgit import stack, git | |
27 | ||
28 | ||
29 | help = 'import a GNU diff file as a new patch' | |
575c575e | 30 | usage = """%prog [options] [<file>|<url>] |
0d2cd1e4 | 31 | |
b8a0986f CM |
32 | Create a new patch and apply the given GNU diff file (or the standard |
33 | input). By default, the file name is used as the patch name but this | |
388f63b6 | 34 | can be overridden with the '--name' option. The patch can either be a |
b8a0986f CM |
35 | normal file with the description at the top or it can have standard |
36 | mail format, the Subject, From and Date headers being used for | |
99c52915 CM |
37 | generating the patch information. The command can also read series and |
38 | mbox files. | |
39 | ||
40 | If a patch does not apply cleanly, the failed diff is written to the | |
41 | .stgit-failed.patch file and an empty StGIT patch is added to the | |
42 | stack. | |
0d2cd1e4 | 43 | |
b8a0986f | 44 | The patch description has to be separated from the data with a '---' |
99e73103 | 45 | line.""" |
0d2cd1e4 CM |
46 | |
47 | options = [make_option('-m', '--mail', | |
48 | help = 'import the patch from a standard e-mail file', | |
49 | action = 'store_true'), | |
99c52915 CM |
50 | make_option('-M', '--mbox', |
51 | help = 'import a series of patches from an mbox file', | |
52 | action = 'store_true'), | |
53 | make_option('-s', '--series', | |
54 | help = 'import a series of patches', | |
55 | action = 'store_true'), | |
575c575e CW |
56 | make_option('-u', '--url', |
57 | help = 'import a patch from a URL', | |
58 | action = 'store_true'), | |
0d2cd1e4 CM |
59 | make_option('-n', '--name', |
60 | help = 'use NAME as the patch name'), | |
b0cdad5e CM |
61 | make_option('-t', '--strip', |
62 | help = 'strip numbering and extension from patch name', | |
63 | action = 'store_true'), | |
9417ece4 CM |
64 | make_option('-i', '--ignore', |
65 | help = 'ignore the applied patches in the series', | |
66 | action = 'store_true'), | |
034db15c CM |
67 | make_option('--replace', |
68 | help = 'replace the unapplied patches in the series', | |
69 | action = 'store_true'), | |
b21bc8d1 | 70 | make_option('-b', '--base', |
35344f86 | 71 | help = 'use BASE instead of HEAD for file importing'), |
33e580e0 CM |
72 | make_option('-e', '--edit', |
73 | help = 'invoke an editor for the patch description', | |
74 | action = 'store_true'), | |
9417ece4 | 75 | make_option('-p', '--showpatch', |
6ad48e48 PBG |
76 | help = 'show the patch content in the editor buffer', |
77 | action = 'store_true'), | |
0d2cd1e4 CM |
78 | make_option('-a', '--author', metavar = '"NAME <EMAIL>"', |
79 | help = 'use "NAME <EMAIL>" as the author details'), | |
80 | make_option('--authname', | |
81 | help = 'use AUTHNAME as the author name'), | |
82 | make_option('--authemail', | |
83 | help = 'use AUTHEMAIL as the author e-mail'), | |
84 | make_option('--authdate', | |
85 | help = 'use AUTHDATE as the author date'), | |
86 | make_option('--commname', | |
87 | help = 'use COMMNAME as the committer name'), | |
88 | make_option('--commemail', | |
89 | help = 'use COMMEMAIL as the committer e-mail')] | |
90 | ||
91 | ||
d4c43e19 PBG |
92 | def __end_descr(line): |
93 | return re.match('---\s*$', line) or re.match('diff -', line) or \ | |
94 | re.match('Index: ', line) | |
99e73103 | 95 | |
b0cdad5e | 96 | def __strip_patch_name(name): |
bcb6d890 CM |
97 | stripped = re.sub('^[0-9]+-(.*)$', '\g<1>', name) |
98 | stripped = re.sub('^(.*)\.(diff|patch)$', '\g<1>', stripped) | |
99 | ||
100 | return stripped | |
b0cdad5e | 101 | |
613a2f16 PBG |
102 | def __replace_slashes_with_dashes(name): |
103 | stripped = name.replace('/', '-') | |
104 | ||
105 | return stripped | |
106 | ||
99e73103 CM |
107 | def __parse_description(descr): |
108 | """Parse the patch description and return the new description and | |
109 | author information (if any). | |
110 | """ | |
111 | subject = body = '' | |
0543bc5f | 112 | authname = authemail = authdate = None |
99e73103 | 113 | |
0543bc5f | 114 | descr_lines = [line.rstrip() for line in descr.split('\n')] |
99e73103 CM |
115 | if not descr_lines: |
116 | raise CmdException, "Empty patch description" | |
117 | ||
0543bc5f | 118 | lasthdr = 0 |
99e73103 CM |
119 | end = len(descr_lines) |
120 | ||
0543bc5f | 121 | # Parse the patch header |
61dabd0e | 122 | for pos in range(0, end): |
0543bc5f TM |
123 | if not descr_lines[pos]: |
124 | continue | |
125 | # check for a "From|Author:" line | |
126 | if re.match('\s*(?:from|author):\s+', descr_lines[pos], re.I): | |
127 | auth = re.findall('^.*?:\s+(.*)$', descr_lines[pos])[0] | |
128 | authname, authemail = name_email(auth) | |
129 | lasthdr = pos + 1 | |
130 | continue | |
131 | # check for a "Date:" line | |
132 | if re.match('\s*date:\s+', descr_lines[pos], re.I): | |
133 | authdate = re.findall('^.*?:\s+(.*)$', descr_lines[pos])[0] | |
134 | lasthdr = pos + 1 | |
135 | continue | |
136 | if subject: | |
137 | break | |
138 | # get the subject | |
139 | subject = descr_lines[pos] | |
140 | lasthdr = pos + 1 | |
99e73103 CM |
141 | |
142 | # get the body | |
0543bc5f TM |
143 | if lasthdr < end: |
144 | body = reduce(lambda x, y: x + '\n' + y, descr_lines[lasthdr:], '') | |
99e73103 | 145 | |
0543bc5f | 146 | return (subject + body, authname, authemail, authdate) |
99e73103 | 147 | |
99c52915 CM |
148 | def __parse_mail(msg): |
149 | """Parse the message object and return (description, authname, | |
150 | authemail, authdate, diff) | |
0d2cd1e4 | 151 | """ |
2ac5a14c CM |
152 | def __decode_header(header): |
153 | """Decode a qp-encoded e-mail header as per rfc2047""" | |
154 | try: | |
155 | words_enc = decode_header(header) | |
156 | hobj = make_header(words_enc) | |
157 | except Exception, ex: | |
158 | raise CmdException, 'header decoding error: %s' % str(ex) | |
159 | return unicode(hobj).encode('utf-8') | |
160 | ||
0d2cd1e4 | 161 | # parse the headers |
6ef533bc CM |
162 | if msg.has_key('from'): |
163 | authname, authemail = name_email(__decode_header(msg['from'])) | |
164 | else: | |
165 | authname = authemail = None | |
166 | ||
99c52915 CM |
167 | # '\n\t' can be found on multi-line headers |
168 | descr = __decode_header(msg['subject']).replace('\n\t', ' ') | |
6ef533bc | 169 | authdate = msg['date'] |
0d2cd1e4 | 170 | |
186e6b6b | 171 | # remove the '[*PATCH*]' expression in the subject |
0d2cd1e4 | 172 | if descr: |
dfeeba67 | 173 | descr = re.findall('^(\[.*?[Pp][Aa][Tt][Cc][Hh].*?\])?\s*(.*)$', |
7c02f338 | 174 | descr)[0][1] |
0d2cd1e4 CM |
175 | descr += '\n\n' |
176 | else: | |
177 | raise CmdException, 'Subject: line not found' | |
178 | ||
6ef533bc CM |
179 | # the rest of the message |
180 | if msg.is_multipart(): | |
99c52915 CM |
181 | # this is assuming that the first part is the patch |
182 | # description and the second part is the attached patch | |
183 | descr += msg.get_payload(0).get_payload(decode = True) | |
184 | diff = msg.get_payload(1).get_payload(decode = True) | |
6ef533bc CM |
185 | else: |
186 | diff = msg.get_payload(decode = True) | |
0d2cd1e4 | 187 | |
6ef533bc CM |
188 | for line in diff.split('\n'): |
189 | if __end_descr(line): | |
190 | break | |
191 | descr += line + '\n' | |
192 | ||
193 | descr.rstrip() | |
0d2cd1e4 | 194 | |
99e73103 | 195 | # parse the description for author information |
6ef533bc CM |
196 | descr, descr_authname, descr_authemail, descr_authdate = \ |
197 | __parse_description(descr) | |
99e73103 CM |
198 | if descr_authname: |
199 | authname = descr_authname | |
200 | if descr_authemail: | |
201 | authemail = descr_authemail | |
0543bc5f TM |
202 | if descr_authdate: |
203 | authdate = descr_authdate | |
99e73103 | 204 | |
6ef533bc | 205 | return (descr, authname, authemail, authdate, diff) |
0d2cd1e4 | 206 | |
99c52915 | 207 | def __parse_patch(fobj): |
0d2cd1e4 | 208 | """Parse the input file and return (description, authname, |
99c52915 | 209 | authemail, authdate, diff) |
0d2cd1e4 | 210 | """ |
0d2cd1e4 | 211 | descr = '' |
6fe6b1bd | 212 | while True: |
99c52915 | 213 | line = fobj.readline() |
6fe6b1bd CM |
214 | if not line: |
215 | break | |
216 | ||
d4c43e19 | 217 | if __end_descr(line): |
0d2cd1e4 CM |
218 | break |
219 | else: | |
220 | descr += line | |
221 | descr.rstrip() | |
222 | ||
99c52915 | 223 | diff = fobj.read() |
0d2cd1e4 | 224 | |
0543bc5f | 225 | descr, authname, authemail, authdate = __parse_description(descr) |
99e73103 CM |
226 | |
227 | # we don't yet have an agreed place for the creation date. | |
228 | # Just return None | |
6ef533bc | 229 | return (descr, authname, authemail, authdate, diff) |
0d2cd1e4 | 230 | |
99c52915 CM |
231 | def __create_patch(patch, message, author_name, author_email, |
232 | author_date, diff, options): | |
233 | """Create a new patch on the stack | |
0d2cd1e4 | 234 | """ |
6ef533bc CM |
235 | if not diff: |
236 | raise CmdException, 'No diff found inside the patch' | |
237 | ||
fff9bce5 | 238 | if not patch: |
99c52915 CM |
239 | patch = make_patch_name(message, crt_series.patch_exists, |
240 | alternative = not (options.ignore | |
241 | or options.replace)) | |
242 | ||
243 | if options.ignore and patch in crt_series.get_applied(): | |
244 | print 'Ignoring already applied patch "%s"' % patch | |
245 | return | |
246 | if options.replace and patch in crt_series.get_unapplied(): | |
247 | crt_series.delete_patch(patch) | |
fff9bce5 | 248 | |
95742cfc PBG |
249 | # refresh_patch() will invoke the editor in this case, with correct |
250 | # patch content | |
9d15ccd8 | 251 | if not message: |
95742cfc | 252 | can_edit = False |
9d15ccd8 | 253 | |
99c52915 CM |
254 | committer_name = committer_email = None |
255 | ||
256 | if options.author: | |
257 | options.authname, options.authemail = name_email(options.author) | |
258 | ||
0d2cd1e4 CM |
259 | # override the automatically parsed settings |
260 | if options.authname: | |
261 | author_name = options.authname | |
262 | if options.authemail: | |
263 | author_email = options.authemail | |
264 | if options.authdate: | |
265 | author_date = options.authdate | |
266 | if options.commname: | |
267 | committer_name = options.commname | |
268 | if options.commemail: | |
269 | committer_email = options.commemail | |
270 | ||
95742cfc | 271 | crt_series.new_patch(patch, message = message, can_edit = False, |
0d2cd1e4 CM |
272 | author_name = author_name, |
273 | author_email = author_email, | |
274 | author_date = author_date, | |
275 | committer_name = committer_name, | |
276 | committer_email = committer_email) | |
277 | ||
9417ece4 | 278 | print 'Importing patch "%s"...' % patch, |
0d2cd1e4 CM |
279 | sys.stdout.flush() |
280 | ||
35344f86 | 281 | if options.base: |
6ef533bc | 282 | git.apply_patch(diff = diff, base = git_id(options.base)) |
35344f86 | 283 | else: |
6ef533bc | 284 | git.apply_patch(diff = diff) |
35344f86 | 285 | |
6ad48e48 PBG |
286 | crt_series.refresh_patch(edit = options.edit, |
287 | show_patch = options.showpatch) | |
0d2cd1e4 | 288 | |
99c52915 CM |
289 | print 'done' |
290 | ||
291 | def __import_file(patch, filename, options): | |
292 | """Import a patch from a file or standard input | |
293 | """ | |
294 | if filename: | |
295 | f = file(filename) | |
296 | else: | |
297 | f = sys.stdin | |
298 | ||
299 | if options.mail: | |
300 | try: | |
301 | msg = email.message_from_file(f) | |
302 | except Exception, ex: | |
303 | raise CmdException, 'error parsing the e-mail file: %s' % str(ex) | |
304 | message, author_name, author_email, author_date, diff = \ | |
305 | __parse_mail(msg) | |
306 | else: | |
307 | message, author_name, author_email, author_date, diff = \ | |
308 | __parse_patch(f) | |
309 | ||
310 | if filename: | |
311 | f.close() | |
312 | ||
313 | __create_patch(patch, message, author_name, author_email, | |
314 | author_date, diff, options) | |
9417ece4 CM |
315 | |
316 | def __import_series(filename, options): | |
317 | """Import a series of patches | |
318 | """ | |
319 | applied = crt_series.get_applied() | |
320 | ||
321 | if filename: | |
322 | f = file(filename) | |
323 | patchdir = os.path.dirname(filename) | |
324 | else: | |
325 | f = sys.stdin | |
326 | patchdir = '' | |
327 | ||
328 | for line in f: | |
329 | patch = re.sub('#.*$', '', line).strip() | |
330 | if not patch: | |
331 | continue | |
bcb6d890 CM |
332 | patchfile = os.path.join(patchdir, patch) |
333 | ||
b0cdad5e CM |
334 | if options.strip: |
335 | patch = __strip_patch_name(patch) | |
613a2f16 | 336 | patch = __replace_slashes_with_dashes(patch); |
9417ece4 | 337 | |
99c52915 CM |
338 | __import_file(patch, patchfile, options) |
339 | ||
340 | if filename: | |
341 | f.close() | |
342 | ||
343 | def __import_mbox(filename, options): | |
344 | """Import a series from an mbox file | |
345 | """ | |
346 | if filename: | |
347 | f = file(filename, 'rb') | |
348 | else: | |
457c3093 | 349 | f = StringIO(sys.stdin.read()) |
99c52915 CM |
350 | |
351 | try: | |
352 | mbox = UnixMailbox(f, email.message_from_file) | |
353 | except Exception, ex: | |
354 | raise CmdException, 'error parsing the mbox file: %s' % str(ex) | |
355 | ||
356 | for msg in mbox: | |
357 | message, author_name, author_email, author_date, diff = \ | |
358 | __parse_mail(msg) | |
359 | __create_patch(None, message, author_name, author_email, | |
360 | author_date, diff, options) | |
361 | ||
457c3093 | 362 | f.close() |
9417ece4 | 363 | |
575c575e CW |
364 | def __import_url(url, options): |
365 | """Import a patch from a URL | |
366 | """ | |
367 | import urllib | |
368 | import tempfile | |
369 | ||
370 | if not url: | |
371 | parser.error('URL argument required') | |
372 | ||
373 | patch = os.path.basename(url) | |
374 | file = os.path.join(tempfile.gettempdir(), patch) | |
375 | urllib.urlretrieve(url, file) | |
376 | __import_file(patch, file, options) | |
377 | ||
9417ece4 CM |
378 | def func(parser, options, args): |
379 | """Import a GNU diff file as a new patch | |
380 | """ | |
381 | if len(args) > 1: | |
382 | parser.error('incorrect number of arguments') | |
383 | ||
384 | check_local_changes() | |
385 | check_conflicts() | |
386 | check_head_top_equal() | |
387 | ||
388 | if len(args) == 1: | |
389 | filename = args[0] | |
390 | else: | |
391 | filename = None | |
392 | ||
393 | if options.series: | |
394 | __import_series(filename, options) | |
99c52915 CM |
395 | elif options.mbox: |
396 | __import_mbox(filename, options) | |
575c575e CW |
397 | elif options.url: |
398 | __import_url(filename, options) | |
9417ece4 CM |
399 | else: |
400 | if options.name: | |
401 | patch = options.name | |
402 | elif filename: | |
403 | patch = os.path.basename(filename) | |
404 | else: | |
fff9bce5 | 405 | patch = '' |
b0cdad5e CM |
406 | if options.strip: |
407 | patch = __strip_patch_name(patch) | |
9417ece4 | 408 | |
99c52915 | 409 | __import_file(patch, filename, options) |
9417ece4 | 410 | |
0d2cd1e4 | 411 | print_crt_patch() |