chiark / gitweb /
4f5350e4bfc09f2983b6f96d69cce55633c9560e
[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
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     descr = authname = authemail = authdate = None
154
155     # parse the headers
156     while True:
157         line = f.readline()
158         if not line:
159             break
160         line = line.strip()
161         if re.match('from:\s+', line, re.I):
162             auth = __decode_header(re.findall('^.*?:\s+(.*)$', line)[0])
163             authname, authemail = name_email(auth)
164         elif re.match('date:\s+', line, re.I):
165             authdate = re.findall('^.*?:\s+(.*)$', line)[0]
166         elif re.match('subject:\s+', line, re.I):
167             descr = __decode_header(re.findall('^.*?:\s+(.*)$', line)[0])
168         elif line == '':
169             # end of headers
170             break
171
172     # remove the '[*PATCH*]' expression in the subject
173     if descr:
174         descr = re.findall('^(\[[^\s]*[Pp][Aa][Tt][Cc][Hh].*?\])?\s*(.*)$',
175                            descr)[0][1]
176         descr += '\n\n'
177     else:
178         raise CmdException, 'Subject: line not found'
179
180     # the rest of the patch description
181     while True:
182         line = f.readline()
183         if not line:
184             break
185         if __end_descr(line):
186             break
187         else:
188             descr += line
189     descr.rstrip()
190
191     if filename:
192         f.close()
193
194     # parse the description for author information
195     descr, descr_authname, descr_authemail, descr_authdate = __parse_description(descr)
196     if descr_authname:
197         authname = descr_authname
198     if descr_authemail:
199         authemail = descr_authemail
200     if descr_authdate:
201        authdate = descr_authdate
202
203     return (descr, authname, authemail, authdate)
204
205 def __parse_patch(filename = None):
206     """Parse the input file and return (description, authname,
207     authemail, authdate)
208     """
209     if filename:
210         f = file(filename)
211     else:
212         f = sys.stdin
213
214     descr = ''
215     while True:
216         line = f.readline()
217         if not line:
218             break
219
220         if __end_descr(line):
221             break
222         else:
223             descr += line
224     descr.rstrip()
225
226     if filename:
227         f.close()
228
229     descr, authname, authemail, authdate = __parse_description(descr)
230
231     # we don't yet have an agreed place for the creation date.
232     # Just return None
233     return (descr, authname, authemail, authdate)
234
235 def __import_patch(patch, filename, options):
236     """Import a patch from a file or standard input
237     """
238     # the defaults
239     message = author_name = author_email = author_date = committer_name = \
240               committer_email = None
241
242     if options.author:
243         options.authname, options.authemail = name_email(options.author)
244
245     if options.mail:
246         message, author_name, author_email, author_date = \
247                  __parse_mail(filename)
248     else:
249         message, author_name, author_email, author_date = \
250                  __parse_patch(filename)
251
252     if not patch:
253         patch = make_patch_name(message, crt_series.patch_exists)
254
255     # refresh_patch() will invoke the editor in this case, with correct
256     # patch content
257     if not message:
258         can_edit = False
259
260     # override the automatically parsed settings
261     if options.authname:
262         author_name = options.authname
263     if options.authemail:
264         author_email = options.authemail
265     if options.authdate:
266         author_date = options.authdate
267     if options.commname:
268         committer_name = options.commname
269     if options.commemail:
270         committer_email = options.commemail
271
272     if options.replace and patch in crt_series.get_unapplied():
273         crt_series.delete_patch(patch)
274
275     crt_series.new_patch(patch, message = message, can_edit = False,
276                          author_name = author_name,
277                          author_email = author_email,
278                          author_date = author_date,
279                          committer_name = committer_name,
280                          committer_email = committer_email)
281
282     print 'Importing patch "%s"...' % patch,
283     sys.stdout.flush()
284
285     if options.base:
286         git.apply_patch(filename, git_id(options.base))
287     else:
288         git.apply_patch(filename)
289
290     crt_series.refresh_patch(edit = options.edit,
291                              show_patch = options.showpatch)
292
293     print 'done'
294
295 def __import_series(filename, options):
296     """Import a series of patches
297     """
298     applied = crt_series.get_applied()
299
300     if filename:
301         f = file(filename)
302         patchdir = os.path.dirname(filename)
303     else:
304         f = sys.stdin
305         patchdir = ''
306
307     for line in f:
308         patch = re.sub('#.*$', '', line).strip()
309         if not patch:
310             continue
311         patchfile = os.path.join(patchdir, patch)
312
313         if options.strip:
314             patch = __strip_patch_name(patch)
315         patch = __replace_slashes_with_dashes(patch);
316         if options.ignore and patch in applied:
317             print 'Ignoring already applied patch "%s"' % patch
318             continue
319
320         __import_patch(patch, patchfile, options)
321
322 def func(parser, options, args):
323     """Import a GNU diff file as a new patch
324     """
325     if len(args) > 1:
326         parser.error('incorrect number of arguments')
327
328     check_local_changes()
329     check_conflicts()
330     check_head_top_equal()
331
332     if len(args) == 1:
333         filename = args[0]
334     else:
335         filename = None
336
337     if options.series:
338         __import_series(filename, options)
339     else:
340         if options.name:
341             patch = options.name
342         elif filename:
343             patch = os.path.basename(filename)
344         else:
345             patch = ''
346         if options.strip:
347             patch = __strip_patch_name(patch)
348
349         __import_patch(patch, filename, options)
350
351     print_crt_patch()