chiark / gitweb /
98fe7089537d34fd14cf71b9c6d09834b9a1f8ec
[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
205     # parse the description for author information
206     descr, descr_authname, descr_authemail, descr_authdate = \
207            __parse_description(descr)
208     if descr_authname:
209         authname = descr_authname
210     if descr_authemail:
211         authemail = descr_authemail
212     if descr_authdate:
213        authdate = descr_authdate
214
215     return (descr, authname, authemail, authdate, diff)
216
217 def __parse_patch(fobj):
218     """Parse the input file and return (description, authname,
219     authemail, authdate, diff)
220     """
221     descr, diff = __split_descr_diff(fobj.read())
222     descr, authname, authemail, authdate = __parse_description(descr)
223
224     # we don't yet have an agreed place for the creation date.
225     # Just return None
226     return (descr, authname, authemail, authdate, diff)
227
228 def __create_patch(filename, message, author_name, author_email,
229                    author_date, diff, options):
230     """Create a new patch on the stack
231     """
232     if options.name:
233         patch = options.name
234     elif filename:
235         patch = os.path.basename(filename)
236     else:
237         patch = ''
238     if options.strip:
239         patch = __strip_patch_name(patch)
240
241     if not patch:
242         if options.ignore or options.replace:
243             unacceptable_name = lambda name: False
244         else:
245             unacceptable_name = crt_series.patch_exists
246         patch = make_patch_name(message, unacceptable_name)
247     else:
248         # fix possible invalid characters in the patch name
249         patch = re.sub('[^\w.]+', '-', patch).strip('-')
250
251     if options.ignore and patch in crt_series.get_applied():
252         out.info('Ignoring already applied patch "%s"' % patch)
253         return
254     if options.replace and patch in crt_series.get_unapplied():
255         crt_series.delete_patch(patch)
256
257     # refresh_patch() will invoke the editor in this case, with correct
258     # patch content
259     if not message:
260         can_edit = False
261
262     committer_name = committer_email = None
263
264     if options.author:
265         options.authname, options.authemail = name_email(options.author)
266
267     # override the automatically parsed settings
268     if options.authname:
269         author_name = options.authname
270     if options.authemail:
271         author_email = options.authemail
272     if options.authdate:
273         author_date = options.authdate
274     if options.commname:
275         committer_name = options.commname
276     if options.commemail:
277         committer_email = options.commemail
278
279     crt_series.new_patch(patch, message = message, can_edit = False,
280                          author_name = author_name,
281                          author_email = author_email,
282                          author_date = author_date,
283                          committer_name = committer_name,
284                          committer_email = committer_email)
285
286     if not diff:
287         out.warn('No diff found, creating empty patch')
288     else:
289         out.start('Importing patch "%s"' % patch)
290         if options.base:
291             git.apply_patch(diff = diff, base = git_id(options.base))
292         else:
293             git.apply_patch(diff = diff)
294         crt_series.refresh_patch(edit = options.edit,
295                                  show_patch = options.showpatch)
296         out.done()
297
298 def __import_file(filename, options, patch = None):
299     """Import a patch from a file or standard input
300     """
301     if filename:
302         f = file(filename)
303     else:
304         f = sys.stdin
305
306     if options.mail:
307         try:
308             msg = email.message_from_file(f)
309         except Exception, ex:
310             raise CmdException, 'error parsing the e-mail file: %s' % str(ex)
311         message, author_name, author_email, author_date, diff = \
312                  __parse_mail(msg)
313     else:
314         message, author_name, author_email, author_date, diff = \
315                  __parse_patch(f)
316
317     if filename:
318         f.close()
319
320     if patch:
321         pname = patch
322     else:
323         pname = filename
324
325     __create_patch(pname, message, author_name, author_email,
326                    author_date, diff, options)
327
328 def __import_series(filename, options):
329     """Import a series of patches
330     """
331     applied = crt_series.get_applied()
332
333     if filename:
334         f = file(filename)
335         patchdir = os.path.dirname(filename)
336     else:
337         f = sys.stdin
338         patchdir = ''
339
340     for line in f:
341         patch = re.sub('#.*$', '', line).strip()
342         if not patch:
343             continue
344         patchfile = os.path.join(patchdir, patch)
345         patch = __replace_slashes_with_dashes(patch);
346
347         __import_file(patchfile, options, patch)
348
349     if filename:
350         f.close()
351
352 def __import_mbox(filename, options):
353     """Import a series from an mbox file
354     """
355     if filename:
356         f = file(filename, 'rb')
357     else:
358         f = StringIO(sys.stdin.read())
359
360     try:
361         mbox = UnixMailbox(f, email.message_from_file)
362     except Exception, ex:
363         raise CmdException, 'error parsing the mbox file: %s' % str(ex)
364
365     for msg in mbox:
366         message, author_name, author_email, author_date, diff = \
367                  __parse_mail(msg)
368         __create_patch(None, message, author_name, author_email,
369                        author_date, diff, options)
370
371     f.close()
372
373 def __import_url(url, options):
374     """Import a patch from a URL
375     """
376     import urllib
377     import tempfile
378
379     if not url:
380         parser.error('URL argument required')
381
382     patch = os.path.basename(urllib.unquote(url))
383     filename = os.path.join(tempfile.gettempdir(), patch)
384     urllib.urlretrieve(url, filename)
385     __import_file(filename, options)
386
387 def func(parser, options, args):
388     """Import a GNU diff file as a new patch
389     """
390     if len(args) > 1:
391         parser.error('incorrect number of arguments')
392
393     check_local_changes()
394     check_conflicts()
395     check_head_top_equal()
396
397     if len(args) == 1:
398         filename = args[0]
399     else:
400         filename = None
401
402     if options.series:
403         __import_series(filename, options)
404     elif options.mbox:
405         __import_mbox(filename, options)
406     elif options.url:
407         __import_url(filename, options)
408     else:
409         __import_file(filename, options)
410
411     print_crt_patch()