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
.config
import config
19 from stgit
.lib
.git
import CommitData
, Date
, Person
20 from stgit
.lib
.transaction
import StackTransaction
, TransactionHalted
21 from stgit
.out
import out
22 from stgit
.run
import Run
25 Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
27 This program is free software; you can redistribute it and/or modify
28 it under the terms of the GNU General Public License version 2 as
29 published by the Free Software Foundation.
31 This program is distributed in the hope that it will be useful,
32 but WITHOUT ANY WARRANTY; without even the implied warranty of
33 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
34 GNU General Public License for more details.
36 You should have received a copy of the GNU General Public License
37 along with this program; if not, see http://www.gnu.org/licenses/.
41 help = 'Import a GNU diff file as a new patch'
43 usage
= ['[options] [--] [<file>|<url>]']
45 Create a new patch and apply the given GNU diff file (or the standard
46 input). By default, the file name is used as the patch name but this
47 can be overridden with the '--name' option. The patch can either be a
48 normal file with the description at the top or it can have standard
49 mail format, the Subject, From and Date headers being used for
50 generating the patch information. The command can also read series and
53 If a patch does not apply cleanly, the failed diff is written to the
54 .stgit-failed.patch file and an empty StGit patch is added to the
57 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',
107 dest
='context_lines',
110 short
='Ensure N lines of surrounding context for each change',
116 short
='Ignore the applied patches in the series',
121 short
='Replace the unapplied patches in the series',
127 short
='Use BASE instead of HEAD for file importing',
132 short
='Leave the rejected hunks in corresponding *.rej files',
137 short
='Do not remove "\\r" from email lines ending with "\\r\\n"',
142 short
='Create Message-Id trailer from Message-ID header',
144 "Create Message-Id trailer in patch description based on the "
145 "Message-ID email header. This option is applicable when importing "
146 "with --mail or --mbox. This behavior may also be enabled via the "
147 "'stgit.import.message-id' configuration option."
154 short
='Invoke an editor for the patch description',
160 short
='Show the patch content in the editor buffer',
163 options
.extend(argparse
.author_options())
164 options
.extend(argparse
.trailer_options())
167 directory
= DirectoryHasRepository()
171 filename
, message
, patch_name
, author_name
, author_email
, author_date
, diff
, options
173 """Create a new patch on the stack"""
174 stack
= directory
.repository
.current_stack
181 name
= os
.path
.basename(filename
)
185 if options
.stripname
:
186 # Removing leading numbers and trailing extension
189 (?:[0-9]+-)? # Optional leading patch number
190 (.*?) # Patch name group (non-greedy)
191 (?:\.(?:diff|patch))? # Optional .diff or .patch extension
199 need_unique
= not (options
.ignore
or options
.replace
)
202 name
= stack
.patches
.make_name(name
, unique
=need_unique
, lower
=False)
204 name
= stack
.patches
.make_name(message
, unique
=need_unique
, lower
=True)
206 if options
.ignore
and name
in stack
.patchorder
.applied
:
207 out
.info('Ignoring already applied patch "%s"' % name
)
210 out
.start('Importing patch "%s"' % name
)
212 author
= options
.author(
216 Date
.maybe(author_date
),
222 out
.warn('No diff found, creating empty patch')
223 tree
= stack
.head
.data
.tree
225 iw
= stack
.repository
.default_iw
229 reject
=options
.reject
,
231 context_lines
=options
.context_lines
,
233 tree
= iw
.index
.write_tree()
237 parents
=[stack
.head
],
241 cd
= update_commit_data(
246 trailers
=options
.trailers
,
249 commit
= stack
.repository
.commit(cd
)
251 trans
= StackTransaction(stack
, 'import: %s' % name
)
254 if options
.replace
and name
in stack
.patchorder
.unapplied
:
255 trans
.delete_patches(lambda pn
: pn
== name
, quiet
=True)
257 trans
.patches
[name
] = commit
258 trans
.applied
.append(name
)
259 except TransactionHalted
:
266 def __get_handle_and_name(filename
):
267 """Return a file object and a patch name derived from filename"""
271 # see if it's a gzip'ed or bzip2'ed patch
272 for copen
, ext
in [(gzip
.open, '.gz'), (bz2
.BZ2File
, '.bz2')]:
277 if filename
.lower().endswith(ext
):
278 filename
= filename
[: -len(ext
)]
284 return (open(filename
, 'rb'), filename
)
287 def __import_file(filename
, options
):
288 """Import a patch from a file or standard input"""
290 f
, filename
= __get_handle_and_name(filename
)
292 f
= os
.fdopen(sys
.__stdin
__.fileno(), 'rb')
294 patch_data
= f
.read()
299 message
, patch_name
, author_name
, author_email
, author_date
, diff
= parse_patch(
300 patch_data
, contains_diff
=True, fail_on_empty_description
=False
315 def __import_series(filename
, options
):
316 """Import a series of patches"""
319 if filename
and tarfile
.is_tarfile(filename
):
320 __import_tarfile(filename
, options
)
324 patchdir
= os
.path
.dirname(filename
)
330 patch
= re
.sub('#.*$', '', line
).strip()
333 # Quilt can have "-p0", "-p1" or "-pab" patches stacked in the
334 # series but as strip level default to 1, only "-p0" can actually
335 # be found in the series file, the other ones are implicit
337 r
'(?P<patchfilename>.*)\s+-p\s*(?P<striplevel>(\d+|ab)?)\s*$', patch
340 patch
= m
.group('patchfilename')
341 if m
.group('striplevel') != '0':
343 "error importing quilt series, patch '%s'"
344 " has unsupported strip level: '-p%s'"
345 % (patch
, m
.group('striplevel'))
350 patchfile
= os
.path
.join(patchdir
, patch
)
352 __import_file(patchfile
, options
)
358 def __import_mail(filename
, options
):
359 """Import a patch from an email file or mbox"""
360 with tempfile
.TemporaryDirectory('.stg') as tmpdir
:
361 mail_paths
= __mailsplit(tmpdir
, filename
, options
)
362 for mail_path
in mail_paths
:
363 __import_mail_path(mail_path
, filename
, options
)
366 def __mailsplit(tmpdir
, filename
, options
):
367 mailsplit_cmd
= ['git', 'mailsplit', '-d4', '-o' + tmpdir
]
369 mailsplit_cmd
.append('-b')
371 mailsplit_cmd
.append('--keep-cr')
374 mailsplit_cmd
.extend(['--', filename
])
375 r
= Run(*mailsplit_cmd
)
377 stdin
= os
.fdopen(sys
.__stdin
__.fileno(), 'rb')
378 r
= Run(*mailsplit_cmd
).encoding(None).raw_input(stdin
.read())
380 num_patches
= int(r
.output_one_line())
382 mail_paths
= [os
.path
.join(tmpdir
, '%04d' % n
) for n
in range(1, num_patches
+ 1)]
387 def __import_mail_path(mail_path
, filename
, options
):
388 with
open(mail_path
, 'rb') as f
:
391 msg_path
= mail_path
+ '-msg'
392 patch_path
= mail_path
+ '-patch'
394 mailinfo_cmd
= ['git', 'mailinfo']
395 if options
.message_id
or config
.getbool('stgit.import.message-id'):
396 mailinfo_cmd
.append('--message-id')
397 mailinfo_cmd
.extend([msg_path
, patch_path
])
407 mailinfo
= dict(line
.split(b
': ', 1) for line
in mailinfo_lines
if line
)
409 with
open(msg_path
, 'rb') as f
:
412 msg_bytes
= mailinfo
[b
'Subject'] + b
'\n\n' + msg_body
414 with
open(patch_path
, 'rb') as f
:
418 None if options
.mbox
else filename
,
419 decode_utf8_with_latin1(msg_bytes
),
421 mailinfo
[b
'Author'].decode('utf-8'),
422 mailinfo
[b
'Email'].decode('utf-8'),
423 mailinfo
[b
'Date'].decode('utf-8'),
429 def __import_url(url
, options
):
430 """Import a patch from a URL"""
431 from urllib
.parse
import unquote
432 from urllib
.request
import urlretrieve
434 with tempfile
.TemporaryDirectory('.stg') as tmpdir
:
435 base
= os
.path
.basename(unquote(url
))
436 filename
= os
.path
.join(tmpdir
, base
)
437 urlretrieve(url
, filename
)
439 __import_series(filename
, options
)
440 elif options
.mail
or options
.mbox
:
441 __import_mail(filename
, options
)
443 __import_file(filename
, options
)
446 def __import_tarfile(tarpath
, options
):
447 """Import patch series from a tar archive"""
450 assert tarfile
.is_tarfile(tarpath
)
452 tar
= tarfile
.open(tarpath
, 'r')
453 names
= tar
.getnames()
455 # verify paths in the tarfile are safe
457 if n
.startswith('/'):
458 raise CmdException("Absolute path found in %s" % tarpath
)
459 if n
.find("..") > -1:
460 raise CmdException("Relative path found in %s" % tarpath
)
462 # find the series file
463 for seriesfile
in names
:
464 if seriesfile
.endswith('/series') or seriesfile
== 'series':
467 raise CmdException("no 'series' file found in %s" % tarpath
)
469 # unpack into a tmp dir
470 with tempfile
.TemporaryDirectory('.stg') as tmpdir
:
471 tar
.extractall(tmpdir
)
472 __import_series(os
.path
.join(tmpdir
, seriesfile
), options
)
475 def func(parser
, options
, args
):
477 parser
.error('incorrect number of arguments')
481 raise CmdException('URL argument required')
485 if not options
.url
and filename
:
486 filename
= os
.path
.abspath(filename
)
488 directory
.cd_to_topdir()
490 repository
= directory
.repository
491 stack
= repository
.current_stack
493 check_local_changes(repository
)
494 check_conflicts(repository
.default_iw
)
495 check_head_top_equal(stack
)
498 __import_url(filename
, options
)
500 __import_series(filename
, options
)
501 elif options
.mail
or options
.mbox
:
502 __import_mail(filename
, options
)
504 __import_file(filename
, options
)