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