chiark / gitweb /
1b7dce8ad8802ef106c28c6dbfa41a3855eb7b2f
[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 __parse_description(descr):
89     """Parse the patch description and return the new description and
90     author information (if any).
91     """
92     subject = body = ''
93     authname = authemail = authdate = None
94
95     descr_lines = [line.rstrip() for line in  descr.split('\n')]
96     if not descr_lines:
97         raise CmdException, "Empty patch description"
98
99     lasthdr = 0
100     end = len(descr_lines)
101
102     # Parse the patch header
103     for pos in range(0, end):
104         if not descr_lines[pos]:
105            continue
106         # check for a "From|Author:" line
107         if re.match('\s*(?:from|author):\s+', descr_lines[pos], re.I):
108             auth = re.findall('^.*?:\s+(.*)$', descr_lines[pos])[0]
109             authname, authemail = name_email(auth)
110             lasthdr = pos + 1
111             continue
112         # check for a "Date:" line
113         if re.match('\s*date:\s+', descr_lines[pos], re.I):
114             authdate = re.findall('^.*?:\s+(.*)$', descr_lines[pos])[0]
115             lasthdr = pos + 1
116             continue
117         if subject:
118             break
119         # get the subject
120         subject = descr_lines[pos]
121         lasthdr = pos + 1
122
123     # get the body
124     if lasthdr < end:
125         body = reduce(lambda x, y: x + '\n' + y, descr_lines[lasthdr:], '')
126
127     return (subject + body, authname, authemail, authdate)
128
129 def __parse_mail(filename = None):
130     """Parse the input file in a mail format and return (description,
131     authname, authemail, authdate)
132     """
133     if filename:
134         f = file(filename)
135     else:
136         f = sys.stdin
137
138     descr = authname = authemail = authdate = None
139
140     # parse the headers
141     while True:
142         line = f.readline()
143         if not line:
144             break
145         line = line.strip()
146         if re.match('from:\s+', line, re.I):
147             auth = re.findall('^.*?:\s+(.*)$', line)[0]
148             authname, authemail = name_email(auth)
149         elif re.match('date:\s+', line, re.I):
150             authdate = re.findall('^.*?:\s+(.*)$', line)[0]
151         elif re.match('subject:\s+', line, re.I):
152             descr = re.findall('^.*?:\s+(.*)$', line)[0]
153         elif line == '':
154             # end of headers
155             break
156
157     # remove the '[*PATCH*]' expression in the subject
158     if descr:
159         descr = re.findall('^(\[[^\s]*[Pp][Aa][Tt][Cc][Hh].*?\])?\s*(.*)$',
160                            descr)[0][1]
161         descr += '\n\n'
162     else:
163         raise CmdException, 'Subject: line not found'
164
165     # the rest of the patch description
166     while True:
167         line = f.readline()
168         if not line:
169             break
170         if __end_descr(line):
171             break
172         else:
173             descr += line
174     descr.rstrip()
175
176     if filename:
177         f.close()
178
179     # parse the description for author information
180     descr, descr_authname, descr_authemail, descr_authdate = __parse_description(descr)
181     if descr_authname:
182         authname = descr_authname
183     if descr_authemail:
184         authemail = descr_authemail
185     if descr_authdate:
186        authdate = descr_authdate
187
188     return (descr, authname, authemail, authdate)
189
190 def __parse_patch(filename = None):
191     """Parse the input file and return (description, authname,
192     authemail, authdate)
193     """
194     if filename:
195         f = file(filename)
196     else:
197         f = sys.stdin
198
199     descr = ''
200     while True:
201         line = f.readline()
202         if not line:
203             break
204
205         if __end_descr(line):
206             break
207         else:
208             descr += line
209     descr.rstrip()
210
211     if filename:
212         f.close()
213
214     descr, authname, authemail, authdate = __parse_description(descr)
215
216     # we don't yet have an agreed place for the creation date.
217     # Just return None
218     return (descr, authname, authemail, authdate)
219
220 def __import_patch(patch, filename, options):
221     """Import a patch from a file or standard input
222     """
223     # the defaults
224     message = author_name = author_email = author_date = committer_name = \
225               committer_email = None
226
227     if options.author:
228         options.authname, options.authemail = name_email(options.author)
229
230     if options.mail:
231         message, author_name, author_email, author_date = \
232                  __parse_mail(filename)
233     else:
234         message, author_name, author_email, author_date = \
235                  __parse_patch(filename)
236
237     if not patch:
238         patch = make_patch_name(message)
239         if not patch:
240             raise CmdException, 'Unknown patch name'
241
242     # refresh_patch() will invoke the editor in this case, with correct
243     # patch content
244     if not message:
245         can_edit = False
246
247     # override the automatically parsed settings
248     if options.authname:
249         author_name = options.authname
250     if options.authemail:
251         author_email = options.authemail
252     if options.authdate:
253         author_date = options.authdate
254     if options.commname:
255         committer_name = options.commname
256     if options.commemail:
257         committer_email = options.commemail
258
259     if options.replace and patch in crt_series.get_unapplied():
260         crt_series.delete_patch(patch)
261
262     crt_series.new_patch(patch, message = message, can_edit = False,
263                          author_name = author_name,
264                          author_email = author_email,
265                          author_date = author_date,
266                          committer_name = committer_name,
267                          committer_email = committer_email)
268
269     print 'Importing patch "%s"...' % patch,
270     sys.stdout.flush()
271
272     if options.base:
273         git.apply_patch(filename, git_id(options.base))
274     else:
275         git.apply_patch(filename)
276
277     crt_series.refresh_patch(edit = options.edit,
278                              show_patch = options.showpatch)
279
280     print 'done'
281
282 def __import_series(filename, options):
283     """Import a series of patches
284     """
285     applied = crt_series.get_applied()
286
287     if filename:
288         f = file(filename)
289         patchdir = os.path.dirname(filename)
290     else:
291         f = sys.stdin
292         patchdir = ''
293
294     for line in f:
295         patch = re.sub('#.*$', '', line).strip()
296         if not patch:
297             continue
298         patchfile = os.path.join(patchdir, patch)
299
300         if options.strip:
301             patch = __strip_patch_name(patch)
302         if options.ignore and patch in applied:
303             print 'Ignoring already applied patch "%s"' % patch
304             continue
305
306         __import_patch(patch, patchfile, options)
307
308 def func(parser, options, args):
309     """Import a GNU diff file as a new patch
310     """
311     if len(args) > 1:
312         parser.error('incorrect number of arguments')
313
314     check_local_changes()
315     check_conflicts()
316     check_head_top_equal()
317
318     if len(args) == 1:
319         filename = args[0]
320     else:
321         filename = None
322
323     if options.series:
324         __import_series(filename, options)
325     else:
326         if options.name:
327             patch = options.name
328         elif filename:
329             patch = os.path.basename(filename)
330         else:
331             patch = ''
332         if options.strip:
333             patch = __strip_patch_name(patch)
334
335         __import_patch(patch, filename, options)
336
337     print_crt_patch()