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