1 # -*- coding: utf-8 -*-
2 from __future__
import (
13 from stgit
import argparse
14 from stgit
.argparse
import opt
15 from stgit
.commands
.common
import (
17 DirectoryHasRepository
,
24 from stgit
.compat
import decode_utf8_with_latin1
25 from stgit
.lib
.git
import CommitData
, Date
, Person
26 from stgit
.lib
.transaction
import StackTransaction
, TransactionHalted
27 from stgit
.out
import out
28 from stgit
.run
import Run
29 from stgit
.utils
import make_patch_name
32 Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
34 This program is free software; you can redistribute it and/or modify
35 it under the terms of the GNU General Public License version 2 as
36 published by the Free Software Foundation.
38 This program is distributed in the hope that it will be useful,
39 but WITHOUT ANY WARRANTY; without even the implied warranty of
40 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
41 GNU General Public License for more details.
43 You should have received a copy of the GNU General Public License
44 along with this program; if not, see http://www.gnu.org/licenses/.
48 help = 'Import a GNU diff file as a new patch'
50 usage
= ['[options] [--] [<file>|<url>]']
52 Create a new patch and apply the given GNU diff file (or the standard
53 input). By default, the file name is used as the patch name but this
54 can be overridden with the '--name' option. The patch can either be a
55 normal file with the description at the top or it can have standard
56 mail format, the Subject, From and Date headers being used for
57 generating the patch information. The command can also read series and
60 If a patch does not apply cleanly, the failed diff is written to the
61 .stgit-failed.patch file and an empty StGIT patch is added to the
64 The patch description has to be separated from the data with a '---'
73 short
='Import the patch from a standard e-mail file',
79 short
='Import a series of patches from an mbox file',
85 short
='Import a series of patches',
87 Import a series of patches from a series file or a tar archive.""",
93 short
='Import a patch from a URL',
98 short
='Use NAME as the patch name',
105 short
='Remove N leading slashes from diff paths (default 1)',
111 short
='Strip numbering and extension from patch name',
117 short
='Ignore the applied patches in the series',
122 short
='Replace the unapplied patches in the series',
128 short
='Use BASE instead of HEAD for file importing',
133 short
='Leave the rejected hunks in corresponding *.rej files',
138 short
='Do not remove "\\r" from email lines ending with "\\r\\n"',
144 short
='Invoke an editor for the patch description',
150 short
='Show the patch content in the editor buffer',
152 ] + argparse
.author_options() + argparse
.sign_options()
154 directory
= DirectoryHasRepository()
157 def __strip_patch_name(name
):
158 stripped
= re
.sub('^[0-9]+-(.*)$', r
'\g<1>', name
)
159 stripped
= re
.sub(r
'^(.*)\.(diff|patch)$', r
'\g<1>', stripped
)
163 def __replace_slashes_with_dashes(name
):
164 stripped
= name
.replace('/', '-')
168 def __create_patch(filename
, message
, author_name
, author_email
,
169 author_date
, diff
, options
):
170 """Create a new patch on the stack
172 stack
= directory
.repository
.current_stack
176 if not stack
.patches
.is_name_valid(name
):
177 raise CmdException('Invalid patch name: %s' % name
)
179 name
= os
.path
.basename(filename
)
182 if options
.stripname
:
183 name
= __strip_patch_name(name
)
186 if options
.ignore
or options
.replace
:
187 def unacceptable_name(name
):
190 unacceptable_name
= stack
.patches
.exists
191 name
= make_patch_name(message
, unacceptable_name
)
193 # fix possible invalid characters in the patch name
194 name
= re
.sub(r
'[^\w.]+', '-', name
).strip('-')
196 assert stack
.patches
.is_name_valid(name
)
198 if options
.ignore
and name
in stack
.patchorder
.applied
:
199 out
.info('Ignoring already applied patch "%s"' % name
)
202 out
.start('Importing patch "%s"' % name
)
207 Date
.maybe(author_date
),
209 author
= options
.author(author
)
213 out
.warn('No diff found, creating empty patch')
214 tree
= stack
.head
.data
.tree
216 iw
= stack
.repository
.default_iw
218 diff
, quiet
=False, reject
=options
.reject
, strip
=options
.strip
220 tree
= iw
.index
.write_tree()
224 parents
=[stack
.head
],
228 cd
= update_commit_data(
232 sign_str
=options
.sign_str
,
235 commit
= stack
.repository
.commit(cd
)
237 trans
= StackTransaction(stack
, 'import: %s' % name
)
240 if options
.replace
and name
in stack
.patchorder
.unapplied
:
241 trans
.delete_patches(lambda pn
: pn
== name
, quiet
=True)
243 trans
.patches
[name
] = commit
244 trans
.applied
.append(name
)
245 except TransactionHalted
:
252 def __mkpatchname(name
, suffix
):
253 if name
.lower().endswith(suffix
.lower()):
254 return name
[:-len(suffix
)]
258 def __get_handle_and_name(filename
):
259 """Return a file object and a patch name derived from filename
264 # see if it's a gzip'ed or bzip2'ed patch
265 for copen
, ext
in [(gzip
.open, '.gz'), (bz2
.BZ2File
, '.bz2')]:
270 return (f
, __mkpatchname(filename
, ext
))
275 return (open(filename
, 'rb'), filename
)
278 def __import_file(filename
, options
, patch
=None):
279 """Import a patch from a file or standard input
283 (f
, pname
) = __get_handle_and_name(filename
)
285 f
= os
.fdopen(sys
.__stdin
__.fileno(), 'rb')
293 message
, author_name
, author_email
, author_date
, diff
294 ) = parse_patch(f
.read(), contains_diff
=True)
299 __create_patch(pname
, message
, author_name
, author_email
,
300 author_date
, diff
, options
)
303 def __import_series(filename
, options
):
304 """Import a series of patches
309 if tarfile
.is_tarfile(filename
):
310 __import_tarfile(filename
, options
)
313 patchdir
= os
.path
.dirname(filename
)
319 patch
= re
.sub('#.*$', '', line
).strip()
322 # Quilt can have "-p0", "-p1" or "-pab" patches stacked in the
323 # series but as strip level default to 1, only "-p0" can actually
324 # be found in the series file, the other ones are implicit
326 r
'(?P<patchfilename>.*)\s+-p\s*(?P<striplevel>(\d+|ab)?)\s*$',
331 patch
= m
.group('patchfilename')
332 if m
.group('striplevel') != '0':
333 raise CmdException("error importing quilt series, patch '%s'"
334 " has unsupported strip level: '-p%s'" %
335 (patch
, m
.group('striplevel')))
337 patchfile
= os
.path
.join(patchdir
, patch
)
338 patch
= __replace_slashes_with_dashes(patch
)
340 __import_file(patchfile
, options
, patch
)
346 def __import_mail(filename
, options
):
347 """Import a patch from an email file or mbox"""
351 tmpdir
= tempfile
.mkdtemp('.stg')
353 mail_paths
= __mailsplit(tmpdir
, filename
, options
)
354 for mail_path
in mail_paths
:
355 __import_mail_path(mail_path
, filename
, options
)
357 shutil
.rmtree(tmpdir
)
360 def __mailsplit(tmpdir
, filename
, options
):
361 mailsplit_cmd
= ['git', 'mailsplit', '-d4', '-o' + tmpdir
]
363 mailsplit_cmd
.append('-b')
365 mailsplit_cmd
.append('--keep-cr')
368 mailsplit_cmd
.extend(['--', filename
])
369 r
= Run(*mailsplit_cmd
)
371 stdin
= os
.fdopen(sys
.__stdin
__.fileno(), 'rb')
372 r
= Run(*mailsplit_cmd
).encoding(None).raw_input(stdin
.read())
374 num_patches
= int(r
.output_one_line())
377 os
.path
.join(tmpdir
, '%04d' % n
) for n
in range(1, num_patches
+ 1)
383 def __import_mail_path(mail_path
, filename
, options
):
384 with
open(mail_path
, 'rb') as f
:
387 msg_path
= mail_path
+ '-msg'
388 patch_path
= mail_path
+ '-patch'
390 mailinfo_lines
= Run(
391 'git', 'mailinfo', msg_path
, patch_path
392 ).encoding(None).decoding(None).raw_input(mail
).output_lines(b
'\n')
394 mailinfo
= dict(line
.split(b
': ', 1) for line
in mailinfo_lines
if line
)
396 with
open(msg_path
, 'rb') as f
:
399 msg_bytes
= mailinfo
[b
'Subject'] + b
'\n\n' + msg_body
401 with
open(patch_path
, 'rb') as f
:
405 None if options
.mbox
else filename
,
406 decode_utf8_with_latin1(msg_bytes
),
407 mailinfo
[b
'Author'].decode('utf-8'),
408 mailinfo
[b
'Email'].decode('utf-8'),
409 mailinfo
[b
'Date'].decode('utf-8'),
415 def __import_url(url
, options
):
416 """Import a patch from a URL
419 from urllib
.request
import urlretrieve
420 from urllib
.parse
import unquote
422 from urllib
import urlretrieve
, unquote
426 raise CmdException('URL argument required')
428 patch
= os
.path
.basename(unquote(url
))
429 filename
= os
.path
.join(tempfile
.gettempdir(), patch
)
430 urlretrieve(url
, filename
)
431 __import_file(filename
, options
)
434 def __import_tarfile(tar
, options
):
435 """Import patch series from a tar archive
441 if not tarfile
.is_tarfile(tar
):
442 raise CmdException("%s is not a tarfile!" % tar
)
444 t
= tarfile
.open(tar
, 'r')
447 # verify paths in the tarfile are safe
449 if n
.startswith('/'):
450 raise CmdException("Absolute path found in %s" % tar
)
451 if n
.find("..") > -1:
452 raise CmdException("Relative path found in %s" % tar
)
454 # find the series file
457 if m
.endswith('/series') or m
== 'series':
461 raise CmdException("no 'series' file found in %s" % tar
)
463 # unpack into a tmp dir
464 tmpdir
= tempfile
.mkdtemp('.stg')
468 __import_series(os
.path
.join(tmpdir
, seriesfile
), options
)
471 shutil
.rmtree(tmpdir
)
474 def func(parser
, options
, args
):
475 """Import a GNU diff file as a new patch
478 parser
.error('incorrect number of arguments')
485 if not options
.url
and filename
:
486 filename
= os
.path
.abspath(filename
)
487 directory
.cd_to_topdir()
489 repository
= directory
.repository
490 stack
= repository
.current_stack
491 check_local_changes(repository
)
492 check_conflicts(repository
.default_iw
)
493 check_head_top_equal(stack
)
496 __import_series(filename
, options
)
497 elif options
.mail
or options
.mbox
:
498 __import_mail(filename
, options
)
500 __import_url(filename
, options
)
502 __import_file(filename
, options
)