Autosign imported patches
[stgit] / stgit / commands / imprt.py
index 5e3eddd..a2d0e50 100644 (file)
@@ -15,124 +15,314 @@ along with this program; if not, write to the Free Software
 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 """
 
 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 """
 
-import sys, os
-from optparse import OptionParser, make_option
-
+import sys, os, re, email, tarfile
+from mailbox import UnixMailbox
+from StringIO import StringIO
+from stgit.argparse import opt
 from stgit.commands.common import *
 from stgit.utils import *
 from stgit.commands.common import *
 from stgit.utils import *
-from stgit import stack, git
-
-
-help = 'import a GNU diff file as a new patch'
-usage = """%prog [options] [<file>]
-
+from stgit.out import *
+from stgit import argparse, stack, git
+
+name = 'import'
+help = 'Import a GNU diff file as a new patch'
+kind = 'patch'
+usage = ['[options] [<file>|<url>]']
+description = """
 Create a new patch and apply the given GNU diff file (or the standard
 input). By default, the file name is used as the patch name but this
 Create a new patch and apply the given GNU diff file (or the standard
 input). By default, the file name is used as the patch name but this
-can be overriden with the '--name' option. The patch can either be a
+can be overridden with the '--name' option. The patch can either be a
 normal file with the description at the top or it can have standard
 mail format, the Subject, From and Date headers being used for
 normal file with the description at the top or it can have standard
 mail format, the Subject, From and Date headers being used for
-generating the patch information.
+generating the patch information. The command can also read series and
+mbox files.
+
+If a patch does not apply cleanly, the failed diff is written to the
+.stgit-failed.patch file and an empty StGIT patch is added to the
+stack.
 
 The patch description has to be separated from the data with a '---'
 
 The patch description has to be separated from the data with a '---'
-line. For a normal file, if no author information is given, the first
-'Signed-off-by:' line is used."""
-
-options = [make_option('-m', '--mail',
-                       help = 'import the patch from a standard e-mail file',
-                       action = 'store_true'),
-           make_option('-n', '--name',
-                       help = 'use NAME as the patch name'),
-           make_option('-a', '--author', metavar = '"NAME <EMAIL>"',
-                       help = 'use "NAME <EMAIL>" as the author details'),
-           make_option('--authname',
-                       help = 'use AUTHNAME as the author name'),
-           make_option('--authemail',
-                       help = 'use AUTHEMAIL as the author e-mail'),
-           make_option('--authdate',
-                       help = 'use AUTHDATE as the author date'),
-           make_option('--commname',
-                       help = 'use COMMNAME as the committer name'),
-           make_option('--commemail',
-                       help = 'use COMMEMAIL as the committer e-mail')]
-
-
-def __parse_mail(filename = None):
-    """Parse the input file in a mail format and return (description,
-    authname, authemail, authdate)
+line."""
+
+args = [argparse.files]
+options = [
+    opt('-m', '--mail', action = 'store_true',
+        short = 'Import the patch from a standard e-mail file'),
+    opt('-M', '--mbox', action = 'store_true',
+        short = 'Import a series of patches from an mbox file'),
+    opt('-s', '--series', action = 'store_true',
+        short = 'Import a series of patches', long = """
+        Import a series of patches from a series file or a tar archive."""),
+    opt('-u', '--url', action = 'store_true',
+        short = 'Import a patch from a URL'),
+    opt('-n', '--name',
+        short = 'Use NAME as the patch name'),
+    opt('-p', '--strip', type = 'int', metavar = 'N',
+        short = 'Remove N leading slashes from diff paths (default 1)'),
+    opt('-t', '--stripname', action = 'store_true',
+        short = 'Strip numbering and extension from patch name'),
+    opt('-i', '--ignore', action = 'store_true',
+        short = 'Ignore the applied patches in the series'),
+    opt('--replace', action = 'store_true',
+        short = 'Replace the unapplied patches in the series'),
+    opt('-b', '--base', args = [argparse.commit],
+        short = 'Use BASE instead of HEAD for file importing'),
+    opt('--reject', action = 'store_true',
+        short = 'Leave the rejected hunks in corresponding *.rej files'),
+    opt('-e', '--edit', action = 'store_true',
+        short = 'Invoke an editor for the patch description'),
+    opt('-d', '--showdiff', action = 'store_true',
+        short = 'Show the patch content in the editor buffer'),
+    opt('-a', '--author', metavar = '"NAME <EMAIL>"',
+        short = 'Use "NAME <EMAIL>" as the author details'),
+    opt('--authname',
+        short = 'Use AUTHNAME as the author name'),
+    opt('--authemail',
+        short = 'Use AUTHEMAIL as the author e-mail'),
+    opt('--authdate',
+        short = 'Use AUTHDATE as the author date'),
+    ] + argparse.sign_options()
+
+directory = DirectoryHasRepository(log = True)
+
+def __strip_patch_name(name):
+    stripped = re.sub('^[0-9]+-(.*)$', '\g<1>', name)
+    stripped = re.sub('^(.*)\.(diff|patch)$', '\g<1>', stripped)
+
+    return stripped
+
+def __replace_slashes_with_dashes(name):
+    stripped = name.replace('/', '-')
+
+    return stripped
+
+def __create_patch(filename, message, author_name, author_email,
+                   author_date, diff, options):
+    """Create a new patch on the stack
     """
     """
-    if filename:
-        f = file(filename)
+    if options.name:
+        patch = options.name
+    elif filename:
+        patch = os.path.basename(filename)
     else:
     else:
-        f = sys.stdin
+        patch = ''
+    if options.stripname:
+        patch = __strip_patch_name(patch)
 
 
-    descr = authname = authemail = authdate = None
+    if not patch:
+        if options.ignore or options.replace:
+            unacceptable_name = lambda name: False
+        else:
+            unacceptable_name = crt_series.patch_exists
+        patch = make_patch_name(message, unacceptable_name)
+    else:
+        # fix possible invalid characters in the patch name
+        patch = re.sub('[^\w.]+', '-', patch).strip('-')
 
 
-    # parse the headers
-    for line in f:
-        line = line.strip()
-        if re.match('from:\s+', line, re.I):
-            auth = re.findall('^.*?:\s+(.*)$', line)[0]
-            authname, authemail = name_email(auth)
-        elif re.match('date:\s+', line, re.I):
-            authdate = re.findall('^.*?:\s+(.*)$', line)[0]
-        elif re.match('subject:\s+', line, re.I):
-            descr = re.findall('^.*?:\s+(.*)$', line)[0]
-        elif line == '':
-            # end of headers
-            break
+    if options.ignore and patch in crt_series.get_applied():
+        out.info('Ignoring already applied patch "%s"' % patch)
+        return
+    if options.replace and patch in crt_series.get_unapplied():
+        crt_series.delete_patch(patch, keep_log = True)
 
 
-    # remove extra '[*PATCH]', 'name:' in the subject
-    if descr:
-        descr = re.findall('^(\[[^\s]*PATCH.*?\])?\s*([^\s]*:)?\s*(.*)$',
-                           descr)[0][2]
-        descr += '\n\n'
-    else:
-        raise CmdException, 'Subject: line not found'
+    # refresh_patch() will invoke the editor in this case, with correct
+    # patch content
+    if not message:
+        can_edit = False
 
 
-    # the rest of the patch description
-    for line in f:
-        if re.match('----*\s*$', line) or re.match('diff -', line):
-            break
+    if options.author:
+        options.authname, options.authemail = name_email(options.author)
+
+    # override the automatically parsed settings
+    if options.authname:
+        author_name = options.authname
+    if options.authemail:
+        author_email = options.authemail
+    if options.authdate:
+        author_date = options.authdate
+
+    sign_str = options.sign_str
+    if not options.sign_str:
+        sign_str = config.get('stgit.autosign')
+
+    crt_series.new_patch(patch, message = message, can_edit = False,
+                         author_name = author_name,
+                         author_email = author_email,
+                         author_date = author_date, sign_str = sign_str)
+
+    if not diff:
+        out.warn('No diff found, creating empty patch')
+    else:
+        out.start('Importing patch "%s"' % patch)
+        if options.base:
+            base = git_id(crt_series, options.base)
         else:
         else:
-            descr += line
-    descr.rstrip()
+            base = None
+        try:
+            git.apply_patch(diff = diff, base = base, reject = options.reject,
+                            strip = options.strip)
+        except git.GitException:
+            if not options.reject:
+                crt_series.delete_patch(patch)
+            raise
+        crt_series.refresh_patch(edit = options.edit,
+                                 show_patch = options.showdiff,
+                                 author_date = author_date,
+                                 backup = False)
+        out.done()
+
+def __mkpatchname(name, suffix):
+    if name.lower().endswith(suffix.lower()):
+        return name[:-len(suffix)]
+    return name
+
+def __get_handle_and_name(filename):
+    """Return a file object and a patch name derived from filename
+    """
+    # see if it's a gzip'ed or bzip2'ed patch
+    import bz2, gzip
+    for copen, ext in [(gzip.open, '.gz'), (bz2.BZ2File, '.bz2')]:
+        try:
+            f = copen(filename)
+            f.read(1)
+            f.seek(0)
+            return (f, __mkpatchname(filename, ext))
+        except IOError, e:
+            pass
+
+    # plain old file...
+    return (open(filename), filename)
+
+def __import_file(filename, options, patch = None):
+    """Import a patch from a file or standard input
+    """
+    pname = None
+    if filename:
+        (f, pname) = __get_handle_and_name(filename)
+    else:
+        f = sys.stdin
+
+    if patch:
+        pname = patch
+    elif not pname:
+        pname = filename
+
+    if options.mail:
+        try:
+            msg = email.message_from_file(f)
+        except Exception, ex:
+            raise CmdException, 'error parsing the e-mail file: %s' % str(ex)
+        message, author_name, author_email, author_date, diff = \
+                 parse_mail(msg)
+    else:
+        message, author_name, author_email, author_date, diff = \
+                 parse_patch(f.read(), contains_diff = True)
 
     if filename:
         f.close()
 
 
     if filename:
         f.close()
 
-    return (descr, authname, authemail, authdate)
+    __create_patch(pname, message, author_name, author_email,
+                   author_date, diff, options)
 
 
-def __parse_patch(filename = None):
-    """Parse the input file and return (description, authname,
-    authemail, authdate)
+def __import_series(filename, options):
+    """Import a series of patches
     """
     """
+    applied = crt_series.get_applied()
+
     if filename:
     if filename:
+        if tarfile.is_tarfile(filename):
+            __import_tarfile(filename, options)
+            return
         f = file(filename)
         f = file(filename)
+        patchdir = os.path.dirname(filename)
     else:
         f = sys.stdin
     else:
         f = sys.stdin
+        patchdir = ''
 
 
-    authname = authemail = authdate = None
-
-    descr = ''
     for line in f:
     for line in f:
-        # the first 'Signed-of-by:' is the author
-        if not authname and re.match('signed-off-by:\s+', line, re.I):
-            auth = re.findall('^.*?:\s+(.*)$', line)[0]
-            authname, authemail = name_email(auth)
-
-        if re.match('----*\s*$', line) or re.match('diff -', line):
-            break
-        else:
-            descr += line
-    descr.rstrip()
+        patch = re.sub('#.*$', '', line).strip()
+        if not patch:
+            continue
+        patchfile = os.path.join(patchdir, patch)
+        patch = __replace_slashes_with_dashes(patch);
 
 
-    if descr == '':
-        descr = None
+        __import_file(patchfile, options, patch)
 
     if filename:
         f.close()
 
 
     if filename:
         f.close()
 
-    return (descr, authname, authemail, authdate)
+def __import_mbox(filename, options):
+    """Import a series from an mbox file
+    """
+    if filename:
+        f = file(filename, 'rb')
+    else:
+        f = StringIO(sys.stdin.read())
+
+    try:
+        mbox = UnixMailbox(f, email.message_from_file)
+    except Exception, ex:
+        raise CmdException, 'error parsing the mbox file: %s' % str(ex)
+
+    for msg in mbox:
+        message, author_name, author_email, author_date, diff = \
+                 parse_mail(msg)
+        __create_patch(None, message, author_name, author_email,
+                       author_date, diff, options)
+
+    f.close()
+
+def __import_url(url, options):
+    """Import a patch from a URL
+    """
+    import urllib
+    import tempfile
+
+    if not url:
+        raise CmdException('URL argument required')
+
+    patch = os.path.basename(urllib.unquote(url))
+    filename = os.path.join(tempfile.gettempdir(), patch)
+    urllib.urlretrieve(url, filename)
+    __import_file(filename, options)
+
+def __import_tarfile(tar, options):
+    """Import patch series from a tar archive
+    """
+    import tempfile
+    import shutil
+
+    if not tarfile.is_tarfile(tar):
+        raise CmdException, "%s is not a tarfile!" % tar
+
+    t = tarfile.open(tar, 'r')
+    names = t.getnames()
+
+    # verify paths in the tarfile are safe
+    for n in names:
+        if n.startswith('/'):
+            raise CmdException, "Absolute path found in %s" % tar
+        if n.find("..") > -1:
+            raise CmdException, "Relative path found in %s" % tar
+
+    # find the series file
+    seriesfile = '';
+    for m in names:
+        if m.endswith('/series') or m == 'series':
+            seriesfile = m
+            break
+    if seriesfile == '':
+        raise CmdException, "no 'series' file found in %s" % tar
+
+    # unpack into a tmp dir
+    tmpdir = tempfile.mkdtemp('.stg')
+    t.extractall(tmpdir)
+
+    # apply the series
+    __import_series(os.path.join(tmpdir, seriesfile), options)
+
+    # cleanup the tmpdir
+    shutil.rmtree(tmpdir)
 
 def func(parser, options, args):
     """Import a GNU diff file as a new patch
 
 def func(parser, options, args):
     """Import a GNU diff file as a new patch
@@ -142,55 +332,24 @@ def func(parser, options, args):
 
     check_local_changes()
     check_conflicts()
 
     check_local_changes()
     check_conflicts()
-    check_head_top_equal()
+    check_head_top_equal(crt_series)
 
     if len(args) == 1:
         filename = args[0]
 
     if len(args) == 1:
         filename = args[0]
-        patch = os.path.basename(filename)
-    elif options.name:
-        filename = None
-        patch = options.name
     else:
     else:
-        raise CmdException, 'Unkown patch name'
+        filename = None
 
 
-    # the defaults
-    message = author_name = author_email = author_date = committer_name = \
-              committer_email = None
+    if not options.url and filename:
+        filename = os.path.abspath(filename)
+    directory.cd_to_topdir()
 
 
-    if options.author:
-        options.authname, options.authemail = name_email(options.author)
-
-    if options.mail:
-        message, author_name, author_email, author_date = \
-                 __parse_mail(filename)
+    if options.series:
+        __import_series(filename, options)
+    elif options.mbox:
+        __import_mbox(filename, options)
+    elif options.url:
+        __import_url(filename, options)
     else:
     else:
-        message, author_name, author_email, author_date = \
-                 __parse_patch(filename)
-
-    # override the automatically parsed settings
-    if options.authname:
-        author_name = options.authname
-    if options.authemail:
-        author_email = options.authemail
-    if options.authdate:
-        author_date = options.authdate
-    if options.commname:
-        committer_name = options.commname
-    if options.commemail:
-        committer_email = options.commemail
-
-    crt_series.new_patch(patch, message = message,
-                         author_name = author_name,
-                         author_email = author_email,
-                         author_date = author_date,
-                         committer_name = committer_name,
-                         committer_email = committer_email)
-
-    print 'Importing patch %s...' % patch,
-    sys.stdout.flush()
-
-    git.apply_patch(filename)
-    crt_series.refresh_patch()
+        __import_file(filename, options)
 
 
-    print 'done'
-    print_crt_patch()
+    print_crt_patch(crt_series)