Add --author option to series
[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
457c3093 21from StringIO import StringIO
0d2cd1e4
CM
22from optparse import OptionParser, make_option
23
24from stgit.commands.common import *
25from stgit.utils import *
26from stgit import stack, git
27
28
29help = 'import a GNU diff file as a new patch'
b8a0986f 30usage = """%prog [options] [<file>]
0d2cd1e4 31
b8a0986f
CM
32Create a new patch and apply the given GNU diff file (or the standard
33input). By default, the file name is used as the patch name but this
388f63b6 34can be overridden with the '--name' option. The patch can either be a
b8a0986f
CM
35normal file with the description at the top or it can have standard
36mail format, the Subject, From and Date headers being used for
99c52915
CM
37generating the patch information. The command can also read series and
38mbox files.
39
40If 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
42stack.
0d2cd1e4 43
b8a0986f 44The patch description has to be separated from the data with a '---'
99e73103 45line."""
0d2cd1e4
CM
46
47options = [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'),
0d2cd1e4
CM
56 make_option('-n', '--name',
57 help = 'use NAME as the patch name'),
b0cdad5e
CM
58 make_option('-t', '--strip',
59 help = 'strip numbering and extension from patch name',
60 action = 'store_true'),
9417ece4
CM
61 make_option('-i', '--ignore',
62 help = 'ignore the applied patches in the series',
63 action = 'store_true'),
034db15c
CM
64 make_option('--replace',
65 help = 'replace the unapplied patches in the series',
66 action = 'store_true'),
b21bc8d1 67 make_option('-b', '--base',
35344f86 68 help = 'use BASE instead of HEAD for file importing'),
33e580e0
CM
69 make_option('-e', '--edit',
70 help = 'invoke an editor for the patch description',
71 action = 'store_true'),
9417ece4 72 make_option('-p', '--showpatch',
6ad48e48
PBG
73 help = 'show the patch content in the editor buffer',
74 action = 'store_true'),
0d2cd1e4
CM
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
d4c43e19
PBG
89def __end_descr(line):
90 return re.match('---\s*$', line) or re.match('diff -', line) or \
91 re.match('Index: ', line)
99e73103 92
b0cdad5e 93def __strip_patch_name(name):
bcb6d890
CM
94 stripped = re.sub('^[0-9]+-(.*)$', '\g<1>', name)
95 stripped = re.sub('^(.*)\.(diff|patch)$', '\g<1>', stripped)
96
97 return stripped
b0cdad5e 98
613a2f16
PBG
99def __replace_slashes_with_dashes(name):
100 stripped = name.replace('/', '-')
101
102 return stripped
103
99e73103
CM
104def __parse_description(descr):
105 """Parse the patch description and return the new description and
106 author information (if any).
107 """
108 subject = body = ''
0543bc5f 109 authname = authemail = authdate = None
99e73103 110
0543bc5f 111 descr_lines = [line.rstrip() for line in descr.split('\n')]
99e73103
CM
112 if not descr_lines:
113 raise CmdException, "Empty patch description"
114
0543bc5f 115 lasthdr = 0
99e73103
CM
116 end = len(descr_lines)
117
0543bc5f 118 # Parse the patch header
61dabd0e 119 for pos in range(0, end):
0543bc5f
TM
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
99e73103
CM
138
139 # get the body
0543bc5f
TM
140 if lasthdr < end:
141 body = reduce(lambda x, y: x + '\n' + y, descr_lines[lasthdr:], '')
99e73103 142
0543bc5f 143 return (subject + body, authname, authemail, authdate)
99e73103 144
99c52915
CM
145def __parse_mail(msg):
146 """Parse the message object and return (description, authname,
147 authemail, authdate, diff)
0d2cd1e4 148 """
2ac5a14c
CM
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
0d2cd1e4 158 # parse the headers
6ef533bc
CM
159 if msg.has_key('from'):
160 authname, authemail = name_email(__decode_header(msg['from']))
161 else:
162 authname = authemail = None
163
99c52915
CM
164 # '\n\t' can be found on multi-line headers
165 descr = __decode_header(msg['subject']).replace('\n\t', ' ')
6ef533bc 166 authdate = msg['date']
0d2cd1e4 167
186e6b6b 168 # remove the '[*PATCH*]' expression in the subject
0d2cd1e4 169 if descr:
dfeeba67 170 descr = re.findall('^(\[.*?[Pp][Aa][Tt][Cc][Hh].*?\])?\s*(.*)$',
7c02f338 171 descr)[0][1]
0d2cd1e4
CM
172 descr += '\n\n'
173 else:
174 raise CmdException, 'Subject: line not found'
175
6ef533bc
CM
176 # the rest of the message
177 if msg.is_multipart():
99c52915
CM
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)
6ef533bc
CM
182 else:
183 diff = msg.get_payload(decode = True)
0d2cd1e4 184
6ef533bc
CM
185 for line in diff.split('\n'):
186 if __end_descr(line):
187 break
188 descr += line + '\n'
189
190 descr.rstrip()
0d2cd1e4 191
99e73103 192 # parse the description for author information
6ef533bc
CM
193 descr, descr_authname, descr_authemail, descr_authdate = \
194 __parse_description(descr)
99e73103
CM
195 if descr_authname:
196 authname = descr_authname
197 if descr_authemail:
198 authemail = descr_authemail
0543bc5f
TM
199 if descr_authdate:
200 authdate = descr_authdate
99e73103 201
6ef533bc 202 return (descr, authname, authemail, authdate, diff)
0d2cd1e4 203
99c52915 204def __parse_patch(fobj):
0d2cd1e4 205 """Parse the input file and return (description, authname,
99c52915 206 authemail, authdate, diff)
0d2cd1e4 207 """
0d2cd1e4 208 descr = ''
6fe6b1bd 209 while True:
99c52915 210 line = fobj.readline()
6fe6b1bd
CM
211 if not line:
212 break
213
d4c43e19 214 if __end_descr(line):
0d2cd1e4
CM
215 break
216 else:
217 descr += line
218 descr.rstrip()
219
99c52915 220 diff = fobj.read()
0d2cd1e4 221
0543bc5f 222 descr, authname, authemail, authdate = __parse_description(descr)
99e73103
CM
223
224 # we don't yet have an agreed place for the creation date.
225 # Just return None
6ef533bc 226 return (descr, authname, authemail, authdate, diff)
0d2cd1e4 227
99c52915
CM
228def __create_patch(patch, message, author_name, author_email,
229 author_date, diff, options):
230 """Create a new patch on the stack
0d2cd1e4 231 """
6ef533bc
CM
232 if not diff:
233 raise CmdException, 'No diff found inside the patch'
234
fff9bce5 235 if not patch:
99c52915
CM
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)
fff9bce5 245
95742cfc
PBG
246 # refresh_patch() will invoke the editor in this case, with correct
247 # patch content
9d15ccd8 248 if not message:
95742cfc 249 can_edit = False
9d15ccd8 250
99c52915
CM
251 committer_name = committer_email = None
252
253 if options.author:
254 options.authname, options.authemail = name_email(options.author)
255
0d2cd1e4
CM
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
95742cfc 268 crt_series.new_patch(patch, message = message, can_edit = False,
0d2cd1e4
CM
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
9417ece4 275 print 'Importing patch "%s"...' % patch,
0d2cd1e4
CM
276 sys.stdout.flush()
277
35344f86 278 if options.base:
6ef533bc 279 git.apply_patch(diff = diff, base = git_id(options.base))
35344f86 280 else:
6ef533bc 281 git.apply_patch(diff = diff)
35344f86 282
6ad48e48
PBG
283 crt_series.refresh_patch(edit = options.edit,
284 show_patch = options.showpatch)
0d2cd1e4 285
99c52915
CM
286 print 'done'
287
288def __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)
9417ece4
CM
312
313def __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
bcb6d890
CM
329 patchfile = os.path.join(patchdir, patch)
330
b0cdad5e
CM
331 if options.strip:
332 patch = __strip_patch_name(patch)
613a2f16 333 patch = __replace_slashes_with_dashes(patch);
9417ece4 334
99c52915
CM
335 __import_file(patch, patchfile, options)
336
337 if filename:
338 f.close()
339
340def __import_mbox(filename, options):
341 """Import a series from an mbox file
342 """
343 if filename:
344 f = file(filename, 'rb')
345 else:
457c3093 346 f = StringIO(sys.stdin.read())
99c52915
CM
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
457c3093 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()