chiark / gitweb /
Add --sign and --ack options to "stg import"
[stgit] / stgit / commands / imprt.py
CommitLineData
0d2cd1e4
CM
1__copyright__ = """
2Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
3
4This program is free software; you can redistribute it and/or modify
5it under the terms of the GNU General Public License version 2 as
6published by the Free Software Foundation.
7
8This program is distributed in the hope that it will be useful,
9but WITHOUT ANY WARRANTY; without even the implied warranty of
10MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11GNU General Public License for more details.
12
13You should have received a copy of the GNU General Public License
14along with this program; if not, write to the Free Software
15Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
16"""
17
6ef533bc 18import sys, os, re, email
2ac5a14c 19from email.Header import decode_header, make_header
99c52915 20from mailbox import UnixMailbox
457c3093 21from StringIO import StringIO
0d2cd1e4
CM
22from optparse import OptionParser, make_option
23
24from stgit.commands.common import *
25from stgit.utils import *
5e888f30 26from stgit.out import *
0d2cd1e4
CM
27from stgit import stack, git
28
29
30help = 'import a GNU diff file as a new patch'
575c575e 31usage = """%prog [options] [<file>|<url>]
0d2cd1e4 32
b8a0986f
CM
33Create a new patch and apply the given GNU diff file (or the standard
34input). By default, the file name is used as the patch name but this
388f63b6 35can be overridden with the '--name' option. The patch can either be a
b8a0986f
CM
36normal file with the description at the top or it can have standard
37mail format, the Subject, From and Date headers being used for
99c52915
CM
38generating the patch information. The command can also read series and
39mbox files.
40
41If a patch does not apply cleanly, the failed diff is written to the
42.stgit-failed.patch file and an empty StGIT patch is added to the
43stack.
0d2cd1e4 44
b8a0986f 45The patch description has to be separated from the data with a '---'
99e73103 46line."""
0d2cd1e4
CM
47
48options = [make_option('-m', '--mail',
49 help = 'import the patch from a standard e-mail file',
50 action = 'store_true'),
99c52915
CM
51 make_option('-M', '--mbox',
52 help = 'import a series of patches from an mbox file',
53 action = 'store_true'),
54 make_option('-s', '--series',
55 help = 'import a series of patches',
56 action = 'store_true'),
575c575e
CW
57 make_option('-u', '--url',
58 help = 'import a patch from a URL',
59 action = 'store_true'),
0d2cd1e4
CM
60 make_option('-n', '--name',
61 help = 'use NAME as the patch name'),
b0cdad5e
CM
62 make_option('-t', '--strip',
63 help = 'strip numbering and extension from patch name',
64 action = 'store_true'),
9417ece4
CM
65 make_option('-i', '--ignore',
66 help = 'ignore the applied patches in the series',
67 action = 'store_true'),
034db15c
CM
68 make_option('--replace',
69 help = 'replace the unapplied patches in the series',
70 action = 'store_true'),
b21bc8d1 71 make_option('-b', '--base',
35344f86 72 help = 'use BASE instead of HEAD for file importing'),
33e580e0
CM
73 make_option('-e', '--edit',
74 help = 'invoke an editor for the patch description',
75 action = 'store_true'),
9417ece4 76 make_option('-p', '--showpatch',
6ad48e48
PBG
77 help = 'show the patch content in the editor buffer',
78 action = 'store_true'),
0d2cd1e4
CM
79 make_option('-a', '--author', metavar = '"NAME <EMAIL>"',
80 help = 'use "NAME <EMAIL>" as the author details'),
81 make_option('--authname',
82 help = 'use AUTHNAME as the author name'),
83 make_option('--authemail',
84 help = 'use AUTHEMAIL as the author e-mail'),
85 make_option('--authdate',
86 help = 'use AUTHDATE as the author date'),
87 make_option('--commname',
88 help = 'use COMMNAME as the committer name'),
89 make_option('--commemail',
130df01a
KH
90 help = 'use COMMEMAIL as the committer e-mail')
91 ] + make_sign_options()
0d2cd1e4
CM
92
93
d4c43e19
PBG
94def __end_descr(line):
95 return re.match('---\s*$', line) or re.match('diff -', line) or \
96 re.match('Index: ', line)
99e73103 97
b0cdad5e 98def __strip_patch_name(name):
bcb6d890
CM
99 stripped = re.sub('^[0-9]+-(.*)$', '\g<1>', name)
100 stripped = re.sub('^(.*)\.(diff|patch)$', '\g<1>', stripped)
101
102 return stripped
b0cdad5e 103
613a2f16
PBG
104def __replace_slashes_with_dashes(name):
105 stripped = name.replace('/', '-')
106
107 return stripped
108
f21ba536
CM
109def __split_descr_diff(string):
110 """Return the description and the diff from the given string
111 """
112 descr = diff = ''
113 top = True
114
115 for line in string.split('\n'):
116 if top:
117 if not __end_descr(line):
118 descr += line + '\n'
119 continue
120 else:
121 top = False
122 diff += line + '\n'
123
124 return (descr.rstrip(), diff)
125
99e73103
CM
126def __parse_description(descr):
127 """Parse the patch description and return the new description and
128 author information (if any).
129 """
130 subject = body = ''
0543bc5f 131 authname = authemail = authdate = None
99e73103 132
0543bc5f 133 descr_lines = [line.rstrip() for line in descr.split('\n')]
99e73103
CM
134 if not descr_lines:
135 raise CmdException, "Empty patch description"
136
0543bc5f 137 lasthdr = 0
99e73103
CM
138 end = len(descr_lines)
139
0543bc5f 140 # Parse the patch header
61dabd0e 141 for pos in range(0, end):
0543bc5f
TM
142 if not descr_lines[pos]:
143 continue
144 # check for a "From|Author:" line
145 if re.match('\s*(?:from|author):\s+', descr_lines[pos], re.I):
146 auth = re.findall('^.*?:\s+(.*)$', descr_lines[pos])[0]
147 authname, authemail = name_email(auth)
148 lasthdr = pos + 1
149 continue
150 # check for a "Date:" line
151 if re.match('\s*date:\s+', descr_lines[pos], re.I):
152 authdate = re.findall('^.*?:\s+(.*)$', descr_lines[pos])[0]
153 lasthdr = pos + 1
154 continue
155 if subject:
156 break
157 # get the subject
158 subject = descr_lines[pos]
159 lasthdr = pos + 1
99e73103
CM
160
161 # get the body
0543bc5f
TM
162 if lasthdr < end:
163 body = reduce(lambda x, y: x + '\n' + y, descr_lines[lasthdr:], '')
99e73103 164
0543bc5f 165 return (subject + body, authname, authemail, authdate)
99e73103 166
99c52915
CM
167def __parse_mail(msg):
168 """Parse the message object and return (description, authname,
169 authemail, authdate, diff)
0d2cd1e4 170 """
2ac5a14c
CM
171 def __decode_header(header):
172 """Decode a qp-encoded e-mail header as per rfc2047"""
173 try:
174 words_enc = decode_header(header)
175 hobj = make_header(words_enc)
176 except Exception, ex:
177 raise CmdException, 'header decoding error: %s' % str(ex)
178 return unicode(hobj).encode('utf-8')
179
0d2cd1e4 180 # parse the headers
6ef533bc
CM
181 if msg.has_key('from'):
182 authname, authemail = name_email(__decode_header(msg['from']))
183 else:
184 authname = authemail = None
185
99c52915
CM
186 # '\n\t' can be found on multi-line headers
187 descr = __decode_header(msg['subject']).replace('\n\t', ' ')
6ef533bc 188 authdate = msg['date']
0d2cd1e4 189
186e6b6b 190 # remove the '[*PATCH*]' expression in the subject
0d2cd1e4 191 if descr:
dfeeba67 192 descr = re.findall('^(\[.*?[Pp][Aa][Tt][Cc][Hh].*?\])?\s*(.*)$',
7c02f338 193 descr)[0][1]
0d2cd1e4
CM
194 else:
195 raise CmdException, 'Subject: line not found'
196
6ef533bc 197 # the rest of the message
f21ba536
CM
198 msg_text = ''
199 for part in msg.walk():
200 if part.get_content_type() == 'text/plain':
201 msg_text += part.get_payload(decode = True)
6ef533bc 202
f21ba536
CM
203 rem_descr, diff = __split_descr_diff(msg_text)
204 if rem_descr:
205 descr += '\n\n' + rem_descr
0d2cd1e4 206
99e73103 207 # parse the description for author information
6ef533bc
CM
208 descr, descr_authname, descr_authemail, descr_authdate = \
209 __parse_description(descr)
99e73103
CM
210 if descr_authname:
211 authname = descr_authname
212 if descr_authemail:
213 authemail = descr_authemail
0543bc5f
TM
214 if descr_authdate:
215 authdate = descr_authdate
99e73103 216
6ef533bc 217 return (descr, authname, authemail, authdate, diff)
0d2cd1e4 218
99c52915 219def __parse_patch(fobj):
0d2cd1e4 220 """Parse the input file and return (description, authname,
99c52915 221 authemail, authdate, diff)
0d2cd1e4 222 """
f21ba536 223 descr, diff = __split_descr_diff(fobj.read())
0543bc5f 224 descr, authname, authemail, authdate = __parse_description(descr)
99e73103
CM
225
226 # we don't yet have an agreed place for the creation date.
227 # Just return None
6ef533bc 228 return (descr, authname, authemail, authdate, diff)
0d2cd1e4 229
fd1c0cfc 230def __create_patch(filename, message, author_name, author_email,
99c52915
CM
231 author_date, diff, options):
232 """Create a new patch on the stack
0d2cd1e4 233 """
fd1c0cfc
CM
234 if options.name:
235 patch = options.name
236 elif filename:
237 patch = os.path.basename(filename)
238 else:
239 patch = ''
240 if options.strip:
241 patch = __strip_patch_name(patch)
6ef533bc 242
fff9bce5 243 if not patch:
c4f99b6c
KH
244 if options.ignore or options.replace:
245 unacceptable_name = lambda name: False
246 else:
247 unacceptable_name = crt_series.patch_exists
248 patch = make_patch_name(message, unacceptable_name)
fd1c0cfc
CM
249 else:
250 # fix possible invalid characters in the patch name
251 patch = re.sub('[^\w.]+', '-', patch).strip('-')
252
99c52915 253 if options.ignore and patch in crt_series.get_applied():
27ac2b7e 254 out.info('Ignoring already applied patch "%s"' % patch)
99c52915
CM
255 return
256 if options.replace and patch in crt_series.get_unapplied():
257 crt_series.delete_patch(patch)
fff9bce5 258
95742cfc
PBG
259 # refresh_patch() will invoke the editor in this case, with correct
260 # patch content
9d15ccd8 261 if not message:
95742cfc 262 can_edit = False
9d15ccd8 263
99c52915
CM
264 committer_name = committer_email = None
265
266 if options.author:
267 options.authname, options.authemail = name_email(options.author)
268
0d2cd1e4
CM
269 # override the automatically parsed settings
270 if options.authname:
271 author_name = options.authname
272 if options.authemail:
273 author_email = options.authemail
274 if options.authdate:
275 author_date = options.authdate
276 if options.commname:
277 committer_name = options.commname
278 if options.commemail:
279 committer_email = options.commemail
280
95742cfc 281 crt_series.new_patch(patch, message = message, can_edit = False,
0d2cd1e4
CM
282 author_name = author_name,
283 author_email = author_email,
284 author_date = author_date,
285 committer_name = committer_name,
286 committer_email = committer_email)
287
5f1629be
CM
288 if not diff:
289 out.warn('No diff found, creating empty patch')
35344f86 290 else:
5f1629be
CM
291 out.start('Importing patch "%s"' % patch)
292 if options.base:
293 git.apply_patch(diff = diff, base = git_id(options.base))
294 else:
295 git.apply_patch(diff = diff)
296 crt_series.refresh_patch(edit = options.edit,
130df01a
KH
297 show_patch = options.showpatch,
298 sign_str = options.sign_str)
5f1629be 299 out.done()
99c52915 300
fd1c0cfc 301def __import_file(filename, options, patch = None):
99c52915
CM
302 """Import a patch from a file or standard input
303 """
304 if filename:
305 f = file(filename)
306 else:
307 f = sys.stdin
308
309 if options.mail:
310 try:
311 msg = email.message_from_file(f)
312 except Exception, ex:
313 raise CmdException, 'error parsing the e-mail file: %s' % str(ex)
314 message, author_name, author_email, author_date, diff = \
315 __parse_mail(msg)
316 else:
317 message, author_name, author_email, author_date, diff = \
318 __parse_patch(f)
319
320 if filename:
321 f.close()
322
fd1c0cfc
CM
323 if patch:
324 pname = patch
325 else:
326 pname = filename
327
328 __create_patch(pname, message, author_name, author_email,
99c52915 329 author_date, diff, options)
9417ece4
CM
330
331def __import_series(filename, options):
332 """Import a series of patches
333 """
334 applied = crt_series.get_applied()
335
336 if filename:
337 f = file(filename)
338 patchdir = os.path.dirname(filename)
339 else:
340 f = sys.stdin
341 patchdir = ''
342
343 for line in f:
344 patch = re.sub('#.*$', '', line).strip()
345 if not patch:
346 continue
bcb6d890 347 patchfile = os.path.join(patchdir, patch)
613a2f16 348 patch = __replace_slashes_with_dashes(patch);
9417ece4 349
fd1c0cfc 350 __import_file(patchfile, options, patch)
99c52915
CM
351
352 if filename:
353 f.close()
354
355def __import_mbox(filename, options):
356 """Import a series from an mbox file
357 """
358 if filename:
359 f = file(filename, 'rb')
360 else:
457c3093 361 f = StringIO(sys.stdin.read())
99c52915
CM
362
363 try:
364 mbox = UnixMailbox(f, email.message_from_file)
365 except Exception, ex:
366 raise CmdException, 'error parsing the mbox file: %s' % str(ex)
367
368 for msg in mbox:
369 message, author_name, author_email, author_date, diff = \
370 __parse_mail(msg)
371 __create_patch(None, message, author_name, author_email,
372 author_date, diff, options)
373
457c3093 374 f.close()
9417ece4 375
575c575e
CW
376def __import_url(url, options):
377 """Import a patch from a URL
378 """
379 import urllib
380 import tempfile
381
382 if not url:
383 parser.error('URL argument required')
384
fd1c0cfc
CM
385 patch = os.path.basename(urllib.unquote(url))
386 filename = os.path.join(tempfile.gettempdir(), patch)
387 urllib.urlretrieve(url, filename)
388 __import_file(filename, options)
575c575e 389
9417ece4
CM
390def func(parser, options, args):
391 """Import a GNU diff file as a new patch
392 """
393 if len(args) > 1:
394 parser.error('incorrect number of arguments')
395
396 check_local_changes()
397 check_conflicts()
398 check_head_top_equal()
399
400 if len(args) == 1:
401 filename = args[0]
402 else:
403 filename = None
404
405 if options.series:
406 __import_series(filename, options)
99c52915
CM
407 elif options.mbox:
408 __import_mbox(filename, options)
575c575e
CW
409 elif options.url:
410 __import_url(filename, options)
9417ece4 411 else:
fd1c0cfc 412 __import_file(filename, options)
9417ece4 413
0d2cd1e4 414 print_crt_patch()