chiark / gitweb /
fad51363f373918032959379e66437201c45cf87
[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()