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