chiark / gitweb /
f972b8933de3ca85d701d7a43c7e3cb043381ddf
[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>|<url>]
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('-u', '--url',
57                        help = 'import a patch from a URL',
58                        action = 'store_true'),
59            make_option('-n', '--name',
60                        help = 'use NAME as the patch name'),
61            make_option('-t', '--strip',
62                        help = 'strip numbering and extension from patch name',
63                        action = 'store_true'),
64            make_option('-i', '--ignore',
65                        help = 'ignore the applied patches in the series',
66                        action = 'store_true'),
67            make_option('--replace',
68                        help = 'replace the unapplied patches in the series',
69                        action = 'store_true'),
70            make_option('-b', '--base',
71                        help = 'use BASE instead of HEAD for file importing'),
72            make_option('-e', '--edit',
73                        help = 'invoke an editor for the patch description',
74                        action = 'store_true'),
75            make_option('-p', '--showpatch',
76                        help = 'show the patch content in the editor buffer',
77                        action = 'store_true'),
78            make_option('-a', '--author', metavar = '"NAME <EMAIL>"',
79                        help = 'use "NAME <EMAIL>" as the author details'),
80            make_option('--authname',
81                        help = 'use AUTHNAME as the author name'),
82            make_option('--authemail',
83                        help = 'use AUTHEMAIL as the author e-mail'),
84            make_option('--authdate',
85                        help = 'use AUTHDATE as the author date'),
86            make_option('--commname',
87                        help = 'use COMMNAME as the committer name'),
88            make_option('--commemail',
89                        help = 'use COMMEMAIL as the committer e-mail')]
90
91
92 def __end_descr(line):
93     return re.match('---\s*$', line) or re.match('diff -', line) or \
94             re.match('Index: ', line)
95
96 def __strip_patch_name(name):
97     stripped = re.sub('^[0-9]+-(.*)$', '\g<1>', name)
98     stripped = re.sub('^(.*)\.(diff|patch)$', '\g<1>', stripped)
99
100     return stripped
101
102 def __replace_slashes_with_dashes(name):
103     stripped = name.replace('/', '-')
104
105     return stripped
106
107 def __split_descr_diff(string):
108     """Return the description and the diff from the given string
109     """
110     descr = diff = ''
111     top = True
112
113     for line in string.split('\n'):
114         if top:
115             if not __end_descr(line):
116                 descr += line + '\n'
117                 continue
118             else:
119                 top = False
120         diff += line + '\n'
121
122     return (descr.rstrip(), diff)
123
124 def __parse_description(descr):
125     """Parse the patch description and return the new description and
126     author information (if any).
127     """
128     subject = body = ''
129     authname = authemail = authdate = None
130
131     descr_lines = [line.rstrip() for line in  descr.split('\n')]
132     if not descr_lines:
133         raise CmdException, "Empty patch description"
134
135     lasthdr = 0
136     end = len(descr_lines)
137
138     # Parse the patch header
139     for pos in range(0, end):
140         if not descr_lines[pos]:
141            continue
142         # check for a "From|Author:" line
143         if re.match('\s*(?:from|author):\s+', descr_lines[pos], re.I):
144             auth = re.findall('^.*?:\s+(.*)$', descr_lines[pos])[0]
145             authname, authemail = name_email(auth)
146             lasthdr = pos + 1
147             continue
148         # check for a "Date:" line
149         if re.match('\s*date:\s+', descr_lines[pos], re.I):
150             authdate = re.findall('^.*?:\s+(.*)$', descr_lines[pos])[0]
151             lasthdr = pos + 1
152             continue
153         if subject:
154             break
155         # get the subject
156         subject = descr_lines[pos]
157         lasthdr = pos + 1
158
159     # get the body
160     if lasthdr < end:
161         body = reduce(lambda x, y: x + '\n' + y, descr_lines[lasthdr:], '')
162
163     return (subject + body, authname, authemail, authdate)
164
165 def __parse_mail(msg):
166     """Parse the message object and return (description, authname,
167     authemail, authdate, diff)
168     """
169     def __decode_header(header):
170         """Decode a qp-encoded e-mail header as per rfc2047"""
171         try:
172             words_enc = decode_header(header)
173             hobj = make_header(words_enc)
174         except Exception, ex:
175             raise CmdException, 'header decoding error: %s' % str(ex)
176         return unicode(hobj).encode('utf-8')
177
178     # parse the headers
179     if msg.has_key('from'):
180         authname, authemail = name_email(__decode_header(msg['from']))
181     else:
182         authname = authemail = None
183
184     # '\n\t' can be found on multi-line headers
185     descr = __decode_header(msg['subject']).replace('\n\t', ' ')
186     authdate = msg['date']
187
188     # remove the '[*PATCH*]' expression in the subject
189     if descr:
190         descr = re.findall('^(\[.*?[Pp][Aa][Tt][Cc][Hh].*?\])?\s*(.*)$',
191                            descr)[0][1]
192     else:
193         raise CmdException, 'Subject: line not found'
194
195     # the rest of the message
196     msg_text = ''
197     for part in msg.walk():
198         if part.get_content_type() == 'text/plain':
199             msg_text += part.get_payload(decode = True)
200
201     rem_descr, diff = __split_descr_diff(msg_text)
202     if rem_descr:
203         descr += '\n\n' + rem_descr
204     if not diff:
205         out.warn('Message does not contain any diff')
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 not diff:
254         raise CmdException, 'No diff found inside the patch'
255
256     if options.ignore and patch in crt_series.get_applied():
257         out.info('Ignoring already applied patch "%s"' % patch)
258         return
259     if options.replace and patch in crt_series.get_unapplied():
260         crt_series.delete_patch(patch)
261
262     # refresh_patch() will invoke the editor in this case, with correct
263     # patch content
264     if not message:
265         can_edit = False
266
267     committer_name = committer_email = None
268
269     if options.author:
270         options.authname, options.authemail = name_email(options.author)
271
272     # override the automatically parsed settings
273     if options.authname:
274         author_name = options.authname
275     if options.authemail:
276         author_email = options.authemail
277     if options.authdate:
278         author_date = options.authdate
279     if options.commname:
280         committer_name = options.commname
281     if options.commemail:
282         committer_email = options.commemail
283
284     crt_series.new_patch(patch, message = message, can_edit = False,
285                          author_name = author_name,
286                          author_email = author_email,
287                          author_date = author_date,
288                          committer_name = committer_name,
289                          committer_email = committer_email)
290
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     out.done()
299
300 def __import_file(filename, options, patch = None):
301     """Import a patch from a file or standard input
302     """
303     if filename:
304         f = file(filename)
305     else:
306         f = sys.stdin
307
308     if options.mail:
309         try:
310             msg = email.message_from_file(f)
311         except Exception, ex:
312             raise CmdException, 'error parsing the e-mail file: %s' % str(ex)
313         message, author_name, author_email, author_date, diff = \
314                  __parse_mail(msg)
315     else:
316         message, author_name, author_email, author_date, diff = \
317                  __parse_patch(f)
318
319     if filename:
320         f.close()
321
322     if patch:
323         pname = patch
324     else:
325         pname = filename
326
327     __create_patch(pname, message, author_name, author_email,
328                    author_date, diff, options)
329
330 def __import_series(filename, options):
331     """Import a series of patches
332     """
333     applied = crt_series.get_applied()
334
335     if filename:
336         f = file(filename)
337         patchdir = os.path.dirname(filename)
338     else:
339         f = sys.stdin
340         patchdir = ''
341
342     for line in f:
343         patch = re.sub('#.*$', '', line).strip()
344         if not patch:
345             continue
346         patchfile = os.path.join(patchdir, patch)
347         patch = __replace_slashes_with_dashes(patch);
348
349         __import_file(patchfile, options, patch)
350
351     if filename:
352         f.close()
353
354 def __import_mbox(filename, options):
355     """Import a series from an mbox file
356     """
357     if filename:
358         f = file(filename, 'rb')
359     else:
360         f = StringIO(sys.stdin.read())
361
362     try:
363         mbox = UnixMailbox(f, email.message_from_file)
364     except Exception, ex:
365         raise CmdException, 'error parsing the mbox file: %s' % str(ex)
366
367     for msg in mbox:
368         message, author_name, author_email, author_date, diff = \
369                  __parse_mail(msg)
370         __create_patch(None, message, author_name, author_email,
371                        author_date, diff, options)
372
373     f.close()
374
375 def __import_url(url, options):
376     """Import a patch from a URL
377     """
378     import urllib
379     import tempfile
380
381     if not url:
382         parser.error('URL argument required')
383
384     patch = os.path.basename(urllib.unquote(url))
385     filename = os.path.join(tempfile.gettempdir(), patch)
386     urllib.urlretrieve(url, filename)
387     __import_file(filename, options)
388
389 def func(parser, options, args):
390     """Import a GNU diff file as a new patch
391     """
392     if len(args) > 1:
393         parser.error('incorrect number of arguments')
394
395     check_local_changes()
396     check_conflicts()
397     check_head_top_equal()
398
399     if len(args) == 1:
400         filename = args[0]
401     else:
402         filename = None
403
404     if options.series:
405         __import_series(filename, options)
406     elif options.mbox:
407         __import_mbox(filename, options)
408     elif options.url:
409         __import_url(filename, options)
410     else:
411         __import_file(filename, options)
412
413     print_crt_patch()