Use tempfile.TemporaryDirectory in imprt.py
[stgit.git] / stgit / commands / imprt.py
blob2439d9fa60b512c6a8e307810c1eaf2bd6e82db8
1 import os
2 import re
3 import sys
4 import tempfile
6 from stgit import argparse
7 from stgit.argparse import opt
8 from stgit.commands.common import (
9 CmdException,
10 DirectoryHasRepository,
11 check_conflicts,
12 check_head_top_equal,
13 check_local_changes,
14 parse_patch,
15 update_commit_data,
17 from stgit.compat import decode_utf8_with_latin1
18 from stgit.lib.git import CommitData, Date, Person
19 from stgit.lib.transaction import StackTransaction, TransactionHalted
20 from stgit.out import out
21 from stgit.run import Run
23 __copyright__ = """
24 Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
26 This program is free software; you can redistribute it and/or modify
27 it under the terms of the GNU General Public License version 2 as
28 published by the Free Software Foundation.
30 This program is distributed in the hope that it will be useful,
31 but WITHOUT ANY WARRANTY; without even the implied warranty of
32 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
33 GNU General Public License for more details.
35 You should have received a copy of the GNU General Public License
36 along with this program; if not, see http://www.gnu.org/licenses/.
37 """
39 name = 'import'
40 help = 'Import a GNU diff file as a new patch'
41 kind = 'patch'
42 usage = ['[options] [--] [<file>|<url>]']
43 description = """
44 Create a new patch and apply the given GNU diff file (or the standard
45 input). By default, the file name is used as the patch name but this
46 can be overridden with the '--name' option. The patch can either be a
47 normal file with the description at the top or it can have standard
48 mail format, the Subject, From and Date headers being used for
49 generating the patch information. The command can also read series and
50 mbox files.
52 If a patch does not apply cleanly, the failed diff is written to the
53 .stgit-failed.patch file and an empty StGIT patch is added to the
54 stack.
56 The patch description has to be separated from the data with a '---'
57 line."""
59 args = ['files']
60 options = (
62 opt(
63 '-m',
64 '--mail',
65 action='store_true',
66 short='Import the patch from a standard e-mail file',
68 opt(
69 '-M',
70 '--mbox',
71 action='store_true',
72 short='Import a series of patches from an mbox file',
74 opt(
75 '-s',
76 '--series',
77 action='store_true',
78 short='Import a series of patches',
79 long="Import a series of patches from a series file or a tar archive.",
81 opt(
82 '-u',
83 '--url',
84 action='store_true',
85 short='Import a patch from a URL',
87 opt(
88 '-n',
89 '--name',
90 short='Use NAME as the patch name',
92 opt(
93 '-p',
94 '--strip',
95 type='int',
96 metavar='N',
97 short='Remove N leading slashes from diff paths (default 1)',
99 opt(
100 '-t',
101 '--stripname',
102 action='store_true',
103 short='Strip numbering and extension from patch name',
105 opt(
106 '-i',
107 '--ignore',
108 action='store_true',
109 short='Ignore the applied patches in the series',
111 opt(
112 '--replace',
113 action='store_true',
114 short='Replace the unapplied patches in the series',
116 opt(
117 '-b',
118 '--base',
119 args=['commit'],
120 short='Use BASE instead of HEAD for file importing',
122 opt(
123 '--reject',
124 action='store_true',
125 short='Leave the rejected hunks in corresponding *.rej files',
127 opt(
128 '--keep-cr',
129 action='store_true',
130 short='Do not remove "\\r" from email lines ending with "\\r\\n"',
132 opt(
133 '-e',
134 '--edit',
135 action='store_true',
136 short='Invoke an editor for the patch description',
138 opt(
139 '-d',
140 '--showdiff',
141 action='store_true',
142 short='Show the patch content in the editor buffer',
145 + argparse.author_options()
146 + argparse.sign_options()
149 directory = DirectoryHasRepository()
152 def __create_patch(
153 filename, message, author_name, author_email, author_date, diff, options
155 """Create a new patch on the stack"""
156 stack = directory.repository.current_stack
158 if options.name:
159 name = options.name
160 elif filename:
161 name = os.path.basename(filename)
162 else:
163 name = ''
165 if options.stripname:
166 # Removing leading numbers and trailing extension
167 name = re.sub(
168 r'''^
169 (?:[0-9]+-)? # Optional leading patch number
170 (.*?) # Patch name group (non-greedy)
171 (?:\.(?:diff|patch))? # Optional .diff or .patch extension
173 ''',
174 r'\g<1>',
175 name,
176 flags=re.VERBOSE,
179 need_unique = not (options.ignore or options.replace)
181 if name:
182 name = stack.patches.make_name(name, unique=need_unique, lower=False)
183 else:
184 name = stack.patches.make_name(message, unique=need_unique, lower=True)
186 if options.ignore and name in stack.patchorder.applied:
187 out.info('Ignoring already applied patch "%s"' % name)
188 return
190 out.start('Importing patch "%s"' % name)
192 author = options.author(
193 Person(
194 author_name,
195 author_email,
196 Date.maybe(author_date),
200 try:
201 if not diff:
202 out.warn('No diff found, creating empty patch')
203 tree = stack.head.data.tree
204 else:
205 iw = stack.repository.default_iw
206 iw.apply(diff, quiet=False, reject=options.reject, strip=options.strip)
207 tree = iw.index.write_tree()
209 cd = CommitData(
210 tree=tree,
211 parents=[stack.head],
212 author=author,
213 message=message,
215 cd = update_commit_data(
217 message=None,
218 author=None,
219 sign_str=options.sign_str,
220 edit=options.edit,
222 commit = stack.repository.commit(cd)
224 trans = StackTransaction(stack, 'import: %s' % name)
226 try:
227 if options.replace and name in stack.patchorder.unapplied:
228 trans.delete_patches(lambda pn: pn == name, quiet=True)
230 trans.patches[name] = commit
231 trans.applied.append(name)
232 except TransactionHalted:
233 pass
234 trans.run()
235 finally:
236 out.done()
239 def __get_handle_and_name(filename):
240 """Return a file object and a patch name derived from filename"""
241 import bz2
242 import gzip
244 # see if it's a gzip'ed or bzip2'ed patch
245 for copen, ext in [(gzip.open, '.gz'), (bz2.BZ2File, '.bz2')]:
246 try:
247 f = copen(filename)
248 f.read(1)
249 f.seek(0)
250 if filename.lower().endswith(ext):
251 filename = filename[: -len(ext)]
252 return (f, filename)
253 except IOError:
254 pass
256 # plain old file...
257 return (open(filename, 'rb'), filename)
260 def __import_file(filename, options):
261 """Import a patch from a file or standard input"""
262 if filename:
263 f, filename = __get_handle_and_name(filename)
264 else:
265 f = os.fdopen(sys.__stdin__.fileno(), 'rb')
267 patch_data = f.read()
269 if filename:
270 f.close()
272 message, author_name, author_email, author_date, diff = parse_patch(
273 patch_data, contains_diff=True
276 __create_patch(
277 filename,
278 message,
279 author_name,
280 author_email,
281 author_date,
282 diff,
283 options,
287 def __import_series(filename, options):
288 """Import a series of patches"""
289 import tarfile
291 if filename and tarfile.is_tarfile(filename):
292 __import_tarfile(filename, options)
293 return
294 elif filename:
295 f = open(filename)
296 patchdir = os.path.dirname(filename)
297 else:
298 f = sys.stdin
299 patchdir = ''
301 for line in f:
302 patch = re.sub('#.*$', '', line).strip()
303 if not patch:
304 continue
305 # Quilt can have "-p0", "-p1" or "-pab" patches stacked in the
306 # series but as strip level default to 1, only "-p0" can actually
307 # be found in the series file, the other ones are implicit
308 m = re.match(
309 r'(?P<patchfilename>.*)\s+-p\s*(?P<striplevel>(\d+|ab)?)\s*$', patch
311 if m:
312 patch = m.group('patchfilename')
313 if m.group('striplevel') != '0':
314 raise CmdException(
315 "error importing quilt series, patch '%s'"
316 " has unsupported strip level: '-p%s'"
317 % (patch, m.group('striplevel'))
319 options.strip = 0
320 else:
321 options.strip = 1
322 patchfile = os.path.join(patchdir, patch)
324 __import_file(patchfile, options)
326 if filename:
327 f.close()
330 def __import_mail(filename, options):
331 """Import a patch from an email file or mbox"""
332 with tempfile.TemporaryDirectory('.stg') as tmpdir:
333 mail_paths = __mailsplit(tmpdir, filename, options)
334 for mail_path in mail_paths:
335 __import_mail_path(mail_path, filename, options)
338 def __mailsplit(tmpdir, filename, options):
339 mailsplit_cmd = ['git', 'mailsplit', '-d4', '-o' + tmpdir]
340 if options.mail:
341 mailsplit_cmd.append('-b')
342 if options.keep_cr:
343 mailsplit_cmd.append('--keep-cr')
345 if filename:
346 mailsplit_cmd.extend(['--', filename])
347 r = Run(*mailsplit_cmd)
348 else:
349 stdin = os.fdopen(sys.__stdin__.fileno(), 'rb')
350 r = Run(*mailsplit_cmd).encoding(None).raw_input(stdin.read())
352 num_patches = int(r.output_one_line())
354 mail_paths = [os.path.join(tmpdir, '%04d' % n) for n in range(1, num_patches + 1)]
356 return mail_paths
359 def __import_mail_path(mail_path, filename, options):
360 with open(mail_path, 'rb') as f:
361 mail = f.read()
363 msg_path = mail_path + '-msg'
364 patch_path = mail_path + '-patch'
366 mailinfo_lines = (
367 Run('git', 'mailinfo', msg_path, patch_path)
368 .encoding(None)
369 .decoding(None)
370 .raw_input(mail)
371 .output_lines(b'\n')
374 mailinfo = dict(line.split(b': ', 1) for line in mailinfo_lines if line)
376 with open(msg_path, 'rb') as f:
377 msg_body = f.read()
379 msg_bytes = mailinfo[b'Subject'] + b'\n\n' + msg_body
381 with open(patch_path, 'rb') as f:
382 diff = f.read()
384 __create_patch(
385 None if options.mbox else filename,
386 decode_utf8_with_latin1(msg_bytes),
387 mailinfo[b'Author'].decode('utf-8'),
388 mailinfo[b'Email'].decode('utf-8'),
389 mailinfo[b'Date'].decode('utf-8'),
390 diff,
391 options,
395 def __import_url(url, options):
396 """Import a patch from a URL"""
397 from urllib.parse import unquote
398 from urllib.request import urlretrieve
400 with tempfile.TemporaryDirectory('.stg') as tmpdir:
401 patch = os.path.basename(unquote(url))
402 filename = os.path.join(tmpdir, patch)
403 urlretrieve(url, filename)
404 __import_file(filename, options)
407 def __import_tarfile(tarpath, options):
408 """Import patch series from a tar archive"""
409 import tarfile
411 assert tarfile.is_tarfile(tarpath)
413 tar = tarfile.open(tarpath, 'r')
414 names = tar.getnames()
416 # verify paths in the tarfile are safe
417 for n in names:
418 if n.startswith('/'):
419 raise CmdException("Absolute path found in %s" % tarpath)
420 if n.find("..") > -1:
421 raise CmdException("Relative path found in %s" % tarpath)
423 # find the series file
424 for seriesfile in names:
425 if seriesfile.endswith('/series') or seriesfile == 'series':
426 break
427 else:
428 raise CmdException("no 'series' file found in %s" % tarpath)
430 # unpack into a tmp dir
431 with tempfile.TemporaryDirectory('.stg') as tmpdir:
432 tar.extractall(tmpdir)
433 __import_series(os.path.join(tmpdir, seriesfile), options)
436 def func(parser, options, args):
437 if len(args) > 1:
438 parser.error('incorrect number of arguments')
439 elif len(args) == 1:
440 filename = args[0]
441 elif options.url:
442 raise CmdException('URL argument required')
443 else:
444 filename = None
446 if not options.url and filename:
447 filename = os.path.abspath(filename)
449 directory.cd_to_topdir()
451 repository = directory.repository
452 stack = repository.current_stack
454 check_local_changes(repository)
455 check_conflicts(repository.default_iw)
456 check_head_top_equal(stack)
458 if options.series:
459 __import_series(filename, options)
460 elif options.mail or options.mbox:
461 __import_mail(filename, options)
462 elif options.url:
463 __import_url(filename, options)
464 else:
465 __import_file(filename, options)