chiark / gitweb /
Stgit: allow importing series files where patch names include slashes
[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)
244         if not patch:
245             raise CmdException, 'Unknown patch name'
246
247     # refresh_patch() will invoke the editor in this case, with correct
248     # patch content
249     if not message:
250         can_edit = False
251
252     # override the automatically parsed settings
253     if options.authname:
254         author_name = options.authname
255     if options.authemail:
256         author_email = options.authemail
257     if options.authdate:
258         author_date = options.authdate
259     if options.commname:
260         committer_name = options.commname
261     if options.commemail:
262         committer_email = options.commemail
263
264     if options.replace and patch in crt_series.get_unapplied():
265         crt_series.delete_patch(patch)
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(filename, git_id(options.base))
279     else:
280         git.apply_patch(filename)
281
282     crt_series.refresh_patch(edit = options.edit,
283                              show_patch = options.showpatch)
284
285     print 'done'
286
287 def __import_series(filename, options):
288     """Import a series of patches
289     """
290     applied = crt_series.get_applied()
291
292     if filename:
293         f = file(filename)
294         patchdir = os.path.dirname(filename)
295     else:
296         f = sys.stdin
297         patchdir = ''
298
299     for line in f:
300         patch = re.sub('#.*$', '', line).strip()
301         if not patch:
302             continue
303         patchfile = os.path.join(patchdir, patch)
304
305         if options.strip:
306             patch = __strip_patch_name(patch)
307         patch = __replace_slashes_with_dashes(patch);
308         if options.ignore and patch in applied:
309             print 'Ignoring already applied patch "%s"' % patch
310             continue
311
312         __import_patch(patch, patchfile, options)
313
314 def func(parser, options, args):
315     """Import a GNU diff file as a new patch
316     """
317     if len(args) > 1:
318         parser.error('incorrect number of arguments')
319
320     check_local_changes()
321     check_conflicts()
322     check_head_top_equal()
323
324     if len(args) == 1:
325         filename = args[0]
326     else:
327         filename = None
328
329     if options.series:
330         __import_series(filename, options)
331     else:
332         if options.name:
333             patch = options.name
334         elif filename:
335             patch = os.path.basename(filename)
336         else:
337             patch = ''
338         if options.strip:
339             patch = __strip_patch_name(patch)
340
341         __import_patch(patch, filename, options)
342
343     print_crt_patch()