6 from stgit
import argparse
7 from stgit
.argparse
import opt
8 from stgit
.commands
.common
import (
10 DirectoryHasRepository
,
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
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/.
40 help = 'Import a GNU diff file as a new patch'
42 usage
= ['[options] [--] [<file>|<url>]']
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
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
56 The patch description has to be separated from the data with a '---'
66 short
='Import the patch from a standard e-mail file',
72 short
='Import a series of patches from an mbox file',
78 short
='Import a series of patches',
79 long="Import a series of patches from a series file or a tar archive.",
85 short
='Import a patch from a URL',
90 short
='Use NAME as the patch name',
97 short
='Remove N leading slashes from diff paths (default 1)',
103 short
='Strip numbering and extension from patch name',
109 short
='Ignore the applied patches in the series',
114 short
='Replace the unapplied patches in the series',
120 short
='Use BASE instead of HEAD for file importing',
125 short
='Leave the rejected hunks in corresponding *.rej files',
130 short
='Do not remove "\\r" from email lines ending with "\\r\\n"',
136 short
='Invoke an editor for the patch description',
142 short
='Show the patch content in the editor buffer',
145 + argparse
.author_options()
146 + argparse
.sign_options()
149 directory
= DirectoryHasRepository()
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
161 name
= os
.path
.basename(filename
)
165 if options
.stripname
:
166 # Removing leading numbers and trailing extension
169 (?:[0-9]+-)? # Optional leading patch number
170 (.*?) # Patch name group (non-greedy)
171 (?:\.(?:diff|patch))? # Optional .diff or .patch extension
179 need_unique
= not (options
.ignore
or options
.replace
)
182 name
= stack
.patches
.make_name(name
, unique
=need_unique
, lower
=False)
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
)
190 out
.start('Importing patch "%s"' % name
)
192 author
= options
.author(
196 Date
.maybe(author_date
),
202 out
.warn('No diff found, creating empty patch')
203 tree
= stack
.head
.data
.tree
205 iw
= stack
.repository
.default_iw
206 iw
.apply(diff
, quiet
=False, reject
=options
.reject
, strip
=options
.strip
)
207 tree
= iw
.index
.write_tree()
211 parents
=[stack
.head
],
215 cd
= update_commit_data(
219 sign_str
=options
.sign_str
,
222 commit
= stack
.repository
.commit(cd
)
224 trans
= StackTransaction(stack
, 'import: %s' % name
)
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
:
239 def __get_handle_and_name(filename
):
240 """Return a file object and a patch name derived from filename"""
244 # see if it's a gzip'ed or bzip2'ed patch
245 for copen
, ext
in [(gzip
.open, '.gz'), (bz2
.BZ2File
, '.bz2')]:
250 if filename
.lower().endswith(ext
):
251 filename
= filename
[: -len(ext
)]
257 return (open(filename
, 'rb'), filename
)
260 def __import_file(filename
, options
):
261 """Import a patch from a file or standard input"""
263 f
, filename
= __get_handle_and_name(filename
)
265 f
= os
.fdopen(sys
.__stdin
__.fileno(), 'rb')
267 patch_data
= f
.read()
272 message
, author_name
, author_email
, author_date
, diff
= parse_patch(
273 patch_data
, contains_diff
=True
287 def __import_series(filename
, options
):
288 """Import a series of patches"""
291 if filename
and tarfile
.is_tarfile(filename
):
292 __import_tarfile(filename
, options
)
296 patchdir
= os
.path
.dirname(filename
)
302 patch
= re
.sub('#.*$', '', line
).strip()
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
309 r
'(?P<patchfilename>.*)\s+-p\s*(?P<striplevel>(\d+|ab)?)\s*$', patch
312 patch
= m
.group('patchfilename')
313 if m
.group('striplevel') != '0':
315 "error importing quilt series, patch '%s'"
316 " has unsupported strip level: '-p%s'"
317 % (patch
, m
.group('striplevel'))
322 patchfile
= os
.path
.join(patchdir
, patch
)
324 __import_file(patchfile
, options
)
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
]
341 mailsplit_cmd
.append('-b')
343 mailsplit_cmd
.append('--keep-cr')
346 mailsplit_cmd
.extend(['--', filename
])
347 r
= Run(*mailsplit_cmd
)
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)]
359 def __import_mail_path(mail_path
, filename
, options
):
360 with
open(mail_path
, 'rb') as f
:
363 msg_path
= mail_path
+ '-msg'
364 patch_path
= mail_path
+ '-patch'
367 Run('git', 'mailinfo', msg_path
, patch_path
)
374 mailinfo
= dict(line
.split(b
': ', 1) for line
in mailinfo_lines
if line
)
376 with
open(msg_path
, 'rb') as f
:
379 msg_bytes
= mailinfo
[b
'Subject'] + b
'\n\n' + msg_body
381 with
open(patch_path
, 'rb') as f
:
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'),
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"""
411 assert tarfile
.is_tarfile(tarpath
)
413 tar
= tarfile
.open(tarpath
, 'r')
414 names
= tar
.getnames()
416 # verify paths in the tarfile are safe
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':
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
):
438 parser
.error('incorrect number of arguments')
442 raise CmdException('URL argument required')
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
)
459 __import_series(filename
, options
)
460 elif options
.mail
or options
.mbox
:
461 __import_mail(filename
, options
)
463 __import_url(filename
, options
)
465 __import_file(filename
, options
)