Allow importing mbox files from stdin
[stgit] / stgit / commands / imprt.py
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
18 import sys, os, re, email
19 from email.Header import decode_header, make_header
20 from mailbox import UnixMailbox
21 from StringIO import StringIO
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'
30 usage = """%prog [options] [<file>]
31
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
34 can be overridden with the '--name' option. The patch can either be a
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
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.
43
44 The patch description has to be separated from the data with a '---'
45 line."""
46
47 options = [make_option('-m', '--mail',
48 help = 'import the patch from a standard e-mail file',
49 action = 'store_true'),
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'),
56 make_option('-n', '--name',
57 help = 'use NAME as the patch name'),
58 make_option('-t', '--strip',
59 help = 'strip numbering and extension from patch name',
60 action = 'store_true'),
61 make_option('-i', '--ignore',
62 help = 'ignore the applied patches in the series',
63 action = 'store_true'),
64 make_option('--replace',
65 help = 'replace the unapplied patches in the series',
66 action = 'store_true'),
67 make_option('-b', '--base',
68 help = 'use BASE instead of HEAD for file importing'),
69 make_option('-e', '--edit',
70 help = 'invoke an editor for the patch description',
71 action = 'store_true'),
72 make_option('-p', '--showpatch',
73 help = 'show the patch content in the editor buffer',
74 action = 'store_true'),
75 make_option('-a', '--author', metavar = '"NAME <EMAIL>"',
76 help = 'use "NAME <EMAIL>" as the author details'),
77 make_option('--authname',
78 help = 'use AUTHNAME as the author name'),
79 make_option('--authemail',
80 help = 'use AUTHEMAIL as the author e-mail'),
81 make_option('--authdate',
82 help = 'use AUTHDATE as the author date'),
83 make_option('--commname',
84 help = 'use COMMNAME as the committer name'),
85 make_option('--commemail',
86 help = 'use COMMEMAIL as the committer e-mail')]
87
88
89 def __end_descr(line):
90 return re.match('---\s*$', line) or re.match('diff -', line) or \
91 re.match('Index: ', line)
92
93 def __strip_patch_name(name):
94 stripped = re.sub('^[0-9]+-(.*)$', '\g<1>', name)
95 stripped = re.sub('^(.*)\.(diff|patch)$', '\g<1>', stripped)
96
97 return stripped
98
99 def __replace_slashes_with_dashes(name):
100 stripped = name.replace('/', '-')
101
102 return stripped
103
104 def __parse_description(descr):
105 """Parse the patch description and return the new description and
106 author information (if any).
107 """
108 subject = body = ''
109 authname = authemail = authdate = None
110
111 descr_lines = [line.rstrip() for line in descr.split('\n')]
112 if not descr_lines:
113 raise CmdException, "Empty patch description"
114
115 lasthdr = 0
116 end = len(descr_lines)
117
118 # Parse the patch header
119 for pos in range(0, end):
120 if not descr_lines[pos]:
121 continue
122 # check for a "From|Author:" line
123 if re.match('\s*(?:from|author):\s+', descr_lines[pos], re.I):
124 auth = re.findall('^.*?:\s+(.*)$', descr_lines[pos])[0]
125 authname, authemail = name_email(auth)
126 lasthdr = pos + 1
127 continue
128 # check for a "Date:" line
129 if re.match('\s*date:\s+', descr_lines[pos], re.I):
130 authdate = re.findall('^.*?:\s+(.*)$', descr_lines[pos])[0]
131 lasthdr = pos + 1
132 continue
133 if subject:
134 break
135 # get the subject
136 subject = descr_lines[pos]
137 lasthdr = pos + 1
138
139 # get the body
140 if lasthdr < end:
141 body = reduce(lambda x, y: x + '\n' + y, descr_lines[lasthdr:], '')
142
143 return (subject + body, authname, authemail, authdate)
144
145 def __parse_mail(msg):
146 """Parse the message object and return (description, authname,
147 authemail, authdate, diff)
148 """
149 def __decode_header(header):
150 """Decode a qp-encoded e-mail header as per rfc2047"""
151 try:
152 words_enc = decode_header(header)
153 hobj = make_header(words_enc)
154 except Exception, ex:
155 raise CmdException, 'header decoding error: %s' % str(ex)
156 return unicode(hobj).encode('utf-8')
157
158 # parse the headers
159 if msg.has_key('from'):
160 authname, authemail = name_email(__decode_header(msg['from']))
161 else:
162 authname = authemail = None
163
164 # '\n\t' can be found on multi-line headers
165 descr = __decode_header(msg['subject']).replace('\n\t', ' ')
166 authdate = msg['date']
167
168 # remove the '[*PATCH*]' expression in the subject
169 if descr:
170 descr = re.findall('^(\[.*?[Pp][Aa][Tt][Cc][Hh].*?\])?\s*(.*)$',
171 descr)[0][1]
172 descr += '\n\n'
173 else:
174 raise CmdException, 'Subject: line not found'
175
176 # the rest of the message
177 if msg.is_multipart():
178 # this is assuming that the first part is the patch
179 # description and the second part is the attached patch
180 descr += msg.get_payload(0).get_payload(decode = True)
181 diff = msg.get_payload(1).get_payload(decode = True)
182 else:
183 diff = msg.get_payload(decode = True)
184
185 for line in diff.split('\n'):
186 if __end_descr(line):
187 break
188 descr += line + '\n'
189
190 descr.rstrip()
191
192 # parse the description for author information
193 descr, descr_authname, descr_authemail, descr_authdate = \
194 __parse_description(descr)
195 if descr_authname:
196 authname = descr_authname
197 if descr_authemail:
198 authemail = descr_authemail
199 if descr_authdate:
200 authdate = descr_authdate
201
202 return (descr, authname, authemail, authdate, diff)
203
204 def __parse_patch(fobj):
205 """Parse the input file and return (description, authname,
206 authemail, authdate, diff)
207 """
208 descr = ''
209 while True:
210 line = fobj.readline()
211 if not line:
212 break
213
214 if __end_descr(line):
215 break
216 else:
217 descr += line
218 descr.rstrip()
219
220 diff = fobj.read()
221
222 descr, authname, authemail, authdate = __parse_description(descr)
223
224 # we don't yet have an agreed place for the creation date.
225 # Just return None
226 return (descr, authname, authemail, authdate, diff)
227
228 def __create_patch(patch, message, author_name, author_email,
229 author_date, diff, options):
230 """Create a new patch on the stack
231 """
232 if not diff:
233 raise CmdException, 'No diff found inside the patch'
234
235 if not patch:
236 patch = make_patch_name(message, crt_series.patch_exists,
237 alternative = not (options.ignore
238 or options.replace))
239
240 if options.ignore and patch in crt_series.get_applied():
241 print 'Ignoring already applied patch "%s"' % patch
242 return
243 if options.replace and patch in crt_series.get_unapplied():
244 crt_series.delete_patch(patch)
245
246 # refresh_patch() will invoke the editor in this case, with correct
247 # patch content
248 if not message:
249 can_edit = False
250
251 committer_name = committer_email = None
252
253 if options.author:
254 options.authname, options.authemail = name_email(options.author)
255
256 # override the automatically parsed settings
257 if options.authname:
258 author_name = options.authname
259 if options.authemail:
260 author_email = options.authemail
261 if options.authdate:
262 author_date = options.authdate
263 if options.commname:
264 committer_name = options.commname
265 if options.commemail:
266 committer_email = options.commemail
267
268 crt_series.new_patch(patch, message = message, can_edit = False,
269 author_name = author_name,
270 author_email = author_email,
271 author_date = author_date,
272 committer_name = committer_name,
273 committer_email = committer_email)
274
275 print 'Importing patch "%s"...' % patch,
276 sys.stdout.flush()
277
278 if options.base:
279 git.apply_patch(diff = diff, base = git_id(options.base))
280 else:
281 git.apply_patch(diff = diff)
282
283 crt_series.refresh_patch(edit = options.edit,
284 show_patch = options.showpatch)
285
286 print 'done'
287
288 def __import_file(patch, filename, options):
289 """Import a patch from a file or standard input
290 """
291 if filename:
292 f = file(filename)
293 else:
294 f = sys.stdin
295
296 if options.mail:
297 try:
298 msg = email.message_from_file(f)
299 except Exception, ex:
300 raise CmdException, 'error parsing the e-mail file: %s' % str(ex)
301 message, author_name, author_email, author_date, diff = \
302 __parse_mail(msg)
303 else:
304 message, author_name, author_email, author_date, diff = \
305 __parse_patch(f)
306
307 if filename:
308 f.close()
309
310 __create_patch(patch, message, author_name, author_email,
311 author_date, diff, options)
312
313 def __import_series(filename, options):
314 """Import a series of patches
315 """
316 applied = crt_series.get_applied()
317
318 if filename:
319 f = file(filename)
320 patchdir = os.path.dirname(filename)
321 else:
322 f = sys.stdin
323 patchdir = ''
324
325 for line in f:
326 patch = re.sub('#.*$', '', line).strip()
327 if not patch:
328 continue
329 patchfile = os.path.join(patchdir, patch)
330
331 if options.strip:
332 patch = __strip_patch_name(patch)
333 patch = __replace_slashes_with_dashes(patch);
334
335 __import_file(patch, patchfile, options)
336
337 if filename:
338 f.close()
339
340 def __import_mbox(filename, options):
341 """Import a series from an mbox file
342 """
343 if filename:
344 f = file(filename, 'rb')
345 else:
346 f = StringIO(sys.stdin.read())
347
348 try:
349 mbox = UnixMailbox(f, email.message_from_file)
350 except Exception, ex:
351 raise CmdException, 'error parsing the mbox file: %s' % str(ex)
352
353 for msg in mbox:
354 message, author_name, author_email, author_date, diff = \
355 __parse_mail(msg)
356 __create_patch(None, message, author_name, author_email,
357 author_date, diff, options)
358
359 f.close()
360
361 def func(parser, options, args):
362 """Import a GNU diff file as a new patch
363 """
364 if len(args) > 1:
365 parser.error('incorrect number of arguments')
366
367 check_local_changes()
368 check_conflicts()
369 check_head_top_equal()
370
371 if len(args) == 1:
372 filename = args[0]
373 else:
374 filename = None
375
376 if options.series:
377 __import_series(filename, options)
378 elif options.mbox:
379 __import_mbox(filename, options)
380 else:
381 if options.name:
382 patch = options.name
383 elif filename:
384 patch = os.path.basename(filename)
385 else:
386 patch = ''
387 if options.strip:
388 patch = __strip_patch_name(patch)
389
390 __import_file(patch, filename, options)
391
392 print_crt_patch()