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