Avoid importing invalid and duplicate patch names
[stgit.git] / stgit / commands / imprt.py
blob48aa82740dcfcb5656475890cbdf6fbf3fa127df
1 import os
2 import re
3 import sys
5 from stgit import argparse
6 from stgit.argparse import opt
7 from stgit.commands.common import (
8 CmdException,
9 DirectoryHasRepository,
10 check_conflicts,
11 check_head_top_equal,
12 check_local_changes,
13 parse_patch,
14 update_commit_data,
16 from stgit.compat import decode_utf8_with_latin1
17 from stgit.lib.git import CommitData, Date, Person
18 from stgit.lib.transaction import StackTransaction, TransactionHalted
19 from stgit.out import out
20 from stgit.run import Run
22 __copyright__ = """
23 Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
25 This program is free software; you can redistribute it and/or modify
26 it under the terms of the GNU General Public License version 2 as
27 published by the Free Software Foundation.
29 This program is distributed in the hope that it will be useful,
30 but WITHOUT ANY WARRANTY; without even the implied warranty of
31 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
32 GNU General Public License for more details.
34 You should have received a copy of the GNU General Public License
35 along with this program; if not, see http://www.gnu.org/licenses/.
36 """
38 name = 'import'
39 help = 'Import a GNU diff file as a new patch'
40 kind = 'patch'
41 usage = ['[options] [--] [<file>|<url>]']
42 description = """
43 Create a new patch and apply the given GNU diff file (or the standard
44 input). By default, the file name is used as the patch name but this
45 can be overridden with the '--name' option. The patch can either be a
46 normal file with the description at the top or it can have standard
47 mail format, the Subject, From and Date headers being used for
48 generating the patch information. The command can also read series and
49 mbox files.
51 If a patch does not apply cleanly, the failed diff is written to the
52 .stgit-failed.patch file and an empty StGIT patch is added to the
53 stack.
55 The patch description has to be separated from the data with a '---'
56 line."""
58 args = ['files']
59 options = (
61 opt(
62 '-m',
63 '--mail',
64 action='store_true',
65 short='Import the patch from a standard e-mail file',
67 opt(
68 '-M',
69 '--mbox',
70 action='store_true',
71 short='Import a series of patches from an mbox file',
73 opt(
74 '-s',
75 '--series',
76 action='store_true',
77 short='Import a series of patches',
78 long="Import a series of patches from a series file or a tar archive.",
80 opt(
81 '-u',
82 '--url',
83 action='store_true',
84 short='Import a patch from a URL',
86 opt(
87 '-n',
88 '--name',
89 short='Use NAME as the patch name',
91 opt(
92 '-p',
93 '--strip',
94 type='int',
95 metavar='N',
96 short='Remove N leading slashes from diff paths (default 1)',
98 opt(
99 '-t',
100 '--stripname',
101 action='store_true',
102 short='Strip numbering and extension from patch name',
104 opt(
105 '-i',
106 '--ignore',
107 action='store_true',
108 short='Ignore the applied patches in the series',
110 opt(
111 '--replace',
112 action='store_true',
113 short='Replace the unapplied patches in the series',
115 opt(
116 '-b',
117 '--base',
118 args=['commit'],
119 short='Use BASE instead of HEAD for file importing',
121 opt(
122 '--reject',
123 action='store_true',
124 short='Leave the rejected hunks in corresponding *.rej files',
126 opt(
127 '--keep-cr',
128 action='store_true',
129 short='Do not remove "\\r" from email lines ending with "\\r\\n"',
131 opt(
132 '-e',
133 '--edit',
134 action='store_true',
135 short='Invoke an editor for the patch description',
137 opt(
138 '-d',
139 '--showdiff',
140 action='store_true',
141 short='Show the patch content in the editor buffer',
144 + argparse.author_options()
145 + argparse.sign_options()
148 directory = DirectoryHasRepository()
151 def __create_patch(
152 filename, message, author_name, author_email, author_date, diff, options
154 """Create a new patch on the stack"""
155 stack = directory.repository.current_stack
157 if options.name:
158 name = options.name
159 elif filename:
160 name = os.path.basename(filename)
161 else:
162 name = ''
164 if options.stripname:
165 # Removing leading numbers and trailing extension
166 name = re.sub(
167 r'''^
168 (?:[0-9]+-)? # Optional leading patch number
169 (.*?) # Patch name group (non-greedy)
170 (?:\.(?:diff|patch))? # Optional .diff or .patch extension
172 ''',
173 r'\g<1>',
174 name,
175 flags=re.VERBOSE,
178 need_unique = not (options.ignore or options.replace)
180 if name:
181 name = stack.patches.make_name(name, unique=need_unique, lower=False)
182 else:
183 name = stack.patches.make_name(message, unique=need_unique, lower=True)
185 if options.ignore and name in stack.patchorder.applied:
186 out.info('Ignoring already applied patch "%s"' % name)
187 return
189 out.start('Importing patch "%s"' % name)
191 author = options.author(
192 Person(
193 author_name,
194 author_email,
195 Date.maybe(author_date),
199 try:
200 if not diff:
201 out.warn('No diff found, creating empty patch')
202 tree = stack.head.data.tree
203 else:
204 iw = stack.repository.default_iw
205 iw.apply(diff, quiet=False, reject=options.reject, strip=options.strip)
206 tree = iw.index.write_tree()
208 cd = CommitData(
209 tree=tree,
210 parents=[stack.head],
211 author=author,
212 message=message,
214 cd = update_commit_data(
216 message=None,
217 author=None,
218 sign_str=options.sign_str,
219 edit=options.edit,
221 commit = stack.repository.commit(cd)
223 trans = StackTransaction(stack, 'import: %s' % name)
225 try:
226 if options.replace and name in stack.patchorder.unapplied:
227 trans.delete_patches(lambda pn: pn == name, quiet=True)
229 trans.patches[name] = commit
230 trans.applied.append(name)
231 except TransactionHalted:
232 pass
233 trans.run()
234 finally:
235 out.done()
238 def __get_handle_and_name(filename):
239 """Return a file object and a patch name derived from filename"""
240 import bz2
241 import gzip
243 # see if it's a gzip'ed or bzip2'ed patch
244 for copen, ext in [(gzip.open, '.gz'), (bz2.BZ2File, '.bz2')]:
245 try:
246 f = copen(filename)
247 f.read(1)
248 f.seek(0)
249 if filename.lower().endswith(ext):
250 filename = filename[: -len(ext)]
251 return (f, filename)
252 except IOError:
253 pass
255 # plain old file...
256 return (open(filename, 'rb'), filename)
259 def __import_file(filename, options):
260 """Import a patch from a file or standard input"""
261 if filename:
262 f, filename = __get_handle_and_name(filename)
263 else:
264 f = os.fdopen(sys.__stdin__.fileno(), 'rb')
266 patch_data = f.read()
268 if filename:
269 f.close()
271 message, author_name, author_email, author_date, diff = parse_patch(
272 patch_data, contains_diff=True
275 __create_patch(
276 filename,
277 message,
278 author_name,
279 author_email,
280 author_date,
281 diff,
282 options,
286 def __import_series(filename, options):
287 """Import a series of patches"""
288 import tarfile
290 if filename and tarfile.is_tarfile(filename):
291 __import_tarfile(filename, options)
292 return
293 elif filename:
294 f = open(filename)
295 patchdir = os.path.dirname(filename)
296 else:
297 f = sys.stdin
298 patchdir = ''
300 for line in f:
301 patch = re.sub('#.*$', '', line).strip()
302 if not patch:
303 continue
304 # Quilt can have "-p0", "-p1" or "-pab" patches stacked in the
305 # series but as strip level default to 1, only "-p0" can actually
306 # be found in the series file, the other ones are implicit
307 m = re.match(
308 r'(?P<patchfilename>.*)\s+-p\s*(?P<striplevel>(\d+|ab)?)\s*$', patch
310 if m:
311 patch = m.group('patchfilename')
312 if m.group('striplevel') != '0':
313 raise CmdException(
314 "error importing quilt series, patch '%s'"
315 " has unsupported strip level: '-p%s'"
316 % (patch, m.group('striplevel'))
318 options.strip = 0
319 else:
320 options.strip = 1
321 patchfile = os.path.join(patchdir, patch)
323 __import_file(patchfile, options)
325 if filename:
326 f.close()
329 def __import_mail(filename, options):
330 """Import a patch from an email file or mbox"""
331 import shutil
332 import tempfile
334 tmpdir = tempfile.mkdtemp('.stg')
335 try:
336 mail_paths = __mailsplit(tmpdir, filename, options)
337 for mail_path in mail_paths:
338 __import_mail_path(mail_path, filename, options)
339 finally:
340 shutil.rmtree(tmpdir)
343 def __mailsplit(tmpdir, filename, options):
344 mailsplit_cmd = ['git', 'mailsplit', '-d4', '-o' + tmpdir]
345 if options.mail:
346 mailsplit_cmd.append('-b')
347 if options.keep_cr:
348 mailsplit_cmd.append('--keep-cr')
350 if filename:
351 mailsplit_cmd.extend(['--', filename])
352 r = Run(*mailsplit_cmd)
353 else:
354 stdin = os.fdopen(sys.__stdin__.fileno(), 'rb')
355 r = Run(*mailsplit_cmd).encoding(None).raw_input(stdin.read())
357 num_patches = int(r.output_one_line())
359 mail_paths = [os.path.join(tmpdir, '%04d' % n) for n in range(1, num_patches + 1)]
361 return mail_paths
364 def __import_mail_path(mail_path, filename, options):
365 with open(mail_path, 'rb') as f:
366 mail = f.read()
368 msg_path = mail_path + '-msg'
369 patch_path = mail_path + '-patch'
371 mailinfo_lines = (
372 Run('git', 'mailinfo', msg_path, patch_path)
373 .encoding(None)
374 .decoding(None)
375 .raw_input(mail)
376 .output_lines(b'\n')
379 mailinfo = dict(line.split(b': ', 1) for line in mailinfo_lines if line)
381 with open(msg_path, 'rb') as f:
382 msg_body = f.read()
384 msg_bytes = mailinfo[b'Subject'] + b'\n\n' + msg_body
386 with open(patch_path, 'rb') as f:
387 diff = f.read()
389 __create_patch(
390 None if options.mbox else filename,
391 decode_utf8_with_latin1(msg_bytes),
392 mailinfo[b'Author'].decode('utf-8'),
393 mailinfo[b'Email'].decode('utf-8'),
394 mailinfo[b'Date'].decode('utf-8'),
395 diff,
396 options,
400 def __import_url(url, options):
401 """Import a patch from a URL"""
402 try:
403 from urllib.parse import unquote
404 from urllib.request import urlretrieve
405 except ImportError:
406 from urllib import unquote, urlretrieve
407 import tempfile
409 if not url:
410 raise CmdException('URL argument required')
412 patch = os.path.basename(unquote(url))
413 filename = os.path.join(tempfile.gettempdir(), patch)
414 urlretrieve(url, filename)
415 __import_file(filename, options)
418 def __import_tarfile(tarpath, options):
419 """Import patch series from a tar archive"""
420 import shutil
421 import tarfile
422 import tempfile
424 assert tarfile.is_tarfile(tarpath)
426 tar = tarfile.open(tarpath, 'r')
427 names = tar.getnames()
429 # verify paths in the tarfile are safe
430 for n in names:
431 if n.startswith('/'):
432 raise CmdException("Absolute path found in %s" % tarpath)
433 if n.find("..") > -1:
434 raise CmdException("Relative path found in %s" % tarpath)
436 # find the series file
437 for seriesfile in names:
438 if seriesfile.endswith('/series') or seriesfile == 'series':
439 break
440 else:
441 raise CmdException("no 'series' file found in %s" % tarpath)
443 # unpack into a tmp dir
444 tmpdir = tempfile.mkdtemp('.stg')
445 try:
446 tar.extractall(tmpdir)
447 __import_series(os.path.join(tmpdir, seriesfile), options)
448 finally:
449 shutil.rmtree(tmpdir)
452 def func(parser, options, args):
453 if len(args) > 1:
454 parser.error('incorrect number of arguments')
455 elif len(args) == 1:
456 filename = args[0]
457 else:
458 filename = None
460 if not options.url and filename:
461 filename = os.path.abspath(filename)
463 directory.cd_to_topdir()
465 repository = directory.repository
466 stack = repository.current_stack
468 check_local_changes(repository)
469 check_conflicts(repository.default_iw)
470 check_head_top_equal(stack)
472 if options.series:
473 __import_series(filename, options)
474 elif options.mail or options.mbox:
475 __import_mail(filename, options)
476 elif options.url:
477 __import_url(filename, options)
478 else:
479 __import_file(filename, options)