Use raw strings for regexps with unescaped \'s
[stgit.git] / stgit / commands / imprt.py
bloba6b01bae94c9b50ac320a2e224fe964dd6474f15
1 # -*- coding: utf-8 -*-
2 from __future__ import absolute_import, division, print_function
3 from contextlib import closing
4 import bz2
5 import email
6 import gzip
7 import mailbox
8 import os
9 import re
10 import sys
11 import tarfile
13 from stgit import argparse, git
14 from stgit.argparse import opt
15 from stgit.config import config
16 from stgit.out import out
17 from stgit.commands.common import (CmdException,
18 DirectoryHasRepository,
19 check_conflicts,
20 check_head_top_equal,
21 check_local_changes,
22 git_id,
23 name_email,
24 parse_mail,
25 parse_patch,
26 print_crt_patch)
27 from stgit.utils import make_patch_name
29 __copyright__ = """
30 Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
32 This program is free software; you can redistribute it and/or modify
33 it under the terms of the GNU General Public License version 2 as
34 published by the Free Software Foundation.
36 This program is distributed in the hope that it will be useful,
37 but WITHOUT ANY WARRANTY; without even the implied warranty of
38 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
39 GNU General Public License for more details.
41 You should have received a copy of the GNU General Public License
42 along with this program; if not, see http://www.gnu.org/licenses/.
43 """
45 name = 'import'
46 help = 'Import a GNU diff file as a new patch'
47 kind = 'patch'
48 usage = ['[options] [--] [<file>|<url>]']
49 description = """
50 Create a new patch and apply the given GNU diff file (or the standard
51 input). By default, the file name is used as the patch name but this
52 can be overridden with the '--name' option. The patch can either be a
53 normal file with the description at the top or it can have standard
54 mail format, the Subject, From and Date headers being used for
55 generating the patch information. The command can also read series and
56 mbox files.
58 If a patch does not apply cleanly, the failed diff is written to the
59 .stgit-failed.patch file and an empty StGIT patch is added to the
60 stack.
62 The patch description has to be separated from the data with a '---'
63 line."""
65 args = [argparse.files]
66 options = [
67 opt('-m', '--mail', action = 'store_true',
68 short = 'Import the patch from a standard e-mail file'),
69 opt('-M', '--mbox', action = 'store_true',
70 short = 'Import a series of patches from an mbox file'),
71 opt('-s', '--series', action = 'store_true',
72 short = 'Import a series of patches', long = """
73 Import a series of patches from a series file or a tar archive."""),
74 opt('-u', '--url', action = 'store_true',
75 short = 'Import a patch from a URL'),
76 opt('-n', '--name',
77 short = 'Use NAME as the patch name'),
78 opt('-p', '--strip', type = 'int', metavar = 'N',
79 short = 'Remove N leading slashes from diff paths (default 1)'),
80 opt('-t', '--stripname', action = 'store_true',
81 short = 'Strip numbering and extension from patch name'),
82 opt('-i', '--ignore', action = 'store_true',
83 short = 'Ignore the applied patches in the series'),
84 opt('--replace', action = 'store_true',
85 short = 'Replace the unapplied patches in the series'),
86 opt('-b', '--base', args = [argparse.commit],
87 short = 'Use BASE instead of HEAD for file importing'),
88 opt('--reject', action = 'store_true',
89 short = 'Leave the rejected hunks in corresponding *.rej files'),
90 opt('-e', '--edit', action = 'store_true',
91 short = 'Invoke an editor for the patch description'),
92 opt('-d', '--showdiff', action = 'store_true',
93 short = 'Show the patch content in the editor buffer'),
94 opt('-a', '--author', metavar = '"NAME <EMAIL>"',
95 short = 'Use "NAME <EMAIL>" as the author details'),
96 opt('--authname',
97 short = 'Use AUTHNAME as the author name'),
98 opt('--authemail',
99 short = 'Use AUTHEMAIL as the author e-mail'),
100 opt('--authdate',
101 short = 'Use AUTHDATE as the author date'),
102 ] + argparse.sign_options()
104 directory = DirectoryHasRepository(log = True)
106 def __strip_patch_name(name):
107 stripped = re.sub('^[0-9]+-(.*)$', r'\g<1>', name)
108 stripped = re.sub(r'^(.*)\.(diff|patch)$', r'\g<1>', stripped)
110 return stripped
112 def __replace_slashes_with_dashes(name):
113 stripped = name.replace('/', '-')
115 return stripped
117 def __create_patch(filename, message, author_name, author_email,
118 author_date, diff, options):
119 """Create a new patch on the stack
121 if options.name:
122 patch = options.name
123 elif filename:
124 patch = os.path.basename(filename)
125 else:
126 patch = ''
127 if options.stripname:
128 patch = __strip_patch_name(patch)
130 if not patch:
131 if options.ignore or options.replace:
132 unacceptable_name = lambda name: False
133 else:
134 unacceptable_name = crt_series.patch_exists
135 patch = make_patch_name(message, unacceptable_name)
136 else:
137 # fix possible invalid characters in the patch name
138 patch = re.sub(r'[^\w.]+', '-', patch).strip('-')
140 if options.ignore and patch in crt_series.get_applied():
141 out.info('Ignoring already applied patch "%s"' % patch)
142 return
143 if options.replace and patch in crt_series.get_unapplied():
144 crt_series.delete_patch(patch, keep_log = True)
146 # refresh_patch() will invoke the editor in this case, with correct
147 # patch content
148 if not message:
149 can_edit = False
151 if options.author:
152 options.authname, options.authemail = name_email(options.author)
154 # override the automatically parsed settings
155 if options.authname:
156 author_name = options.authname
157 if options.authemail:
158 author_email = options.authemail
159 if options.authdate:
160 author_date = options.authdate
162 sign_str = options.sign_str
163 if not options.sign_str:
164 sign_str = config.get('stgit.autosign')
166 crt_series.new_patch(patch, message = message, can_edit = False,
167 author_name = author_name,
168 author_email = author_email,
169 author_date = author_date, sign_str = sign_str)
171 if not diff:
172 out.warn('No diff found, creating empty patch')
173 else:
174 out.start('Importing patch "%s"' % patch)
175 if options.base:
176 base = git_id(crt_series, options.base)
177 else:
178 base = None
179 try:
180 git.apply_patch(diff = diff, base = base, reject = options.reject,
181 strip = options.strip)
182 except git.GitException:
183 if not options.reject:
184 crt_series.delete_patch(patch)
185 raise
186 crt_series.refresh_patch(edit = options.edit,
187 show_patch = options.showdiff,
188 author_date = author_date,
189 backup = False)
190 out.done()
192 def __mkpatchname(name, suffix):
193 if name.lower().endswith(suffix.lower()):
194 return name[:-len(suffix)]
195 return name
197 def __get_handle_and_name(filename):
198 """Return a file object and a patch name derived from filename
200 # see if it's a gzip'ed or bzip2'ed patch
201 for copen, ext in [(gzip.open, '.gz'), (bz2.BZ2File, '.bz2')]:
202 try:
203 f = copen(filename)
204 f.read(1)
205 f.seek(0)
206 return (f, __mkpatchname(filename, ext))
207 except IOError:
208 pass
210 # plain old file...
211 return (open(filename), filename)
213 def __import_file(filename, options, patch = None):
214 """Import a patch from a file or standard input
216 pname = None
217 if filename:
218 (f, pname) = __get_handle_and_name(filename)
219 else:
220 f = sys.stdin
222 if patch:
223 pname = patch
224 elif not pname:
225 pname = filename
227 if options.mail:
228 try:
229 msg = email.message_from_file(f)
230 except Exception as ex:
231 raise CmdException('error parsing the e-mail file: %s' % str(ex))
232 message, author_name, author_email, author_date, diff = \
233 parse_mail(msg)
234 else:
235 message, author_name, author_email, author_date, diff = \
236 parse_patch(f.read(), contains_diff = True)
238 if filename:
239 f.close()
241 __create_patch(pname, message, author_name, author_email,
242 author_date, diff, options)
244 def __import_series(filename, options):
245 """Import a series of patches
247 applied = crt_series.get_applied()
249 if filename:
250 if tarfile.is_tarfile(filename):
251 __import_tarfile(filename, options)
252 return
253 f = open(filename)
254 patchdir = os.path.dirname(filename)
255 else:
256 f = sys.stdin
257 patchdir = ''
259 for line in f:
260 patch = re.sub('#.*$', '', line).strip()
261 if not patch:
262 continue
263 # Quilt can have "-p0", "-p1" or "-pab" patches stacked in the
264 # series but as strip level default to 1, only "-p0" can actually
265 # be found in the series file, the other ones are implicit
266 m = re.match(r'(?P<patchfilename>.*)\s+-p\s*(?P<striplevel>(\d+|ab)?)\s*$', patch)
267 options.strip = 1
268 if m:
269 patch = m.group('patchfilename')
270 if m.group('striplevel') != '0':
271 raise CmdException("error importing quilt series, patch '%s'"
272 " has unsupported strip level: '-p%s'" %
273 (patch, m.group('striplevel')))
274 options.strip = 0
275 patchfile = os.path.join(patchdir, patch)
276 patch = __replace_slashes_with_dashes(patch)
278 __import_file(patchfile, options, patch)
280 if filename:
281 f.close()
283 def __import_mbox(filename, options):
284 """Import a series from an mbox file
286 if filename:
287 namedtemp = None
288 else:
289 from tempfile import NamedTemporaryFile
290 stdin = os.fdopen(sys.stdin.fileno(), 'rb')
291 namedtemp = NamedTemporaryFile('wb', suffix='.mbox', delete=False)
292 namedtemp.write(stdin.read())
293 namedtemp.close()
294 filename = namedtemp.name
296 try:
297 try:
298 mbox = mailbox.mbox(filename, email.message_from_file,
299 create=False)
300 except Exception as ex:
301 raise CmdException('error parsing the mbox file: %s' % str(ex))
303 with closing(mbox):
304 for msg in mbox:
305 (message,
306 author_name,
307 author_email,
308 author_date,
309 diff) = parse_mail(msg)
310 __create_patch(None, message, author_name, author_email,
311 author_date, diff, options)
312 finally:
313 if namedtemp is not None:
314 os.unlink(namedtemp.name)
317 def __import_url(url, options):
318 """Import a patch from a URL
320 try:
321 from urllib.request import urlretrieve
322 from urllib.parse import unquote
323 except ImportError:
324 from urllib import urlretrieve, unquote
325 import tempfile
327 if not url:
328 raise CmdException('URL argument required')
330 patch = os.path.basename(unquote(url))
331 filename = os.path.join(tempfile.gettempdir(), patch)
332 urlretrieve(url, filename)
333 __import_file(filename, options)
335 def __import_tarfile(tar, options):
336 """Import patch series from a tar archive
338 import tempfile
339 import shutil
341 if not tarfile.is_tarfile(tar):
342 raise CmdException("%s is not a tarfile!" % tar)
344 t = tarfile.open(tar, 'r')
345 names = t.getnames()
347 # verify paths in the tarfile are safe
348 for n in names:
349 if n.startswith('/'):
350 raise CmdException("Absolute path found in %s" % tar)
351 if n.find("..") > -1:
352 raise CmdException("Relative path found in %s" % tar)
354 # find the series file
355 seriesfile = ''
356 for m in names:
357 if m.endswith('/series') or m == 'series':
358 seriesfile = m
359 break
360 if seriesfile == '':
361 raise CmdException("no 'series' file found in %s" % tar)
363 # unpack into a tmp dir
364 tmpdir = tempfile.mkdtemp('.stg')
365 t.extractall(tmpdir)
367 # apply the series
368 __import_series(os.path.join(tmpdir, seriesfile), options)
370 # cleanup the tmpdir
371 shutil.rmtree(tmpdir)
373 def func(parser, options, args):
374 """Import a GNU diff file as a new patch
376 if len(args) > 1:
377 parser.error('incorrect number of arguments')
379 check_local_changes()
380 check_conflicts()
381 check_head_top_equal(crt_series)
383 if len(args) == 1:
384 filename = args[0]
385 else:
386 filename = None
388 if not options.url and filename:
389 filename = os.path.abspath(filename)
390 directory.cd_to_topdir()
392 if options.series:
393 __import_series(filename, options)
394 elif options.mbox:
395 __import_mbox(filename, options)
396 elif options.url:
397 __import_url(filename, options)
398 else:
399 __import_file(filename, options)
401 print_crt_patch(crt_series)