Add support for multipart messages to import
[stgit] / stgit / commands / imprt.py
index fdb91ea..4ecb861 100644 (file)
@@ -15,7 +15,8 @@ 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
+import sys, os, re, email
+from email.Header import decode_header, make_header
 from optparse import OptionParser, make_option
 
 from stgit.commands.common import *
 from optparse import OptionParser, make_option
 
 from stgit.commands.common import *
@@ -28,7 +29,7 @@ usage = """%prog [options] [<file>]
 
 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
 generating the patch information.
 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.
@@ -41,12 +42,24 @@ options = [make_option('-m', '--mail',
                        action = 'store_true'),
            make_option('-n', '--name',
                        help = 'use NAME as the patch name'),
                        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'),
+           make_option('--replace',
+                       help = 'replace the unapplied patches in the series',
+                       action = 'store_true'),
            make_option('-b', '--base',
                        help = 'use BASE instead of HEAD for file importing'),
            make_option('-e', '--edit',
                        help = 'invoke an editor for the patch description',
                        action = 'store_true'),
            make_option('-b', '--base',
                        help = 'use BASE instead of HEAD for file importing'),
            make_option('-e', '--edit',
                        help = 'invoke an editor for the patch description',
                        action = 'store_true'),
-           make_option('-s', '--showpatch',
+           make_option('-p', '--showpatch',
                        help = 'show the patch content in the editor buffer',
                        action = 'store_true'),
            make_option('-a', '--author', metavar = '"NAME <EMAIL>"',
                        help = 'show the patch content in the editor buffer',
                        action = 'store_true'),
            make_option('-a', '--author', metavar = '"NAME <EMAIL>"',
@@ -67,6 +80,17 @@ def __end_descr(line):
     return re.match('---\s*$', line) or re.match('diff -', line) or \
             re.match('Index: ', line)
 
     return re.match('---\s*$', line) or re.match('diff -', line) or \
             re.match('Index: ', line)
 
+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 __parse_description(descr):
     """Parse the patch description and return the new description and
     author information (if any).
 def __parse_description(descr):
     """Parse the patch description and return the new description and
     author information (if any).
@@ -78,13 +102,11 @@ def __parse_description(descr):
     if not descr_lines:
         raise CmdException, "Empty patch description"
 
     if not descr_lines:
         raise CmdException, "Empty patch description"
 
-    pos = -1
     lasthdr = 0
     end = len(descr_lines)
 
     # Parse the patch header
     lasthdr = 0
     end = len(descr_lines)
 
     # Parse the patch header
-    while pos < end:
-        pos += 1
+    for pos in range(0, end):
         if not descr_lines[pos]:
            continue
         # check for a "From|Author:" line
         if not descr_lines[pos]:
            continue
         # check for a "From|Author:" line
@@ -114,29 +136,33 @@ def __parse_mail(filename = None):
     """Parse the input file in a mail format and return (description,
     authname, authemail, authdate)
     """
     """Parse the input file in a mail format and return (description,
     authname, authemail, authdate)
     """
+    def __decode_header(header):
+        """Decode a qp-encoded e-mail header as per rfc2047"""
+        try:
+            words_enc = decode_header(header)
+            hobj = make_header(words_enc)
+        except Exception, ex:
+            raise CmdException, 'header decoding error: %s' % str(ex)
+        return unicode(hobj).encode('utf-8')
+
     if filename:
         f = file(filename)
     else:
         f = sys.stdin
 
     if filename:
         f = file(filename)
     else:
         f = sys.stdin
 
-    descr = authname = authemail = authdate = None
+    msg = email.message_from_file(f)
+
+    if filename:
+        f.close()
 
     # parse the headers
 
     # parse the headers
-    while True:
-        line = f.readline()
-        if not line:
-            break
-        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 msg.has_key('from'):
+        authname, authemail = name_email(__decode_header(msg['from']))
+    else:
+        authname = authemail = None
+
+    descr = __decode_header(msg['subject'])
+    authdate = msg['date']
 
     # remove the '[*PATCH*]' expression in the subject
     if descr:
 
     # remove the '[*PATCH*]' expression in the subject
     if descr:
@@ -146,22 +172,23 @@ def __parse_mail(filename = None):
     else:
         raise CmdException, 'Subject: line not found'
 
     else:
         raise CmdException, 'Subject: line not found'
 
-    # the rest of the patch description
-    while True:
-        line = f.readline()
-        if not line:
-            break
-        if __end_descr(line):
-            break
-        else:
-            descr += line
-    descr.rstrip()
+    # the rest of the message
+    if msg.is_multipart():
+        descr += msg.get_payload(0, decode = True)
+        diff = msg.get_payload(1, decode = True)
+    else:
+        diff = msg.get_payload(decode = True)
 
 
-    if filename:
-        f.close()
+        for line in diff.split('\n'):
+            if __end_descr(line):
+                break
+            descr += line + '\n'
+
+    descr.rstrip()
 
     # parse the description for author information
 
     # parse the description for author information
-    descr, descr_authname, descr_authemail, descr_authdate = __parse_description(descr)
+    descr, descr_authname, descr_authemail, descr_authdate = \
+           __parse_description(descr)
     if descr_authname:
         authname = descr_authname
     if descr_authemail:
     if descr_authname:
         authname = descr_authname
     if descr_authemail:
@@ -169,7 +196,7 @@ def __parse_mail(filename = None):
     if descr_authdate:
        authdate = descr_authdate
 
     if descr_authdate:
        authdate = descr_authdate
 
-    return (descr, authname, authemail, authdate)
+    return (descr, authname, authemail, authdate, diff)
 
 def __parse_patch(filename = None):
     """Parse the input file and return (description, authname,
 
 def __parse_patch(filename = None):
     """Parse the input file and return (description, authname,
@@ -192,6 +219,8 @@ def __parse_patch(filename = None):
             descr += line
     descr.rstrip()
 
             descr += line
     descr.rstrip()
 
+    diff = f.read()
+
     if filename:
         f.close()
 
     if filename:
         f.close()
 
@@ -199,30 +228,11 @@ def __parse_patch(filename = None):
 
     # we don't yet have an agreed place for the creation date.
     # Just return None
 
     # we don't yet have an agreed place for the creation date.
     # Just return None
-    return (descr, authname, authemail, authdate)
+    return (descr, authname, authemail, authdate, diff)
 
 
-def func(parser, options, args):
-    """Import a GNU diff file as a new patch
+def __import_patch(patch, filename, options):
+    """Import a patch from a file or standard input
     """
     """
-    if len(args) > 1:
-        parser.error('incorrect number of arguments')
-
-    check_local_changes()
-    check_conflicts()
-    check_head_top_equal()
-
-    if len(args) == 1:
-        filename = args[0]
-    else:
-        filename = None
-
-    if options.name:
-        patch = options.name
-    elif filename:
-        patch = os.path.basename(filename)
-    else:
-        raise CmdException, 'Unkown patch name'
-
     # the defaults
     message = author_name = author_email = author_date = committer_name = \
               committer_email = None
     # the defaults
     message = author_name = author_email = author_date = committer_name = \
               committer_email = None
@@ -231,12 +241,18 @@ def func(parser, options, args):
         options.authname, options.authemail = name_email(options.author)
 
     if options.mail:
         options.authname, options.authemail = name_email(options.author)
 
     if options.mail:
-        message, author_name, author_email, author_date = \
+        message, author_name, author_email, author_date, diff = \
                  __parse_mail(filename)
     else:
                  __parse_mail(filename)
     else:
-        message, author_name, author_email, author_date = \
+        message, author_name, author_email, author_date, diff = \
                  __parse_patch(filename)
 
                  __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)
+
     # refresh_patch() will invoke the editor in this case, with correct
     # patch content
     if not message:
     # refresh_patch() will invoke the editor in this case, with correct
     # patch content
     if not message:
@@ -254,6 +270,9 @@ def func(parser, options, args):
     if options.commemail:
         committer_email = options.commemail
 
     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,
     crt_series.new_patch(patch, message = message, can_edit = False,
                          author_name = author_name,
                          author_email = author_email,
@@ -261,16 +280,73 @@ def func(parser, options, args):
                          committer_name = committer_name,
                          committer_email = committer_email)
 
                          committer_name = committer_name,
                          committer_email = committer_email)
 
-    print 'Importing patch %s...' % patch,
+    print 'Importing patch "%s"...' % patch,
     sys.stdout.flush()
 
     if options.base:
     sys.stdout.flush()
 
     if options.base:
-        git.apply_patch(filename, git_id(options.base))
+        git.apply_patch(diff = diff, base = git_id(options.base))
     else:
     else:
-        git.apply_patch(filename)
+        git.apply_patch(diff = diff)
 
     crt_series.refresh_patch(edit = options.edit,
                              show_patch = options.showpatch)
 
     print 'done'
 
     crt_series.refresh_patch(edit = options.edit,
                              show_patch = options.showpatch)
 
     print 'done'
+
+def __import_series(filename, options):
+    """Import a series of patches
+    """
+    applied = crt_series.get_applied()
+
+    if filename:
+        f = file(filename)
+        patchdir = os.path.dirname(filename)
+    else:
+        f = sys.stdin
+        patchdir = ''
+
+    for line in f:
+        patch = re.sub('#.*$', '', line).strip()
+        if not patch:
+            continue
+        patchfile = os.path.join(patchdir, patch)
+
+        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)
+
+def func(parser, options, args):
+    """Import a GNU diff file as a new patch
+    """
+    if len(args) > 1:
+        parser.error('incorrect number of arguments')
+
+    check_local_changes()
+    check_conflicts()
+    check_head_top_equal()
+
+    if len(args) == 1:
+        filename = args[0]
+    else:
+        filename = None
+
+    if options.series:
+        __import_series(filename, options)
+    else:
+        if options.name:
+            patch = options.name
+        elif filename:
+            patch = os.path.basename(filename)
+        else:
+            patch = ''
+        if options.strip:
+            patch = __strip_patch_name(patch)
+
+        __import_patch(patch, filename, options)
+
     print_crt_patch()
     print_crt_patch()