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