Add mbox support to "import"
authorCatalin Marinas <catalin.marinas@gmail.com>
Wed, 8 Nov 2006 22:30:02 +0000 (22:30 +0000)
committerCatalin Marinas <catalin.marinas@gmail.com>
Wed, 8 Nov 2006 22:30:02 +0000 (22:30 +0000)
This patch allows the "import" command to read an mbox file. It also fixes
various bugs and modifies git.apply_patch() to dump the failed patch to
the .stgit-failed.patch file for easy inspection or manual applying.

Signed-off-by: Catalin Marinas <catalin.marinas@gmail.com>
stgit/commands/common.py
stgit/commands/imprt.py
stgit/git.py
stgit/stack.py

index d986711..4e802bc 100644 (file)
@@ -294,14 +294,15 @@ def patch_name_from_msg(msg):
     subject_line = msg[:30].lstrip().split('\n', 1)[0].lower()
     return re.sub('[\W]+', '-', subject_line).strip('-')
 
-def make_patch_name(msg, unacceptable, default_name = 'patch'):
+def make_patch_name(msg, unacceptable, default_name = 'patch',
+                    alternative = True):
     """Return a patch name generated from the given commit message,
     guaranteed to make unacceptable(name) be false. If the commit
     message is empty, base the name on default_name instead."""
     patchname = patch_name_from_msg(msg)
     if not patchname:
-        patchname = 'patch'
-    if unacceptable(patchname):
+        patchname = default_name
+    if alternative and unacceptable(patchname):
         suffix = 0
         while unacceptable('%s-%d' % (patchname, suffix)):
             suffix += 1
index 4ecb861..9c97498 100644 (file)
@@ -17,6 +17,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 
 import sys, os, re, email
 from email.Header import decode_header, make_header
+from mailbox import UnixMailbox
 from optparse import OptionParser, make_option
 
 from stgit.commands.common import *
@@ -32,7 +33,12 @@ input). By default, the file name is used as the patch name but this
 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
-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 '---'
 line."""
@@ -40,14 +46,17 @@ line."""
 options = [make_option('-m', '--mail',
                        help = 'import the patch from a standard e-mail file',
                        action = 'store_true'),
+           make_option('-M', '--mbox',
+                       help = 'import a series of patches from an mbox file',
+                       action = 'store_true'),
+           make_option('-s', '--series',
+                       help = 'import a series of patches',
+                       action = 'store_true'),
            make_option('-n', '--name',
                        help = 'use NAME as the patch name'),
            make_option('-t', '--strip',
                        help = 'strip numbering and extension from patch name',
                        action = 'store_true'),
-           make_option('-s', '--series',
-                       help = 'import a series of patches',
-                       action = 'store_true'),
            make_option('-i', '--ignore',
                        help = 'ignore the applied patches in the series',
                        action = 'store_true'),
@@ -132,9 +141,9 @@ def __parse_description(descr):
 
     return (subject + body, authname, authemail, authdate)
 
-def __parse_mail(filename = None):
-    """Parse the input file in a mail format and return (description,
-    authname, authemail, authdate)
+def __parse_mail(msg):
+    """Parse the message object and return (description, authname,
+    authemail, authdate, diff)
     """
     def __decode_header(header):
         """Decode a qp-encoded e-mail header as per rfc2047"""
@@ -145,23 +154,14 @@ def __parse_mail(filename = None):
             raise CmdException, 'header decoding error: %s' % str(ex)
         return unicode(hobj).encode('utf-8')
 
-    if filename:
-        f = file(filename)
-    else:
-        f = sys.stdin
-
-    msg = email.message_from_file(f)
-
-    if filename:
-        f.close()
-
     # parse the headers
     if msg.has_key('from'):
         authname, authemail = name_email(__decode_header(msg['from']))
     else:
         authname = authemail = None
 
-    descr = __decode_header(msg['subject'])
+    # '\n\t' can be found on multi-line headers
+    descr = __decode_header(msg['subject']).replace('\n\t', ' ')
     authdate = msg['date']
 
     # remove the '[*PATCH*]' expression in the subject
@@ -174,8 +174,10 @@ def __parse_mail(filename = None):
 
     # the rest of the message
     if msg.is_multipart():
-        descr += msg.get_payload(0, decode = True)
-        diff = msg.get_payload(1, decode = True)
+        # this is assuming that the first part is the patch
+        # description and the second part is the attached patch
+        descr += msg.get_payload(0).get_payload(decode = True)
+        diff = msg.get_payload(1).get_payload(decode = True)
     else:
         diff = msg.get_payload(decode = True)
 
@@ -198,18 +200,13 @@ def __parse_mail(filename = None):
 
     return (descr, authname, authemail, authdate, diff)
 
-def __parse_patch(filename = None):
+def __parse_patch(fobj):
     """Parse the input file and return (description, authname,
-    authemail, authdate)
+    authemail, authdate, diff)
     """
-    if filename:
-        f = file(filename)
-    else:
-        f = sys.stdin
-
     descr = ''
     while True:
-        line = f.readline()
+        line = fobj.readline()
         if not line:
             break
 
@@ -219,10 +216,7 @@ def __parse_patch(filename = None):
             descr += line
     descr.rstrip()
 
-    diff = f.read()
-
-    if filename:
-        f.close()
+    diff = fobj.read()
 
     descr, authname, authemail, authdate = __parse_description(descr)
 
@@ -230,34 +224,34 @@ def __parse_patch(filename = None):
     # Just return None
     return (descr, authname, authemail, authdate, diff)
 
-def __import_patch(patch, filename, options):
-    """Import a patch from a file or standard input
+def __create_patch(patch, message, author_name, author_email,
+                   author_date, diff, options):
+    """Create a new patch on the stack
     """
-    # the defaults
-    message = author_name = author_email = author_date = committer_name = \
-              committer_email = None
-
-    if options.author:
-        options.authname, options.authemail = name_email(options.author)
-
-    if options.mail:
-        message, author_name, author_email, author_date, diff = \
-                 __parse_mail(filename)
-    else:
-        message, author_name, author_email, author_date, diff = \
-                 __parse_patch(filename)
-
     if not diff:
         raise CmdException, 'No diff found inside the patch'
 
     if not patch:
-        patch = make_patch_name(message, crt_series.patch_exists)
+        patch = make_patch_name(message, crt_series.patch_exists,
+                                alternative = not (options.ignore
+                                                   or options.replace))
+
+    if options.ignore and patch in crt_series.get_applied():
+        print 'Ignoring already applied patch "%s"' % patch
+        return
+    if options.replace and patch in crt_series.get_unapplied():
+        crt_series.delete_patch(patch)
 
     # refresh_patch() will invoke the editor in this case, with correct
     # patch content
     if not message:
         can_edit = False
 
+    committer_name = committer_email = None
+
+    if options.author:
+        options.authname, options.authemail = name_email(options.author)
+
     # override the automatically parsed settings
     if options.authname:
         author_name = options.authname
@@ -270,9 +264,6 @@ def __import_patch(patch, filename, options):
     if options.commemail:
         committer_email = options.commemail
 
-    if options.replace and patch in crt_series.get_unapplied():
-        crt_series.delete_patch(patch)
-
     crt_series.new_patch(patch, message = message, can_edit = False,
                          author_name = author_name,
                          author_email = author_email,
@@ -291,7 +282,32 @@ def __import_patch(patch, filename, options):
     crt_series.refresh_patch(edit = options.edit,
                              show_patch = options.showpatch)
 
-    print 'done'
+    print 'done'    
+
+def __import_file(patch, filename, options):
+    """Import a patch from a file or standard input
+    """
+    if filename:
+        f = file(filename)
+    else:
+        f = sys.stdin
+
+    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)
+
+    if filename:
+        f.close()
+
+    __create_patch(patch, message, author_name, author_email,
+                   author_date, diff, options)
 
 def __import_series(filename, options):
     """Import a series of patches
@@ -314,11 +330,33 @@ def __import_series(filename, options):
         if options.strip:
             patch = __strip_patch_name(patch)
         patch = __replace_slashes_with_dashes(patch);
-        if options.ignore and patch in applied:
-            print 'Ignoring already applied patch "%s"' % patch
-            continue
 
-        __import_patch(patch, patchfile, options)
+        __import_file(patch, patchfile, options)
+
+    if filename:
+        f.close()
+
+def __import_mbox(filename, options):
+    """Import a series from an mbox file
+    """
+    if filename:
+        f = file(filename, 'rb')
+    else:
+        f = sys.stdin
+
+    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)
+
+    if filename:
+        f.close()
 
 def func(parser, options, args):
     """Import a GNU diff file as a new patch
@@ -337,6 +375,8 @@ def func(parser, options, args):
 
     if options.series:
         __import_series(filename, options)
+    elif options.mbox:
+        __import_mbox(filename, options)
     else:
         if options.name:
             patch = options.name
@@ -347,6 +387,6 @@ def func(parser, options, args):
         if options.strip:
             patch = __strip_patch_name(patch)
 
-        __import_patch(patch, filename, options)
+        __import_file(patch, filename, options)
 
     print_crt_patch()
index 5f3f030..f5e2f32 100644 (file)
@@ -698,33 +698,40 @@ def pull(repository = 'origin', refspec = None):
     if __run(config.get('stgit', 'pullcmd'), args) != 0:
         raise GitException, 'Failed "git-pull %s"' % repository
 
-def apply_patch(filename = None, diff = None, base = None):
+def apply_patch(filename = None, diff = None, base = None,
+                fail_dump = True):
     """Apply a patch onto the current or given index. There must not
     be any local changes in the tree, otherwise the command fails
     """
-    def __apply_patch():
-        try:
-            if filename:
-                return __run('git-apply --index', [filename]) == 0
-            elif diff:
-                _input_str('git-apply --index', diff)
-            else:
-                _input('git-apply --index', sys.stdin)
-        except GitException:
-            return False
-        return True
-
     if base:
         orig_head = get_head()
         switch(base)
     else:
-        refresh_index()         # needed since __apply_patch() doesn't do it
+        refresh_index()
+
+    if diff is None:
+        if filename:
+            f = file(filename)
+        else:
+            f = sys.stdin
+        diff = f.read()
+        if filename:
+            f.close()
 
-    if not __apply_patch():
+    try:
+        _input_str('git-apply --index', diff)
+    except GitException:
         if base:
             switch(orig_head)
-        raise GitException, 'Patch does not apply cleanly'
-    elif base:
+        if fail_dump:
+            # write the failed diff to a file
+            f = file('.stgit-failed.patch', 'w+')
+            f.write(diff)
+            f.close()
+
+        raise
+
+    if base:
         top = commit(message = 'temporary commit used for applying a patch',
                      parents = [base])
         switch(orig_head)
index 2f1a8bc..a477e7d 100644 (file)
@@ -430,7 +430,7 @@ class Series:
     def patch_exists(self, name):
         """Return true if there is a patch with the given name, false
         otherwise."""
-        return self.__patch_applied(name) or self.__patch_applied(name)
+        return self.__patch_applied(name) or self.__patch_unapplied(name)
 
     def __begin_stack_check(self):
         """Save the current HEAD into .git/refs/heads/base if the stack