Autosign imported patches
[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
972de1db 18import sys, os, re, email, tarfile
99c52915 19from mailbox import UnixMailbox
457c3093 20from StringIO import StringIO
575bbdae 21from stgit.argparse import opt
0d2cd1e4
CM
22from stgit.commands.common import *
23from stgit.utils import *
5e888f30 24from stgit.out import *
20a52e06 25from stgit import argparse, stack, git
0d2cd1e4 26
575bbdae
KH
27name = 'import'
28help = 'Import a GNU diff file as a new patch'
33ff9cdd 29kind = 'patch'
575bbdae
KH
30usage = ['[options] [<file>|<url>]']
31description = """
b8a0986f
CM
32Create a new patch and apply the given GNU diff file (or the standard
33input). By default, the file name is used as the patch name but this
388f63b6 34can be overridden with the '--name' option. The patch can either be a
b8a0986f
CM
35normal file with the description at the top or it can have standard
36mail format, the Subject, From and Date headers being used for
99c52915
CM
37generating the patch information. The command can also read series and
38mbox files.
39
40If a patch does not apply cleanly, the failed diff is written to the
41.stgit-failed.patch file and an empty StGIT patch is added to the
42stack.
0d2cd1e4 43
b8a0986f 44The patch description has to be separated from the data with a '---'
99e73103 45line."""
0d2cd1e4 46
6c8a90e1 47args = [argparse.files]
575bbdae
KH
48options = [
49 opt('-m', '--mail', action = 'store_true',
50 short = 'Import the patch from a standard e-mail file'),
51 opt('-M', '--mbox', action = 'store_true',
52 short = 'Import a series of patches from an mbox file'),
53 opt('-s', '--series', action = 'store_true',
972de1db
CW
54 short = 'Import a series of patches', long = """
55 Import a series of patches from a series file or a tar archive."""),
575bbdae
KH
56 opt('-u', '--url', action = 'store_true',
57 short = 'Import a patch from a URL'),
58 opt('-n', '--name',
59 short = 'Use NAME as the patch name'),
c18842cc
CM
60 opt('-p', '--strip', type = 'int', metavar = 'N',
61 short = 'Remove N leading slashes from diff paths (default 1)'),
62 opt('-t', '--stripname', action = 'store_true',
575bbdae
KH
63 short = 'Strip numbering and extension from patch name'),
64 opt('-i', '--ignore', action = 'store_true',
65 short = 'Ignore the applied patches in the series'),
66 opt('--replace', action = 'store_true',
67 short = 'Replace the unapplied patches in the series'),
6c8a90e1 68 opt('-b', '--base', args = [argparse.commit],
575bbdae 69 short = 'Use BASE instead of HEAD for file importing'),
fc8dcca7 70 opt('--reject', action = 'store_true',
46cc1037 71 short = 'Leave the rejected hunks in corresponding *.rej files'),
575bbdae
KH
72 opt('-e', '--edit', action = 'store_true',
73 short = 'Invoke an editor for the patch description'),
c18842cc 74 opt('-d', '--showdiff', action = 'store_true',
575bbdae
KH
75 short = 'Show the patch content in the editor buffer'),
76 opt('-a', '--author', metavar = '"NAME <EMAIL>"',
77 short = 'Use "NAME <EMAIL>" as the author details'),
78 opt('--authname',
79 short = 'Use AUTHNAME as the author name'),
80 opt('--authemail',
81 short = 'Use AUTHEMAIL as the author e-mail'),
82 opt('--authdate',
83 short = 'Use AUTHDATE as the author date'),
575bbdae 84 ] + argparse.sign_options()
0d2cd1e4 85
117ed129 86directory = DirectoryHasRepository(log = True)
0d2cd1e4 87
b0cdad5e 88def __strip_patch_name(name):
bcb6d890
CM
89 stripped = re.sub('^[0-9]+-(.*)$', '\g<1>', name)
90 stripped = re.sub('^(.*)\.(diff|patch)$', '\g<1>', stripped)
91
92 return stripped
b0cdad5e 93
613a2f16
PBG
94def __replace_slashes_with_dashes(name):
95 stripped = name.replace('/', '-')
96
97 return stripped
98
fd1c0cfc 99def __create_patch(filename, message, author_name, author_email,
99c52915
CM
100 author_date, diff, options):
101 """Create a new patch on the stack
0d2cd1e4 102 """
fd1c0cfc
CM
103 if options.name:
104 patch = options.name
105 elif filename:
106 patch = os.path.basename(filename)
107 else:
108 patch = ''
c18842cc 109 if options.stripname:
fd1c0cfc 110 patch = __strip_patch_name(patch)
6ef533bc 111
fff9bce5 112 if not patch:
c4f99b6c
KH
113 if options.ignore or options.replace:
114 unacceptable_name = lambda name: False
115 else:
116 unacceptable_name = crt_series.patch_exists
117 patch = make_patch_name(message, unacceptable_name)
fd1c0cfc
CM
118 else:
119 # fix possible invalid characters in the patch name
120 patch = re.sub('[^\w.]+', '-', patch).strip('-')
121
99c52915 122 if options.ignore and patch in crt_series.get_applied():
27ac2b7e 123 out.info('Ignoring already applied patch "%s"' % patch)
99c52915
CM
124 return
125 if options.replace and patch in crt_series.get_unapplied():
c26ca1b2 126 crt_series.delete_patch(patch, keep_log = True)
fff9bce5 127
95742cfc
PBG
128 # refresh_patch() will invoke the editor in this case, with correct
129 # patch content
9d15ccd8 130 if not message:
95742cfc 131 can_edit = False
9d15ccd8 132
99c52915
CM
133 if options.author:
134 options.authname, options.authemail = name_email(options.author)
135
0d2cd1e4
CM
136 # override the automatically parsed settings
137 if options.authname:
138 author_name = options.authname
139 if options.authemail:
140 author_email = options.authemail
141 if options.authdate:
142 author_date = options.authdate
0d2cd1e4 143
e929f0ac
CM
144 sign_str = options.sign_str
145 if not options.sign_str:
146 sign_str = config.get('stgit.autosign')
147
95742cfc 148 crt_series.new_patch(patch, message = message, can_edit = False,
0d2cd1e4
CM
149 author_name = author_name,
150 author_email = author_email,
e929f0ac 151 author_date = author_date, sign_str = sign_str)
0d2cd1e4 152
5f1629be
CM
153 if not diff:
154 out.warn('No diff found, creating empty patch')
35344f86 155 else:
5f1629be
CM
156 out.start('Importing patch "%s"' % patch)
157 if options.base:
fc8dcca7 158 base = git_id(crt_series, options.base)
5f1629be 159 else:
fc8dcca7 160 base = None
46cc1037
CM
161 try:
162 git.apply_patch(diff = diff, base = base, reject = options.reject,
163 strip = options.strip)
164 except git.GitException:
165 if not options.reject:
166 crt_series.delete_patch(patch)
167 raise
5f1629be 168 crt_series.refresh_patch(edit = options.edit,
c18842cc 169 show_patch = options.showdiff,
24a7f944 170 author_date = author_date,
cb688601 171 backup = False)
5f1629be 172 out.done()
99c52915 173
354d2031
CW
174def __mkpatchname(name, suffix):
175 if name.lower().endswith(suffix.lower()):
176 return name[:-len(suffix)]
177 return name
178
179def __get_handle_and_name(filename):
180 """Return a file object and a patch name derived from filename
181 """
182 # see if it's a gzip'ed or bzip2'ed patch
183 import bz2, gzip
184 for copen, ext in [(gzip.open, '.gz'), (bz2.BZ2File, '.bz2')]:
185 try:
186 f = copen(filename)
187 f.read(1)
188 f.seek(0)
189 return (f, __mkpatchname(filename, ext))
190 except IOError, e:
191 pass
192
193 # plain old file...
194 return (open(filename), filename)
195
fd1c0cfc 196def __import_file(filename, options, patch = None):
99c52915
CM
197 """Import a patch from a file or standard input
198 """
354d2031 199 pname = None
99c52915 200 if filename:
354d2031 201 (f, pname) = __get_handle_and_name(filename)
99c52915
CM
202 else:
203 f = sys.stdin
204
354d2031
CW
205 if patch:
206 pname = patch
207 elif not pname:
208 pname = filename
209
99c52915
CM
210 if options.mail:
211 try:
212 msg = email.message_from_file(f)
213 except Exception, ex:
214 raise CmdException, 'error parsing the e-mail file: %s' % str(ex)
215 message, author_name, author_email, author_date, diff = \
ed60fdae 216 parse_mail(msg)
99c52915
CM
217 else:
218 message, author_name, author_email, author_date, diff = \
ef954fe6 219 parse_patch(f.read(), contains_diff = True)
99c52915
CM
220
221 if filename:
222 f.close()
223
fd1c0cfc 224 __create_patch(pname, message, author_name, author_email,
99c52915 225 author_date, diff, options)
9417ece4
CM
226
227def __import_series(filename, options):
228 """Import a series of patches
229 """
230 applied = crt_series.get_applied()
231
232 if filename:
972de1db
CW
233 if tarfile.is_tarfile(filename):
234 __import_tarfile(filename, options)
235 return
9417ece4
CM
236 f = file(filename)
237 patchdir = os.path.dirname(filename)
238 else:
239 f = sys.stdin
240 patchdir = ''
241
242 for line in f:
243 patch = re.sub('#.*$', '', line).strip()
244 if not patch:
245 continue
bcb6d890 246 patchfile = os.path.join(patchdir, patch)
613a2f16 247 patch = __replace_slashes_with_dashes(patch);
9417ece4 248
fd1c0cfc 249 __import_file(patchfile, options, patch)
99c52915
CM
250
251 if filename:
252 f.close()
253
254def __import_mbox(filename, options):
255 """Import a series from an mbox file
256 """
257 if filename:
258 f = file(filename, 'rb')
259 else:
457c3093 260 f = StringIO(sys.stdin.read())
99c52915
CM
261
262 try:
263 mbox = UnixMailbox(f, email.message_from_file)
264 except Exception, ex:
265 raise CmdException, 'error parsing the mbox file: %s' % str(ex)
266
267 for msg in mbox:
268 message, author_name, author_email, author_date, diff = \
ed60fdae 269 parse_mail(msg)
99c52915
CM
270 __create_patch(None, message, author_name, author_email,
271 author_date, diff, options)
272
457c3093 273 f.close()
9417ece4 274
575c575e
CW
275def __import_url(url, options):
276 """Import a patch from a URL
277 """
278 import urllib
279 import tempfile
280
281 if not url:
4a8e79dc 282 raise CmdException('URL argument required')
575c575e 283
fd1c0cfc
CM
284 patch = os.path.basename(urllib.unquote(url))
285 filename = os.path.join(tempfile.gettempdir(), patch)
286 urllib.urlretrieve(url, filename)
287 __import_file(filename, options)
575c575e 288
972de1db
CW
289def __import_tarfile(tar, options):
290 """Import patch series from a tar archive
291 """
292 import tempfile
293 import shutil
294
295 if not tarfile.is_tarfile(tar):
296 raise CmdException, "%s is not a tarfile!" % tar
297
298 t = tarfile.open(tar, 'r')
299 names = t.getnames()
300
301 # verify paths in the tarfile are safe
302 for n in names:
303 if n.startswith('/'):
304 raise CmdException, "Absolute path found in %s" % tar
305 if n.find("..") > -1:
306 raise CmdException, "Relative path found in %s" % tar
307
308 # find the series file
309 seriesfile = '';
310 for m in names:
311 if m.endswith('/series') or m == 'series':
312 seriesfile = m
313 break
314 if seriesfile == '':
315 raise CmdException, "no 'series' file found in %s" % tar
316
317 # unpack into a tmp dir
318 tmpdir = tempfile.mkdtemp('.stg')
319 t.extractall(tmpdir)
320
321 # apply the series
322 __import_series(os.path.join(tmpdir, seriesfile), options)
323
324 # cleanup the tmpdir
325 shutil.rmtree(tmpdir)
326
9417ece4
CM
327def func(parser, options, args):
328 """Import a GNU diff file as a new patch
329 """
330 if len(args) > 1:
331 parser.error('incorrect number of arguments')
332
333 check_local_changes()
334 check_conflicts()
6972fd6b 335 check_head_top_equal(crt_series)
9417ece4
CM
336
337 if len(args) == 1:
338 filename = args[0]
339 else:
340 filename = None
341
4a8e79dc 342 if not options.url and filename:
47d51d91
CM
343 filename = os.path.abspath(filename)
344 directory.cd_to_topdir()
345
9417ece4
CM
346 if options.series:
347 __import_series(filename, options)
99c52915
CM
348 elif options.mbox:
349 __import_mbox(filename, options)
575c575e
CW
350 elif options.url:
351 __import_url(filename, options)
9417ece4 352 else:
fd1c0cfc 353 __import_file(filename, options)
9417ece4 354
6972fd6b 355 print_crt_patch(crt_series)