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