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