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