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