1 # -*- coding: utf-8 -*-
2 from __future__
import absolute_import
, division
, print_function
3 from contextlib
import closing
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
,
27 from stgit
.utils
import make_patch_name
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/.
46 help = 'Import a GNU diff file as a new patch'
48 usage
= ['[options] [--] [<file>|<url>]']
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
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
62 The patch description has to be separated from the data with a '---'
65 args
= [argparse
.files
]
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'),
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'),
97 short
= 'Use AUTHNAME as the author name'),
99 short
= 'Use AUTHEMAIL as the author e-mail'),
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
)
112 def __replace_slashes_with_dashes(name
):
113 stripped
= name
.replace('/', '-')
117 def __create_patch(filename
, message
, author_name
, author_email
,
118 author_date
, diff
, options
):
119 """Create a new patch on the stack
124 patch
= os
.path
.basename(filename
)
127 if options
.stripname
:
128 patch
= __strip_patch_name(patch
)
131 if options
.ignore
or options
.replace
:
132 unacceptable_name
= lambda name
: False
134 unacceptable_name
= crt_series
.patch_exists
135 patch
= make_patch_name(message
, unacceptable_name
)
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
)
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
152 options
.authname
, options
.authemail
= name_email(options
.author
)
154 # override the automatically parsed settings
156 author_name
= options
.authname
157 if options
.authemail
:
158 author_email
= options
.authemail
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
)
172 out
.warn('No diff found, creating empty patch')
174 out
.start('Importing patch "%s"' % patch
)
176 base
= git_id(crt_series
, options
.base
)
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
)
186 crt_series
.refresh_patch(edit
= options
.edit
,
187 show_patch
= options
.showdiff
,
188 author_date
= author_date
,
192 def __mkpatchname(name
, suffix
):
193 if name
.lower().endswith(suffix
.lower()):
194 return name
[:-len(suffix
)]
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')]:
206 return (f
, __mkpatchname(filename
, ext
))
211 return (open(filename
), filename
)
213 def __import_file(filename
, options
, patch
= None):
214 """Import a patch from a file or standard input
218 (f
, pname
) = __get_handle_and_name(filename
)
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
= \
235 message
, author_name
, author_email
, author_date
, diff
= \
236 parse_patch(f
.read(), contains_diff
= True)
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()
250 if tarfile
.is_tarfile(filename
):
251 __import_tarfile(filename
, options
)
254 patchdir
= os
.path
.dirname(filename
)
260 patch
= re
.sub('#.*$', '', line
).strip()
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
)
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')))
275 patchfile
= os
.path
.join(patchdir
, patch
)
276 patch
= __replace_slashes_with_dashes(patch
)
278 __import_file(patchfile
, options
, patch
)
283 def __import_mbox(filename
, options
):
284 """Import a series from an mbox file
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())
294 filename
= namedtemp
.name
298 mbox
= mailbox
.mbox(filename
, email
.message_from_file
,
300 except Exception as ex
:
301 raise CmdException('error parsing the mbox file: %s' % str(ex
))
309 diff
) = parse_mail(msg
)
310 __create_patch(None, message
, author_name
, author_email
,
311 author_date
, diff
, options
)
313 if namedtemp
is not None:
314 os
.unlink(namedtemp
.name
)
317 def __import_url(url
, options
):
318 """Import a patch from a URL
321 from urllib
.request
import urlretrieve
322 from urllib
.parse
import unquote
324 from urllib
import urlretrieve
, unquote
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
341 if not tarfile
.is_tarfile(tar
):
342 raise CmdException("%s is not a tarfile!" % tar
)
344 t
= tarfile
.open(tar
, 'r')
347 # verify paths in the tarfile are safe
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
357 if m
.endswith('/series') or m
== 'series':
361 raise CmdException("no 'series' file found in %s" % tar
)
363 # unpack into a tmp dir
364 tmpdir
= tempfile
.mkdtemp('.stg')
368 __import_series(os
.path
.join(tmpdir
, seriesfile
), options
)
371 shutil
.rmtree(tmpdir
)
373 def func(parser
, options
, args
):
374 """Import a GNU diff file as a new patch
377 parser
.error('incorrect number of arguments')
379 check_local_changes()
381 check_head_top_equal(crt_series
)
388 if not options
.url
and filename
:
389 filename
= os
.path
.abspath(filename
)
390 directory
.cd_to_topdir()
393 __import_series(filename
, options
)
395 __import_mbox(filename
, options
)
397 __import_url(filename
, options
)
399 __import_file(filename
, options
)
401 print_crt_patch(crt_series
)