changeset 896:01215ad04283

Merge with BOS
author mpm@selenic.com
date Sat, 13 Aug 2005 19:43:42 -0800
parents 0cd2ee61b10a (current diff) 77b52b864249 (diff)
children fe30f5434b51
files .hgignore CONTRIBUTORS TODO contrib/patchbomb doc/hg.1.txt mercurial/bdiff.c mercurial/commands.py mercurial/hg.py mercurial/hgweb.py mercurial/revlog.py mercurial/util.py templates/map tests/test-help tests/test-help.out tests/test-merge-revert.out tests/test-merge-revert2 tests/test-merge-revert2.out tests/test-walk tests/test-walk.out
diffstat 7 files changed, 598 insertions(+), 89 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/contrib/patchbomb	Sat Aug 13 19:43:42 2005 -0800
@@ -0,0 +1,247 @@
+#!/usr/bin/python
+#
+# Interactive script for sending a collection of Mercurial changesets
+# as a series of patch emails.
+#
+# The series is started off with a "[PATCH 0 of N]" introduction,
+# which describes the series as a whole.
+#
+# Each patch email has a Subject line of "[PATCH M of N] ...", using
+# the first line of the changeset description as the subject text.
+# The message contains two or three body parts:
+#
+#   The remainder of the changeset description.
+#
+#   [Optional] If the diffstat program is installed, the result of
+#   running diffstat on the patch.
+#
+#   The patch itself, as generated by "hg export".
+#
+# Each message refers to all of its predecessors using the In-Reply-To
+# and References headers, so they will show up as a sequence in
+# threaded mail and news readers, and in mail archives.
+#
+# For each changeset, you will be prompted with a diffstat summary and
+# the changeset summary, so you can be sure you are sending the right
+# changes.
+#
+# It is best to run this script with the "-n" (test only) flag before
+# firing it up "for real", in which case it will use your pager to
+# display each of the messages that it would send.
+#
+# To configure a default mail host, add a section like this to your
+# hgrc file:
+#
+# [smtp]
+# host = my_mail_host
+# port = 1025
+#
+# To configure other defaults, add a section like this to your hgrc
+# file:
+#
+# [patchbomb]
+# from = My Name <my@email>
+# to = recipient1, recipient2, ...
+# cc = cc1, cc2, ...
+
+from email.MIMEMultipart import MIMEMultipart
+from email.MIMEText import MIMEText
+from mercurial import commands
+from mercurial import fancyopts
+from mercurial import hg
+from mercurial import ui
+import os
+import popen2
+import readline
+import smtplib
+import socket
+import sys
+import tempfile
+import time
+
+def diffstat(patch):
+    fd, name = tempfile.mkstemp()
+    try:
+        p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
+        try:
+            for line in patch: print >> p.tochild, line
+            p.tochild.close()
+            if p.wait(): return
+            fp = os.fdopen(fd, 'r')
+            stat = []
+            for line in fp: stat.append(line.lstrip())
+            last = stat.pop()
+            stat.insert(0, last)
+            stat = ''.join(stat)
+            if stat.startswith('0 files'): raise ValueError
+            return stat
+        except: raise
+    finally:
+        try: os.unlink(name)
+        except: pass
+
+def patchbomb(ui, repo, *revs, **opts):
+    def prompt(prompt, default = None, rest = ': ', empty_ok = False):
+        if default: prompt += ' [%s]' % default
+        prompt += rest
+        while True:
+            r = raw_input(prompt)
+            if r: return r
+            if default is not None: return default
+            if empty_ok: return r
+            ui.warn('Please enter a valid value.\n')
+
+    def confirm(s):
+        if not prompt(s, default = 'y', rest = '? ').lower().startswith('y'):
+            raise ValueError
+
+    def cdiffstat(summary, patch):
+        s = diffstat(patch)
+        if s:
+            if summary:
+                ui.write(summary, '\n')
+                ui.write(s, '\n')
+            confirm('Does the diffstat above look okay')
+        return s
+
+    def makepatch(patch, idx, total):
+        desc = []
+        node = None
+        for line in patch:
+            if line.startswith('#'):
+                if line.startswith('# Node ID'): node = line.split()[-1]
+                continue
+            if line.startswith('diff -r'): break
+            desc.append(line)
+        if not node: raise ValueError
+        body = ('\n'.join(desc[1:]).strip() or
+                'Patch subject is complete summary.')
+        body += '\n\n\n'
+        if opts['diffstat']:
+            body += cdiffstat('\n'.join(desc), patch) + '\n\n'
+        body += '\n'.join(patch)
+        msg = MIMEText(body)
+        subj = '[PATCH %d of %d] %s' % (idx, total, desc[0].strip())
+        if subj.endswith('.'): subj = subj[:-1]
+        msg['Subject'] = subj
+        msg['X-Mercurial-Node'] = node
+        return msg
+
+    start_time = int(time.time())
+
+    def genmsgid(id):
+        return '<%s.%s@%s>' % (id[:20], start_time, socket.getfqdn())
+
+    patches = []
+
+    class exportee:
+        def __init__(self, container):
+            self.lines = []
+            self.container = container
+            self.name = 'email'
+
+        def write(self, data):
+            self.lines.append(data)
+
+        def close(self):
+            self.container.append(''.join(self.lines).split('\n'))
+            self.lines = []
+
+    commands.export(ui, repo, *args, **{'output': exportee(patches)})
+
+    jumbo = []
+    msgs = []
+
+    ui.write('This patch series consists of %d patches.\n\n' % len(patches))
+
+    for p, i in zip(patches, range(len(patches))):
+        jumbo.extend(p)
+        msgs.append(makepatch(p, i + 1, len(patches)))
+
+    ui.write('\nWrite the introductory message for the patch series.\n\n')
+
+    sender = (opts['from'] or ui.config('patchbomb', 'from') or
+              prompt('From', ui.username()))
+
+    msg = MIMEMultipart()
+    msg['Subject'] = '[PATCH 0 of %d] %s' % (
+        len(patches),
+        opts['subject'] or
+        prompt('Subject:', rest = ' [PATCH 0 of %d] ' % len(patches)))
+    to = (opts['to'] or ui.config('patchbomb', 'to') or
+          [s.strip() for s in prompt('To').split(',')])
+    cc = (opts['cc'] or ui.config('patchbomb', 'cc') or
+          [s.strip() for s in prompt('Cc', default = '').split(',')])
+
+    ui.write('Finish with ^D or a dot on a line by itself.\n\n')
+
+    body = []
+
+    while True:
+        try: l = raw_input()
+        except EOFError: break
+        if l == '.': break
+        body.append(l)
+
+    msg.attach(MIMEText('\n'.join(body) + '\n'))
+
+    ui.write('\n')
+
+    d = cdiffstat('Final summary:\n', jumbo)
+    if d: msg.attach(MIMEText(d))
+
+    msgs.insert(0, msg)
+
+    if not opts['test']:
+        s = smtplib.SMTP()
+        s.connect(host = ui.config('smtp', 'host', 'mail'),
+                  port = int(ui.config('smtp', 'port', 25)))
+
+    parent = None
+    tz = time.strftime('%z')
+    for m in msgs:
+        try:
+            m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
+        except TypeError:
+            m['Message-Id'] = genmsgid('patchbomb')
+        if parent:
+            m['In-Reply-To'] = parent
+        else:
+            parent = m['Message-Id']
+        m['Date'] = time.strftime('%a, %e %b %Y %T ', time.localtime(start_time)) + tz
+        start_time += 1
+        m['From'] = sender
+        m['To'] = ', '.join(to)
+        if cc: m['Cc'] = ', '.join(cc)
+        ui.status('Sending ', m['Subject'], ' ...\n')
+        if opts['test']:
+            fp = os.popen(os.getenv('PAGER', 'more'), 'w')
+            fp.write(m.as_string(0))
+            fp.write('\n')
+            fp.close()
+        else:
+            s.sendmail(sender, to + cc, m.as_string(0))
+    if not opts['test']:
+        s.close()
+
+if __name__ == '__main__':
+    optspec = [('c', 'cc', [], 'email addresses of copy recipients'),
+               ('d', 'diffstat', None, 'add diffstat output to messages'),
+               ('f', 'from', '', 'email address of sender'),
+               ('n', 'test', None, 'print messages that would be sent'),
+               ('s', 'subject', '', 'subject of introductory message'),
+               ('t', 'to', [], 'email addresses of recipients')]
+    options = {}
+    try:
+        args = fancyopts.fancyopts(sys.argv[1:], commands.globalopts + optspec,
+                                   options)
+    except fancyopts.getopt.GetoptError, inst:
+        u = ui.ui()
+        u.warn('error: %s' % inst)
+        sys.exit(1)
+
+    u = ui.ui(options["verbose"], options["debug"], options["quiet"],
+              not options["noninteractive"])
+    repo = hg.repository(ui = u)
+
+    patchbomb(u, repo, *args, **options)
--- a/mercurial/commands.py	Sat Aug 13 12:41:00 2005 -0800
+++ b/mercurial/commands.py	Sat Aug 13 19:43:42 2005 -0800
@@ -14,9 +14,6 @@
 class UnknownCommand(Exception):
     """Exception raised if command is not in the command table."""
 
-class Abort(Exception):
-    """Raised if a command needs to print an error and exit."""
-
 def filterfiles(filters, files):
     l = [x for x in files if x in filters]
 
@@ -35,30 +32,19 @@
 def relpath(repo, args):
     cwd = repo.getcwd()
     if cwd:
-        return [util.pconvert(os.path.normpath(os.path.join(cwd, x)))
-                for x in args]
+        return [util.normpath(os.path.join(cwd, x)) for x in args]
     return args
 
-def matchpats(cwd, pats = [], opts = {}, head = ''):
-    return util.matcher(cwd, pats or ['.'], opts.get('include'),
+def matchpats(repo, cwd, pats = [], opts = {}, head = ''):
+    return util.matcher(repo, cwd, pats or ['.'], opts.get('include'),
                         opts.get('exclude'), head)
 
-def pathto(n1, n2):
-    '''return the relative path from one place to another'''
-    if not n1: return n2
-    a, b = n1.split(os.sep), n2.split(os.sep)
-    a.reverse(), b.reverse()
-    while a and b and a[-1] == b[-1]:
-        a.pop(), b.pop()
-    b.reverse()
-    return os.sep.join((['..'] * len(a)) + b)
-
 def makewalk(repo, pats, opts, head = ''):
     cwd = repo.getcwd()
-    files, matchfn = matchpats(cwd, pats, opts, head)
+    files, matchfn = matchpats(repo, cwd, pats, opts, head)
     def walk():
         for src, fn in repo.walk(files = files, match = matchfn):
-            yield src, fn, pathto(cwd, fn)
+            yield src, fn, util.pathto(cwd, fn)
     return files, matchfn, walk()
 
 def walk(repo, pats, opts, head = ''):
@@ -89,7 +75,7 @@
                 try:
                     num = revlog.rev(revlog.lookup(val))
                 except KeyError:
-                    raise Abort('invalid revision identifier %s', val)
+                    raise util.Abort('invalid revision identifier %s', val)
         return num
     for spec in revs:
         if spec.find(revrangesep) >= 0:
@@ -144,7 +130,7 @@
             i += 1
         return ''.join(newname)
     except KeyError, inst:
-        raise Abort("invalid format spec '%%%s' in output file name",
+        raise util.Abort("invalid format spec '%%%s' in output file name",
                     inst.args[0])
 
 def make_file(repo, r, pat, node=None,
@@ -396,11 +382,10 @@
     q = dict(zip(pats, pats))
     add, remove = [], []
     for src, abs, rel in walk(repo, pats, opts):
-        if src == 'f':
-            if repo.dirstate.state(abs) == '?':
-                add.append(abs)
-                if rel not in q: ui.status('adding ', rel, '\n')
-        elif repo.dirstate.state(abs) != 'r' and not os.path.exists(rel):
+        if src == 'f' and repo.dirstate.state(abs) == '?':
+            add.append(abs)
+            if rel not in q: ui.status('adding ', rel, '\n')
+        if repo.dirstate.state(abs) != 'r' and not os.path.exists(rel):
             remove.append(abs)
             if rel not in q: ui.status('removing ', rel, '\n')
     repo.add(add)
@@ -427,7 +412,7 @@
             return name
 
     if not pats:
-        raise Abort('at least one file name or pattern required')
+        raise util.Abort('at least one file name or pattern required')
 
     bcache = {}
     opmap = [['user', getname], ['number', str], ['changeset', getnode]]
@@ -478,6 +463,8 @@
         ui.warn("abort: destination '%s' already exists\n" % dest)
         return 1
 
+    dest = os.path.realpath(dest)
+
     class Dircleanup:
         def __init__(self, dir_):
             self.rmtree = shutil.rmtree
@@ -541,7 +528,7 @@
     if not pats and cwd:
         opts['include'] = [os.path.join(cwd, i) for i in opts['include']]
         opts['exclude'] = [os.path.join(cwd, x) for x in opts['exclude']]
-    fns, match = matchpats((pats and repo.getcwd()) or '', pats, opts)
+    fns, match = matchpats(repo, (pats and repo.getcwd()) or '', pats, opts)
     if pats:
         c, a, d, u = repo.changes(files = fns, match = match)
         files = c + a + [fn for fn in d if repo.dirstate.state(fn) == 'r']
@@ -583,7 +570,7 @@
             ui.warn("%s in manifest1, but listed as state %s" % (f, state))
             errors += 1
     if errors:
-        raise Abort(".hg/dirstate inconsistent with current parent's manifest")
+        raise util.Abort(".hg/dirstate inconsistent with current parent's manifest")
 
 def debugstate(ui, repo):
     """show the contents of the current dirstate"""
@@ -621,6 +608,7 @@
 
 def debugwalk(ui, repo, *pats, **opts):
     items = list(walk(repo, pats, opts))
+    if not items: return
     fmt = '%%s  %%-%ds  %%s' % max([len(abs) for (src, abs, rel) in items])
     for i in items: print fmt % i
 
@@ -631,12 +619,14 @@
         revs = map(lambda x: repo.lookup(x), opts['rev'])
 
     if len(revs) > 2:
-        raise Abort("too many revisions to diff")
+        raise util.Abort("too many revisions to diff")
 
     files = []
-    roots, match, results = makewalk(repo, pats, opts)
-    for src, abs, rel in results:
-        files.append(abs)
+    match = util.always
+    if pats:
+        roots, match, results = makewalk(repo, pats, opts)
+        for src, abs, rel in results:
+            files.append(abs)
     dodiff(sys.stdout, ui, repo, files, *revs, **{'match': match})
 
 def doexport(ui, repo, changeset, seqno, total, revwidth, opts):
@@ -665,7 +655,7 @@
 def export(ui, repo, *changesets, **opts):
     """dump the header and diffs for one or more changesets"""
     if not changesets:
-        raise Abort("export requires at least one changeset")
+        raise util.Abort("export requires at least one changeset")
     seqno = 0
     revs = list(revrange(ui, repo, changesets))
     total = len(revs)
@@ -762,7 +752,7 @@
                     files.append(pf)
         patcherr = f.close()
         if patcherr:
-            raise Abort("patch failed")
+            raise util.Abort("patch failed")
 
         if len(files) > 0:
             addremove(ui, repo, *files)
@@ -772,7 +762,7 @@
     """create a new repository in the current directory"""
 
     if source:
-        raise Abort("no longer supported: use \"hg clone\" instead")
+        raise util.Abort("no longer supported: use \"hg clone\" instead")
     hg.repository(ui, ".", create=1)
 
 def locate(ui, repo, *pats, **opts):
@@ -1078,8 +1068,8 @@
     '''
 
     cwd = repo.getcwd()
-    files, matchfn = matchpats(cwd, pats, opts)
-    (c, a, d, u) = [[pathto(cwd, x) for x in n]
+    files, matchfn = matchpats(repo, cwd, pats, opts)
+    (c, a, d, u) = [[util.pathto(cwd, x) for x in n]
                     for n in repo.changes(files=files, match=matchfn)]
 
     changetypes = [('modified', 'M', c),
@@ -1471,8 +1461,6 @@
             if options['traceback']:
                 traceback.print_exc()
             raise
-    except util.CommandError, inst:
-        u.warn("abort: %s\n" % inst.args)
     except hg.RepoError, inst:
         u.warn("abort: ", inst, "!\n")
     except SignalInterrupt:
@@ -1500,7 +1488,7 @@
             u.warn("abort: %s: %s\n" % (inst.strerror, inst.filename))
         else:
             u.warn("abort: %s\n" % inst.strerror)
-    except Abort, inst:
+    except util.Abort, inst:
         u.warn('abort: ', inst.args[0] % inst.args[1:], '\n')
         sys.exit(1)
     except TypeError, inst:
--- a/mercurial/hg.py	Sat Aug 13 12:41:00 2005 -0800
+++ b/mercurial/hg.py	Sat Aug 13 19:43:42 2005 -0800
@@ -10,8 +10,8 @@
 from revlog import *
 from demandload import *
 demandload(globals(), "re lock urllib urllib2 transaction time socket")
-demandload(globals(), "tempfile httprangereader bdiff urlparse stat")
-demandload(globals(), "bisect select")
+demandload(globals(), "tempfile httprangereader bdiff urlparse")
+demandload(globals(), "bisect errno select stat")
 
 class filelog(revlog):
     def __init__(self, opener, path):
@@ -300,6 +300,11 @@
     def wjoin(self, f):
         return os.path.join(self.root, f)
 
+    def getcwd(self):
+        cwd = os.getcwd()
+        if cwd == self.root: return ''
+        return cwd[len(self.root) + 1:]
+
     def ignore(self, f):
         if not self.ignorefunc:
             bigpat = []
@@ -307,14 +312,14 @@
                 l = file(self.wjoin(".hgignore"))
                 for pat in l:
                     if pat != "\n":
-                        p = util.pconvert(pat[:-1])
+			p = pat[:-1]
                         try:
-                            r = re.compile(p)
+                            re.compile(p)
                         except:
                             self.ui.warn("ignoring invalid ignore"
                                          + " regular expression '%s'\n" % p)
                         else:
-                            bigpat.append(util.pconvert(pat[:-1]))
+                            bigpat.append(p)
             except IOError: pass
 
             if bigpat:
@@ -437,22 +442,69 @@
             st.write(e + f)
         self.dirty = 0
 
-    def walk(self, files = None, match = util.always):
+    def filterfiles(self, files):
+        ret = {}
+        unknown = []
+
+        for x in files:
+            if x is '.':
+                return self.map.copy()
+            if x not in self.map:
+                unknown.append(x)
+            else:
+                ret[x] = self.map[x]
+                
+        if not unknown:
+            return ret
+
+        b = self.map.keys()
+        b.sort()
+        blen = len(b)
+
+        for x in unknown:
+            bs = bisect.bisect(b, x)
+            if bs != 0 and  b[bs-1] == x: 
+                ret[x] = self.map[x]
+                continue
+            while bs < blen:
+                s = b[bs]
+                if len(s) > len(x) and s.startswith(x) and s[len(x)] == '/':
+                    ret[s] = self.map[s]
+                else:
+                    break
+                bs += 1
+        return ret
+
+    def walk(self, files = None, match = util.always, dc=None):
         self.read()
-        dc = self.map.copy()
+
         # walk all files by default
-        if not files: files = [self.root]
+        if not files:
+            files = [self.root]
+            if not dc:
+                dc = self.map.copy()
+        elif not dc:
+            dc = self.filterfiles(files)
+                    
         known = {'.hg': 1}
         def seen(fn):
             if fn in known: return True
             known[fn] = 1
         def traverse():
-            for f in util.unique(files):
-                f = os.path.join(self.root, f)
-                if os.path.isdir(f):
+            for ff in util.unique(files):
+                f = os.path.join(self.root, ff)
+                try:
+                    st = os.stat(f)
+                except OSError, inst:
+                    if ff not in dc: self.ui.warn('%s: %s\n' % (
+                        util.pathto(self.getcwd(), ff),
+                        inst.strerror))
+                    continue
+                if stat.S_ISDIR(st.st_mode):
                     for dir, subdirs, fl in os.walk(f):
                         d = dir[len(self.root) + 1:]
-                        nd = os.path.normpath(d)
+                        nd = util.normpath(d)
+                        if nd == '.': nd = ''
                         if seen(nd):
                             subdirs[:] = []
                             continue
@@ -465,8 +517,18 @@
                         for fn in fl:
                             fn = util.pconvert(os.path.join(d, fn))
                             yield 'f', fn
+                elif stat.S_ISREG(st.st_mode):
+                    yield 'f', ff
                 else:
-                    yield 'f', f[len(self.root) + 1:]
+                    kind = 'unknown'
+                    if stat.S_ISCHR(st.st_mode): kind = 'character device'
+                    elif stat.S_ISBLK(st.st_mode): kind = 'block device'
+                    elif stat.S_ISFIFO(st.st_mode): kind = 'fifo'
+                    elif stat.S_ISLNK(st.st_mode): kind = 'symbolic link'
+                    elif stat.S_ISSOCK(st.st_mode): kind = 'socket'
+                    self.ui.warn('%s: unsupported file type (type is %s)\n' % (
+                        util.pathto(self.getcwd(), ff),
+                        kind))
 
             ks = dc.keys()
             ks.sort()
@@ -477,22 +539,23 @@
         # not in .hgignore
 
         for src, fn in util.unique(traverse()):
-            fn = os.path.normpath(fn)
+            fn = util.normpath(fn)
             if seen(fn): continue
-            if fn in dc:
-                del dc[fn]
-            elif self.ignore(fn):
+            if fn not in dc and self.ignore(fn):
                 continue
             if match(fn):
                 yield src, fn
 
     def changes(self, files=None, match=util.always):
         self.read()
-        dc = self.map.copy()
+        if not files:
+            dc = self.map.copy()
+        else:
+            dc = self.filterfiles(files)
         lookup, modified, added, unknown = [], [], [], []
         removed, deleted = [], []
 
-        for src, fn in self.walk(files, match):
+        for src, fn in self.walk(files, match, dc=dc):
             try:
                 s = os.stat(os.path.join(self.root, fn))
             except OSError:
@@ -697,9 +760,7 @@
         return filelog(self.opener, f)
 
     def getcwd(self):
-        cwd = os.getcwd()
-        if cwd == self.root: return ''
-        return cwd[len(self.root) + 1:]
+        return self.dirstate.getcwd()
 
     def wfile(self, f, mode='r'):
         return self.wopener(f, mode)
--- a/mercurial/hgweb.py	Sat Aug 13 12:41:00 2005 -0800
+++ b/mercurial/hgweb.py	Sat Aug 13 19:43:42 2005 -0800
@@ -708,7 +708,12 @@
     import BaseHTTPServer
 
     class IPv6HTTPServer(BaseHTTPServer.HTTPServer):
-        address_family = socket.AF_INET6
+        address_family = getattr(socket, 'AF_INET6', None)
+
+        def __init__(self, *args, **kwargs):
+            if self.address_family is None:
+                raise RepoError('IPv6 not available on this system')
+            BaseHTTPServer.HTTPServer.__init__(self, *args, **kwargs)
 
     class hgwebhandler(BaseHTTPServer.BaseHTTPRequestHandler):
         def log_error(self, format, *args):
--- a/mercurial/util.py	Sat Aug 13 12:41:00 2005 -0800
+++ b/mercurial/util.py	Sat Aug 13 19:43:42 2005 -0800
@@ -16,7 +16,8 @@
             seen[f] = 1
             yield f
 
-class CommandError(Exception): pass
+class Abort(Exception):
+    """Raised if a command needs to print an error and exit."""
 
 def always(fn): return True
 def never(fn): return False
@@ -68,36 +69,52 @@
 
 _globchars = {'[': 1, '{': 1, '*': 1, '?': 1}
 
-def matcher(cwd, names, inc, exc, head = ''):
+def pathto(n1, n2):
+    '''return the relative path from one place to another.
+    this returns a path in the form used by the local filesystem, not hg.'''
+    if not n1: return localpath(n2)
+    a, b = n1.split('/'), n2.split('/')
+    a.reverse(), b.reverse()
+    while a and b and a[-1] == b[-1]:
+        a.pop(), b.pop()
+    b.reverse()
+    return os.sep.join((['..'] * len(a)) + b)
+
+def canonpath(repo, cwd, myname):
+    rootsep = repo.root + os.sep
+    name = myname
+    if not name.startswith(os.sep):
+        name = os.path.join(repo.root, cwd, name)
+    name = os.path.normpath(name)
+    if name.startswith(rootsep):
+        return pconvert(name[len(rootsep):])
+    elif name == repo.root:
+        return ''
+    else:
+        raise Abort('%s not under repository root' % myname)
+    
+def matcher(repo, cwd, names, inc, exc, head = ''):
     def patkind(name):
-        for prefix in 're:', 'glob:', 'path:':
+        for prefix in 're:', 'glob:', 'path:', 'relpath:':
             if name.startswith(prefix): return name.split(':', 1)
         for c in name:
             if c in _globchars: return 'glob', name
         return 'relpath', name
 
-    cwdsep = cwd + os.sep
-
-    def regex(name, tail):
+    def regex(kind, name, tail):
         '''convert a pattern into a regular expression'''
-        kind, name = patkind(name)
         if kind == 're':
             return name
         elif kind == 'path':
-            return '^' + re.escape(name) + '$'
-        if cwd: name = os.path.join(cwdsep, name)
-        name = os.path.normpath(name)
-        if name == '.': name = '**'
+            return '^' + re.escape(name) + '(?:/|$)'
+        elif kind == 'relpath':
+            return head + re.escape(name) + tail
         return head + globre(name, '', tail)
 
-    def under(fn):
-        """check if fn is under our cwd"""
-        return not cwd or fn.startswith(cwdsep)
-
     def matchfn(pats, tail):
         """build a matching function from a set of patterns"""
         if pats:
-            pat = '(?:%s)' % '|'.join([regex(p, tail) for p in pats])
+            pat = '(?:%s)' % '|'.join([regex(k, p, tail) for (k, p) in pats])
             return re.compile(pat).match
 
     def globprefix(pat):
@@ -106,18 +123,29 @@
         for p in pat.split(os.sep):
             if patkind(p)[0] == 'glob': break
             root.append(p)
-        return os.sep.join(root)
+        return '/'.join(root)
 
-    patkinds = map(patkind, names)
-    pats = [name for (kind, name) in patkinds if kind != 'relpath']
-    files = [name for (kind, name) in patkinds if kind == 'relpath']
-    roots = filter(None, map(globprefix, pats)) + files
-    if cwd: roots = [cwdsep + r for r in roots]
+    pats = []
+    files = []
+    roots = []
+    for kind, name in map(patkind, names):
+        if kind in ('glob', 'relpath'):
+            name = canonpath(repo, cwd, name)
+            if name == '':
+                kind, name = 'glob', '**'
+        if kind in ('glob', 'path', 're'):
+            pats.append((kind, name))
+        if kind == 'glob':
+            root = globprefix(name)
+            if root: roots.append(root)
+        elif kind == 'relpath':
+            files.append((kind, name))
+            roots.append(name)
         
     patmatch = matchfn(pats, '$') or always
     filematch = matchfn(files, '(?:/|$)') or always
-    incmatch = matchfn(inc, '(?:/|$)') or always
-    excmatch = matchfn(exc, '(?:/|$)') or (lambda fn: False)
+    incmatch = matchfn(map(patkind, inc), '(?:/|$)') or always
+    excmatch = matchfn(map(patkind, exc), '(?:/|$)') or (lambda fn: False)
 
     return roots, lambda fn: (incmatch(fn) and not excmatch(fn) and
                               (fn.endswith('/') or
@@ -133,7 +161,7 @@
                             explain_exit(rc)[0])
         if errprefix:
             errmsg = "%s: %s" % (errprefix, errmsg)
-        raise CommandError(errmsg)
+        raise Abort(errmsg)
 
 def rename(src, dst):
     try:
@@ -178,6 +206,12 @@
     def pconvert(path):
         return path.replace("\\", "/")
 
+    def localpath(path):
+        return path.replace('/', '\\')
+
+    def normpath(path):
+        return pconvert(os.path.normpath(path))
+
     makelock = _makelock_file
     readlock = _readlock_file
 
@@ -206,6 +240,11 @@
     def pconvert(path):
         return path
 
+    def localpath(path):
+        return path
+
+    normpath = os.path.normpath
+
     def makelock(info, pathname):
         try:
             os.symlink(info, pathname)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-walk	Sat Aug 13 19:43:42 2005 -0800
@@ -0,0 +1,55 @@
+#!/bin/sh
+
+mkdir t
+cd t
+hg init
+mkdir -p beans
+for b in kidney navy turtle borlotti black pinto; do
+    echo $b > beans/$b
+done
+mkdir -p mammals/Procyonidae
+for m in cacomistle coatimundi raccoon; do
+    echo $m > mammals/Procyonidae/$m
+done
+echo skunk > mammals/skunk
+echo fennel > fennel
+echo fenugreek > fenugreek
+echo fiddlehead > fiddlehead
+echo glob:glob > glob:glob
+hg addremove
+hg commit -m "commit #0" -d "0 0"
+hg debugwalk
+cd mammals
+hg debugwalk
+hg debugwalk Procyonidae
+cd Procyonidae
+hg debugwalk
+hg debugwalk ..
+cd ..
+hg debugwalk ../beans
+hg debugwalk
+cd ..
+hg debugwalk -Ibeans
+hg debugwalk 'mammals/../beans/b*'
+hg debugwalk '-X*/Procyonidae' mammals
+hg debugwalk path:mammals
+hg debugwalk ..
+hg debugwalk beans/../..
+# Don't know how to test absolute paths without always getting a false
+# error.
+#hg debugwalk `pwd`/beans
+#hg debugwalk `pwd`/..
+hg debugwalk glob:\*
+hg debugwalk 're:.*[kb]$'
+hg debugwalk path:beans/black
+hg debugwalk beans 'beans/*'
+hg debugwalk 'j*'
+hg debugwalk NOEXIST
+mkfifo fifo
+hg debugwalk fifo
+rm fenugreek
+hg debugwalk fenugreek
+hg rm fenugreek
+hg debugwalk fenugreek
+touch new
+hg debugwalk new
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-walk.out	Sat Aug 13 19:43:42 2005 -0800
@@ -0,0 +1,114 @@
++ hg init
++ hg addremove
+adding fennel
+adding fenugreek
+adding fiddlehead
+adding glob:glob
+adding beans/black
+adding beans/borlotti
+adding beans/kidney
+adding beans/navy
+adding beans/pinto
+adding beans/turtle
+adding mammals/skunk
+adding mammals/Procyonidae/cacomistle
+adding mammals/Procyonidae/coatimundi
+adding mammals/Procyonidae/raccoon
++ hg commit -m commit #0 -d 0 0
++ hg debugwalk
+f  fennel                          fennel
+f  fenugreek                       fenugreek
+f  fiddlehead                      fiddlehead
+f  glob:glob                       glob:glob
+f  beans/black                     beans/black
+f  beans/borlotti                  beans/borlotti
+f  beans/kidney                    beans/kidney
+f  beans/navy                      beans/navy
+f  beans/pinto                     beans/pinto
+f  beans/turtle                    beans/turtle
+f  mammals/skunk                   mammals/skunk
+f  mammals/Procyonidae/cacomistle  mammals/Procyonidae/cacomistle
+f  mammals/Procyonidae/coatimundi  mammals/Procyonidae/coatimundi
+f  mammals/Procyonidae/raccoon     mammals/Procyonidae/raccoon
++ hg debugwalk
+f  mammals/skunk                   skunk
+f  mammals/Procyonidae/cacomistle  Procyonidae/cacomistle
+f  mammals/Procyonidae/coatimundi  Procyonidae/coatimundi
+f  mammals/Procyonidae/raccoon     Procyonidae/raccoon
++ hg debugwalk Procyonidae
+f  mammals/Procyonidae/cacomistle  Procyonidae/cacomistle
+f  mammals/Procyonidae/coatimundi  Procyonidae/coatimundi
+f  mammals/Procyonidae/raccoon     Procyonidae/raccoon
++ hg debugwalk
+f  mammals/Procyonidae/cacomistle  cacomistle
+f  mammals/Procyonidae/coatimundi  coatimundi
+f  mammals/Procyonidae/raccoon     raccoon
++ hg debugwalk ..
+f  mammals/skunk                   ../skunk
+f  mammals/Procyonidae/cacomistle  cacomistle
+f  mammals/Procyonidae/coatimundi  coatimundi
+f  mammals/Procyonidae/raccoon     raccoon
++ hg debugwalk ../beans
+f  beans/black     ../beans/black
+f  beans/borlotti  ../beans/borlotti
+f  beans/kidney    ../beans/kidney
+f  beans/navy      ../beans/navy
+f  beans/pinto     ../beans/pinto
+f  beans/turtle    ../beans/turtle
++ hg debugwalk
+f  mammals/skunk                   skunk
+f  mammals/Procyonidae/cacomistle  Procyonidae/cacomistle
+f  mammals/Procyonidae/coatimundi  Procyonidae/coatimundi
+f  mammals/Procyonidae/raccoon     Procyonidae/raccoon
++ hg debugwalk -Ibeans
+f  beans/black     beans/black
+f  beans/borlotti  beans/borlotti
+f  beans/kidney    beans/kidney
+f  beans/navy      beans/navy
+f  beans/pinto     beans/pinto
+f  beans/turtle    beans/turtle
++ hg debugwalk mammals/../beans/b*
+f  beans/black     beans/black
+f  beans/borlotti  beans/borlotti
++ hg debugwalk -X*/Procyonidae mammals
+f  mammals/skunk  mammals/skunk
++ hg debugwalk path:mammals
+f  mammals/skunk                   mammals/skunk
+f  mammals/Procyonidae/cacomistle  mammals/Procyonidae/cacomistle
+f  mammals/Procyonidae/coatimundi  mammals/Procyonidae/coatimundi
+f  mammals/Procyonidae/raccoon     mammals/Procyonidae/raccoon
++ hg debugwalk ..
+abort: .. not under repository root
++ hg debugwalk beans/../..
+abort: beans/../.. not under repository root
++ hg debugwalk glob:*
+f  fennel      fennel
+f  fenugreek   fenugreek
+f  fiddlehead  fiddlehead
+f  glob:glob   glob:glob
++ hg debugwalk re:.*[kb]$
+f  fenugreek      fenugreek
+f  glob:glob      glob:glob
+f  beans/black    beans/black
+f  mammals/skunk  mammals/skunk
++ hg debugwalk path:beans/black
+f  beans/black  beans/black
++ hg debugwalk beans beans/*
+f  beans/black     beans/black
+f  beans/borlotti  beans/borlotti
+f  beans/kidney    beans/kidney
+f  beans/navy      beans/navy
+f  beans/pinto     beans/pinto
+f  beans/turtle    beans/turtle
++ hg debugwalk j*
++ hg debugwalk NOEXIST
+NOEXIST: No such file or directory
++ hg debugwalk fifo
+fifo: unsupported file type (type is fifo)
++ hg debugwalk fenugreek
+m  fenugreek  fenugreek
++ hg rm fenugreek
++ hg debugwalk fenugreek
+m  fenugreek  fenugreek
++ hg debugwalk new
+f  new  new