Commits

Patrick Mézard committed 999fa67

Create mbox extension

Comments (0)

Files changed (2)

+syntax: glob
+
+*.elc
+*.orig
+*.rej
+*~
+*.mergebackup
+*.o
+*.so
+*.pyd
+*.pyc
+*.swp
+*.prof
+tests/.coverage*
+tests/annotated
+tests/*.err
+build
+contrib/hgsh/hgsh
+dist
+doc/*.[0-9]
+doc/*.[0-9].gendoc.txt
+doc/*.[0-9].{x,ht}ml
+MANIFEST
+patches
+mercurial/__version__.py
+Output/Mercurial-*.exe
+.DS_Store
+tags
+cscope.*
+
+syntax: regexp
+^\.pc/
+^\.(pydev)?project
+# mbox.py - qimport patches from mailboxes
+#
+# Copyright 2008 Patrick Mezard <pmezard@gmail.com>
+#
+# This program is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the
+# Free Software Foundation; either version 2 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
+# Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+'''qimport patches from mailboxes
+
+This extension let you read patches from mailboxes and append them to an
+existing Mercurial Queue as the qimport command would do. Patch selection
+is done interactively.
+
+To enable this extension:
+
+  [extensions]
+  hgext.mbox =
+  # MQ extension must be enabled as well
+  hgext.mq =
+
+  [mbox]
+  # A list of mailboxes paths separated by the platform specific path separator,
+  # colon on unixes, semi-colon on Windows.
+  paths = mailbox1_path;mailbox_path2
+'''
+
+import mailbox, os, re
+from mercurial import commands, hg, util, extensions, tempfile
+from mercurial.i18n import gettext, _
+
+class mboxerror(Exception):
+    pass
+
+re_patch = re.compile(r'^\s*\[PATCH\s+(?:(\d+)\s+of\s+(\d+))\]\s*(.+)$')
+
+def parsesubject(s):
+    s = s.replace('\n\t', ' ')
+    m = re_patch.search(s)
+    if not m:
+        raise mboxerror(_('does not look like a patch message'))
+    try:
+        index = int(m.group(1))
+        count = int(m.group(2)) + 1
+    except IndexError:
+        index = 0
+        count = 1
+    if count < 1:
+        raise mboxerror(_('invalid patch count: %d') % count)
+    title = m.group(3)
+    return s, title, index, count
+
+class patchmessage:
+    def __init__(self, msg):
+        self.msg = msg
+
+        s = msg.get('Subject', '')
+        self.subject, self.title, self.index, self.count = parsesubject(s)
+
+        # Extract threading information
+        self.id = msg.get('Message-ID')
+        if not self.id:
+            raise mboxerror(_('no message id'))
+        self.parentid = msg.get('In-Reply-to')
+        if not self.parentid:
+            self.parentid = msg.get_all('References', [None])[-1]
+
+    def __cmp__(self, other):
+        return cmp(self.id, other.id)
+
+    def __hash__(self):
+        return hash(self.id)
+
+def listmessages(paths):
+    for path in paths:
+        mbox = mailbox.mbox(path, create=False)
+        for m in mbox:
+            try:
+                msg = patchmessage(m)
+            except mboxerror:
+                continue
+            yield msg
+
+def listgroups(paths):
+    pendings = {}
+    for m in listmessages(paths):
+        if m.count == 1:
+            yield None, [m]
+            continue
+        msgid = m.parentid
+        if m.index == 0:
+            msgid = m.id
+        if not msgid:
+            continue
+        pendings.setdefault(msgid, []).append(m)
+        msgs = pendings[msgid]
+        if len(msgs) != m.count:
+            continue
+        msgs = [(m.index, m) for m in msgs]
+        msgs.sort()
+        msgs = [m[1] for m in msgs]
+        del pendings[msgid]
+        yield msgs[0], msgs[1:]
+
+def makepatchname(existing, title):
+    def cleanup(s):
+        s = re.sub('\s', '_', s.lower())
+        s = re.sub('\W', '', s)
+        return s
+
+    name = cleanup(title)
+    for i in xrange(100):
+        if i:
+            n = '%s__%d' % (name, i)
+        else:
+            n = name
+        if n not in existing:
+            return n
+
+def importpatch(ui, repo, patchname, msg):
+    try:
+        mq = extensions.find('mq')
+    except KeyError:
+        raise util.Abort(_("'mq' extension not loaded"))
+
+    s = msg.get_payload()
+    s = re.sub('\r\n', '\n', s)
+    tmpfd, tmppath = tempfile.mkstemp(prefix='hg-mbox-')
+    try:
+        try:
+            fp = os.fdopen(tmpfd, 'wb')
+            fp.write(s)
+            fp.close()
+            tmpfd = None
+        except IOError:
+            if tmpfd:
+                os.close(tmpfd)
+            raise
+
+        mq.qimport(ui, repo, tmppath, name=patchname, existing=False,
+                   force=False, rev=[], git=False)
+    finally:
+        os.remove(tmppath)
+        
+def importpatches(ui, repo, groups):
+    imported = []
+    q = repo.mq
+    for patches in groups[::-1]:
+        for p in patches[::-1]:
+            name = makepatchname(q.series, p.title)
+            importpatch(ui, repo, name, p.msg)
+            imported.append(name)
+    ui.status(_('%d patches imported\n') % len(imported))
+
+def mimport(ui, repo, **opts):
+    """qimport patches from mailboxes
+
+    You will be prompted for whether to qimport items from every patch group.
+    For each query, the following responses are possible:
+
+    y - skip this patch group
+    i - qimport this patch group
+
+    d - done, import selected patches and quit
+    q - quit, importing nothing
+
+    ? - display help
+    """
+    paths = ui.config('mbox', 'paths', '').split(os.pathsep)
+    paths = [p.strip() for p in paths if p]
+    if not paths:
+        raise util.Abort(_('no mailbox path configured'))
+
+    groups = []
+    stop = False
+    for intro, patches in listgroups(paths):
+        if intro:
+            ui.status('%s\n' % intro.subject)
+            for p in patches:
+                ui.status('    %s\n' % p.subject)
+        else:
+            ui.status('%s\n' % patches[0].subject)
+
+        while 1:
+            choices = _('[Yidq?]')
+            r = ui.prompt(_('skip this group %s ') % choices, 
+                          '(?i)%s?$' % choices) or 'y'
+            if r == _('?'):
+                doc = gettext(mimport.__doc__)
+                c = doc.find(_('y - skip this patch group'))
+                for l in doc[c:].splitlines():
+                    if l:
+                        ui.write(l.strip(), '\n')
+                continue
+            if r == _('i'):
+                groups.append(patches)
+            elif r == _('d'):
+                stop = True
+            elif r == _('q'):
+                raise util.Abort(_('user quit'))
+            break
+
+        if stop:
+            break
+
+    importpatches(ui, repo, groups)
+
+cmdtable = {}
+
+def extsetup():
+    try:
+        mq = extensions.find('mq')
+    except KeyError:
+        return
+
+    cmdtable['mimport'] = (mimport, [], _('hg mimport'))