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