Merge with BOS
authormpm@selenic.com
Sat, 13 Aug 2005 19:43:42 -0800
changeset 896 01215ad0428341bf0b5806b199c54fe158d9affd
parent 867 0cd2ee61b10a03992daabee417949866318c308b (current diff)
parent 895 77b52b864249768a0ec615af331be778fdbb65e9 (diff)
child 897 fe30f5434b5152e14638c1039d8c8b48113d2ecf
push id1
push usergszorc@mozilla.com
push dateWed, 18 Mar 2015 16:34:57 +0000
Merge with BOS
.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
new file mode 100755
--- /dev/null
+++ b/contrib/patchbomb
@@ -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
+++ b/mercurial/commands.py
@@ -9,19 +9,16 @@ from demandload import demandload
 demandload(globals(), "os re sys signal shutil")
 demandload(globals(), "fancyopts ui hg util")
 demandload(globals(), "fnmatch hgweb mdiff random signal time traceback")
 demandload(globals(), "errno socket version struct atexit")
 
 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]
 
     for t in filters:
         if t and t[-1] != "/":
             t += "/"
         l += [x for x in files if x.startswith(t)]
     return l
@@ -30,40 +27,29 @@ def relfilter(repo, files):
     cwd = repo.getcwd()
     if cwd:
         return filterfiles([util.pconvert(cwd)], files)
     return files
 
 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 = ''):
     files, matchfn, results = makewalk(repo, pats, opts, head)
     for r in results: yield r
 
 revrangesep = ':'
 
@@ -84,17 +70,17 @@ def revrange(ui, repo, revs, revlog=None
                 raise ValueError
         except ValueError:
             try:
                 num = repo.changelog.rev(repo.lookup(val))
             except KeyError:
                 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:
             start, end = spec.split(revrangesep, 1)
             start = fix(start, 0)
             end = fix(end, revcount - 1)
             if end > start:
                 end += 1
@@ -139,17 +125,17 @@ def make_filename(repo, r, pat, node=Non
             if c == '%':
                 i += 1
                 c = pat[i]
                 c = expander[c]()
             newname.append(c)
             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,
               total=None, seqno=None, revwidth=None, mode='wb'):
     if not pat or pat == '-':
         if 'w' in mode: return sys.stdout
         else: return sys.stdin
     if hasattr(pat, 'write') and 'w' in mode:
@@ -391,21 +377,20 @@ def add(ui, repo, *pats, **opts):
             names.append(abs)
     repo.add(names)
 
 def addremove(ui, repo, *pats, **opts):
     """add all new files, delete all missing files"""
     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)
     repo.remove(remove)
 
 def annotate(ui, repo, *pats, **opts):
     """show changeset information per file line"""
     def getnode(rev):
@@ -422,17 +407,17 @@ def annotate(ui, repo, *pats, **opts):
                 name = name[:f]
             f = name.find('<')
             if f >= 0:
                 name = name[f+1:]
             bcache[rev] = name
             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]]
     if not opts['user'] and not opts['changeset']:
         opts['number'] = 1
 
     if opts['rev']:
         node = repo.changelog.lookup(opts['rev'])
@@ -473,16 +458,18 @@ def clone(ui, source, dest=None, **opts)
     """make a copy of an existing repository"""
     if dest is None:
         dest = os.path.basename(os.path.normpath(source))
 
     if os.path.exists(dest):
         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
             self.dir_ = dir_
             os.mkdir(dir_)
         def close(self):
             self.dir_ = None
         def __del__(self):
@@ -536,17 +523,17 @@ def commit(ui, repo, *pats, **opts):
             ui.warn("Can't read commit message %s: %s\n" % (logfile, why))
 
     if opts['addremove']:
         addremove(ui, repo, *pats, **opts)
     cwd = repo.getcwd()
     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']
     else:
         files = []
     repo.commit(files, message, opts['user'], opts['date'], match)
 
 def copy(ui, repo, source, dest):
@@ -578,17 +565,17 @@ def debugcheckstate(ui, repo):
                     (f, state))
             errors += 1
     for f in m1:
         state = repo.dirstate.state(f)
         if state not in "nrm":
             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"""
     repo.dirstate.read()
     dc = repo.dirstate.map
     keys = dc.keys()
     keys.sort()
     for file_ in keys:
@@ -616,32 +603,35 @@ def debugindexdot(ui, file_):
         e = r.index[i]
         ui.write("\t%d -> %d\n" % (r.rev(e[4]), i))
         if e[5] != hg.nullid:
             ui.write("\t%d -> %d\n" % (r.rev(e[5]), i))
     ui.write("}\n")
 
 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
 
 def diff(ui, repo, *pats, **opts):
     """diff working directory (or selected files)"""
     revs = []
     if opts['rev']:
         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):
     node = repo.lookup(changeset)
     prev, other = repo.changelog.parents(node)
     change = repo.changelog.read(node)
 
     fp = make_file(repo, repo.changelog, opts['output'],
@@ -660,17 +650,17 @@ def doexport(ui, repo, changeset, seqno,
     fp.write("\n\n")
 
     dodiff(fp, ui, repo, None, prev, node)
     if fp != sys.stdout: fp.close()
 
 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)
     revwidth = max(len(revs[0]), len(revs[-1]))
     ui.note(len(revs) > 1 and "Exporting patches:\n" or "Exporting patch:\n")
     for cset in revs:
         seqno += 1
         doexport(ui, repo, cset, seqno, total, revwidth, opts)
@@ -757,27 +747,27 @@ def import_(ui, repo, patch1, *patches, 
             l.rstrip('\r\n');
             ui.status("%s\n" % l)
             if l.startswith('patching file '):
                 pf = l[14:]
                 if pf not in files:
                     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)
         repo.commit(files, message, user)
 
 def init(ui, source=None):
     """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):
     """locate files matching specific patterns"""
     end = '\n'
     if opts['print0']: end = '\0'
 
     for src, abs, rel in walk(repo, pats, opts, '(?:.*/|)'):
@@ -1073,18 +1063,18 @@ def status(ui, repo, *pats, **opts):
 
     M = modified
     A = added
     R = removed
     ? = not tracked
     '''
 
     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),
                    ('added', 'A', a),
                    ('removed', 'R', d),
                    ('unknown', '?', u)]
 
     for opt, char, changes in ([ct for ct in changetypes if opts[ct[0]]]
@@ -1466,18 +1456,16 @@ def dispatch(args):
                 stats.print_stats(40)
                 return r
             else:
                 return d()
         except:
             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:
         u.warn("killed!\n")
     except KeyboardInterrupt:
         try:
             u.warn("interrupted!\n")
         except IOError, inst:
@@ -1495,17 +1483,17 @@ def dispatch(args):
             if u.debugflag: u.warn("broken pipe\n")
         else:
             raise
     except OSError, inst:
         if hasattr(inst, "filename"):
             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:
         # was this an argument error?
         tb = traceback.extract_tb(sys.exc_info()[2])
         if len(tb) > 2: # no
             raise
         u.debug(inst, "\n")
--- a/mercurial/hg.py
+++ b/mercurial/hg.py
@@ -5,18 +5,18 @@
 # This software may be used and distributed according to the terms
 # of the GNU General Public License, incorporated herein by reference.
 
 import sys, struct, os
 import util
 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):
         revlog.__init__(self, opener,
                         os.path.join("data", self.encodedir(path + ".i")),
                         os.path.join("data", self.encodedir(path + ".d")))
 
     # This avoids a collision between a file named foo and a dir named
@@ -295,31 +295,36 @@ class dirstate:
         self.map = None
         self.pl = None
         self.copies = {}
         self.ignorefunc = None
 
     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 = []
             try:
                 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:
                 s = "(?:%s)" % (")|(?:".join(bigpat))
                 r = re.compile(s)
                 self.ignorefunc = r.search
             else:
                 self.ignorefunc = util.never
@@ -432,72 +437,130 @@ class dirstate:
         for f, e in self.map.items():
             c = self.copied(f)
             if c:
                 f = f + "\0" + c
             e = struct.pack(">cllll", e[0], e[1], e[2], e[3], len(f))
             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
                         for sd in subdirs:
                             ds = os.path.join(nd, sd +'/')
                             if self.ignore(ds) or not match(ds):
                                 subdirs.remove(sd)
                         subdirs.sort()
                         fl.sort()
                         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()
             for k in ks:
                 yield 'm', k
 
         # yield only files that match: all in dirstate, others only if
         # 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:
                 continue
             if not stat.S_ISREG(s.st_mode):
                 continue
             c = dc.get(fn)
             if c:
@@ -692,19 +755,17 @@ class localrepository:
     def wjoin(self, f):
         return os.path.join(self.root, f)
 
     def file(self, f):
         if f[0] == '/': f = f[1:]
         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)
 
     def transaction(self):
         # save dirstate for undo
         try:
             ds = self.opener("dirstate").read()
--- a/mercurial/hgweb.py
+++ b/mercurial/hgweb.py
@@ -703,17 +703,22 @@ class hgweb:
             write(self.t("error"))
 
 def create_server(path, name, templates, address, port, use_ipv6 = False,
                   accesslog = sys.stdout, errorlog = sys.stderr):
 
     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):
             errorlog.write("%s - - [%s] %s\n" % (self.address_string(),
                                                  self.log_date_time_string(),
                                                  format % args))
             
         def log_message(self, format, *args):
--- a/mercurial/util.py
+++ b/mercurial/util.py
@@ -11,17 +11,18 @@ demandload(globals(), "re")
 
 def unique(g):
     seen = {}
     for f in g:
         if f not in seen:
             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
 
 def globre(pat, head = '^', tail = '$'):
     "convert a glob pattern into a regexp"
     i, n = 0, len(pat)
     res = ''
@@ -63,82 +64,109 @@ def globre(pat, head = '^', tail = '$'):
         elif c == ',' and group:
             res += '|'
         else:
             res += re.escape(c)
     return head + res + tail
 
 _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):
         '''return the non-glob prefix of a path, e.g. foo/* -> foo'''
         root = []
         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
                                (not pats and not files) or
                                (pats and patmatch(fn)) or
                                (files and filematch(fn))))
 
 def system(cmd, errprefix=None):
     """execute a shell command that must succeed"""
     rc = os.system(cmd)
     if rc:
         errmsg = "%s %s" % (os.path.basename(cmd.split(None, 1)[0]),
                             explain_exit(rc)[0])
         if errprefix:
             errmsg = "%s: %s" % (errprefix, errmsg)
-        raise CommandError(errmsg)
+        raise Abort(errmsg)
 
 def rename(src, dst):
     try:
         os.rename(src, dst)
     except:
         os.unlink(dst)
         os.rename(src, dst)
 
@@ -173,16 +201,22 @@ if os.name == 'nt':
         return last
 
     def set_exec(f, mode):
         pass
 
     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
 
     def explain_exit(code):
         return "exited with status %d" % code, code
 
 else:
     nulldev = '/dev/null'
@@ -201,16 +235,21 @@ else:
             os.umask(umask)
             os.chmod(f, s | (s & 0444) >> 2 & ~umask)
         else:
             os.chmod(f, s & 0666)
 
     def pconvert(path):
         return path
 
+    def localpath(path):
+        return path
+
+    normpath = os.path.normpath
+
     def makelock(info, pathname):
         try:
             os.symlink(info, pathname)
         except OSError, why:
             if why.errno == errno.EEXIST:
                 raise
             else:
                 _makelock_file(info, pathname)
new file mode 100755
--- /dev/null
+++ b/tests/test-walk
@@ -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
new file mode 100644
--- /dev/null
+++ b/tests/test-walk.out
@@ -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