Merge from BOS
authormpm@selenic.com
Wed, 20 Jul 2005 20:00:29 -0500
changeset 740 d2422f10c136
parent 722 e5b39ce2c3c9 (current diff)
parent 739 36edb39e8e8c (diff)
child 741 156dc2f3be7f
Merge from BOS manifest hash: 2276dbd96bb4221e579c871a1de2403c92c85659
.hgignore
MANIFEST.in
contrib/hgit
doc/FAQ.txt
doc/hg.1.txt
mercurial/commands.py
mercurial/hg.py
mercurial/util.py
templates/map
tests/test-help.out
--- a/doc/hg.1.txt	Wed Jul 20 11:40:27 2005 -0500
+++ b/doc/hg.1.txt	Wed Jul 20 20:00:29 2005 -0500
@@ -33,7 +33,8 @@
 ----------------
 
 files ...::
-    indicates one or more filename or relative path filenames
+    indicates one or more filename or relative path filenames; see
+    "FILE NAME PATTERNS" for information on pattern matching
 
 path::
     indicates a path on the local machine
@@ -51,11 +52,14 @@
 COMMANDS
 --------
 
-add [files ...]::
+add [options] [files ...]::
     Schedule files to be version controlled and added to the repository.
     
     The files will be added to the repository at the next commit.
 
+    If no names are given, add all files in the current directory and
+    its subdirectories.
+
 addremove::
     Add all new files and remove all missing files from the repository.
     
@@ -69,6 +73,8 @@
     place.
     
     options:
+    -I, --include <pat>  include directories matching the given patterns
+    -X, --exclude <pat>  exclude directories matching the given patterns
     -r, --revision <rev>  annotate the specified revision
     -u, --user            list the author
     -c, --changeset       list the changeset
@@ -129,6 +135,10 @@
     revisions are specified, the working directory files are compared
     to its parent.
 
+    options:
+    -I, --include <pat>  include directories matching the given patterns
+    -X, --exclude <pat>  exclude directories matching the given patterns
+
 export [-o filespec] [revision] ...::
     Print the changeset header and diffs for one or more revisions.
 
@@ -183,14 +193,10 @@
 init::
     Initialize a new repository in the current directory.
 
-locate [options] [patterns]::
-    Print all files under Mercurial control whose basenames match the
+locate [options] [files]::
+    Print all files under Mercurial control whose names match the
     given patterns.
 
-    Patterns are shell-style globs.  To restrict searches to specific
-    directories, use the "-i <pat>" option.  To eliminate particular
-    directories from searching, use the "-x <pat>" option.
-
     This command searches the current directory and its
     subdirectories.  To search an entire repository, move to the root
     of the repository.
@@ -207,9 +213,9 @@
 
     -0, --print0         end filenames with NUL, for use with xargs
     -f, --fullpath       print complete paths from the filesystem root
-    -i, --include <pat>  include directories matching the given globs
+    -I, --include <pat>  include directories matching the given patterns
     -r, --rev <rev>      search the repository as it stood at rev
-    -x, --exclude <pat>  exclude directories matching the given globs
+    -X, --exclude <pat>  exclude directories matching the given patterns
 
 log [-r revision ...] [-p] [file]::
     Print the revision history of the specified file or the entire project.
@@ -319,8 +325,10 @@
     -n, --name <name>        name to show in web pages (default: working dir)
     -t, --templatedir <path> web templates to use
 
-status::
-    Show changed files in the working directory.
+status [options] [files]::
+    Show changed files in the working directory.  If no names are
+    given, all files are shown.  Otherwise, only files matching the
+    given names are shown.
 
     The codes used to show the status of files are:
     
@@ -329,6 +337,11 @@
     R = removed
     ? = not tracked
 
+    options:
+
+    -I, --include <pat>  include directories matching the given patterns
+    -X, --exclude <pat>  exclude directories matching the given patterns
+
 tag [-l -t <text> -d <datecode> -u <user>] <name> [revision]::
     Name a particular revision using <name>.
     
@@ -398,6 +411,52 @@
     the changelog, manifest, and tracked files, as well as the
     integrity of their crosslinks and indices.
 
+FILE NAME PATTERNS
+------------------
+
+    Mercurial accepts several notations for identifying one or more
+    file at a time.
+
+    By default, Mercurial treats file names as shell-style extended
+    glob patterns.
+
+    Alternate pattern notations must be specified explicitly.
+
+    To use a plain path name without any pattern matching, start a
+    name with "path:".  These path names must match completely, from
+    the root of the current repository.
+
+    To use an extended glob, start a name with "glob:".  Globs are
+    rooted at the current directory; a glob such as "*.c" will match
+    files ending in ".c" in the current directory only.
+
+    The supported glob syntax extensions are "**" to match any string
+    across path separators, and "{a,b}" to mean "a or b".
+
+    To use a Perl/Python regular expression, start a name with "re:".
+    Regexp pattern matching is anchored at the root of the repository.
+
+    Plain examples:
+
+    path:foo/bar   a name bar in a directory named foo in the root of
+                   the repository
+    path:path:name a file or directory named "path:name"
+
+    Glob examples:
+
+    glob:*.c       any name ending in ".c" in the current directory
+    *.c            any name ending in ".c" in the current directory
+    **.c           any name ending in ".c" in the current directory, or
+                   any subdirectory
+    foo/*.c        any name ending in ".c" in the directory foo
+    foo/**.c       any name ending in ".c" in the directory foo, or any
+                   subdirectory
+
+    Regexp examples:
+
+    re:.*\.c$      any name ending in ".c", anywhere in the repsitory
+
+
 SPECIFYING SINGLE REVISIONS
 ---------------------------
 
--- a/mercurial/commands.py	Wed Jul 20 11:40:27 2005 -0500
+++ b/mercurial/commands.py	Wed Jul 20 20:00:29 2005 -0500
@@ -14,6 +14,9 @@
 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]
 
@@ -36,6 +39,41 @@
                 for x in args]
     return args
 
+def matchpats(ui, cwd, pats = [], opts = {}, emptyok = True):
+    if not pats and not emptyok:
+        raise Abort('at least one file name or pattern required')
+    head = ''
+    if opts.get('rootless'): head = '(?:.*/|)'
+    def reify(name, tail):
+        if name.startswith('re:'):
+            return name[3:]
+        elif name.startswith('glob:'):
+            return head + util.globre(name[5:], '', tail)
+        elif name.startswith('path:'):
+            return '^' + re.escape(name[5:]) + '$'
+        return head + util.globre(name, '', tail)
+    cwdsep = cwd + os.sep
+    def under(fn):
+        if not cwd or fn.startswith(cwdsep): return True
+    def matchfn(pats, tail, ifempty = util.always):
+        if not pats: return ifempty
+        pat = '(?:%s)' % '|'.join([reify(p, tail) for p in pats])
+        if cwd: pat = re.escape(cwd + os.sep) + pat
+        ui.debug('regexp: %s\n' % pat)
+        return re.compile(pat).match
+    patmatch = matchfn(pats, '$')
+    incmatch = matchfn(opts.get('include'), '(?:/|$)', under)
+    excmatch = matchfn(opts.get('exclude'), '(?:/|$)', util.never)
+    return lambda fn: (incmatch(fn) and not excmatch(fn) and
+                       (fn.endswith('/') or patmatch(fn)))
+
+def walk(repo, pats, opts, emptyok = True):
+    cwd = repo.getcwd()
+    if cwd: c = len(cwd) + 1
+    for src, fn in repo.walk(match = matchpats(repo.ui, cwd, pats, opts, emptyok)):
+        if cwd: yield src, fn, fn[c:]
+        else: yield src, fn, fn
+
 revrangesep = ':'
 
 def revrange(ui, repo, revs, revlog=None):
@@ -60,8 +98,7 @@
                 try:
                     num = revlog.rev(revlog.lookup(val))
                 except KeyError:
-                    ui.warn('abort: invalid revision identifier %s\n' % val)
-                    sys.exit(1)
+                    raise Abort('invalid revision identifier %s', val)
         return num
     for spec in revs:
         if spec.find(revrangesep) >= 0:
@@ -91,29 +128,45 @@
         'b': lambda: os.path.basename(repo.root),
         }
 
-    if node:
-        expander.update(node_expander)
-    if node and revwidth is not None:
-        expander['r'] = lambda: str(r.rev(node)).zfill(revwidth)
-    if total is not None:
-        expander['N'] = lambda: str(total)
-    if seqno is not None:
-        expander['n'] = lambda: str(seqno)
-    if total is not None and seqno is not None:
-        expander['n'] = lambda:str(seqno).zfill(len(str(total)))
+    try:
+        if node:
+            expander.update(node_expander)
+        if node and revwidth is not None:
+            expander['r'] = lambda: str(r.rev(node)).zfill(revwidth)
+        if total is not None:
+            expander['N'] = lambda: str(total)
+        if seqno is not None:
+            expander['n'] = lambda: str(seqno)
+        if total is not None and seqno is not None:
+            expander['n'] = lambda:str(seqno).zfill(len(str(total)))
 
-    newname = []
-    patlen = len(pat)
-    i = 0
-    while i < patlen:
-        c = pat[i]
-        if c == '%':
+        newname = []
+        patlen = len(pat)
+        i = 0
+        while i < patlen:
+            c = pat[i]
+            if c == '%':
+                i += 1
+                c = pat[i]
+                c = expander[c]()
+            newname.append(c)
             i += 1
-            c = pat[i]
-            c = expander[c]()
-        newname.append(c)
-        i += 1
-    return ''.join(newname)
+        return ''.join(newname)
+    except KeyError, inst:
+        raise 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:
+        return pat
+    if hasattr(pat, 'read') and 'r' in mode:
+        return pat
+    return open(make_filename(repo, r, pat, node, total, seqno, revwidth),
+                mode)
 
 def dodiff(fp, ui, repo, files=None, node1=None, node2=None):
     def date(c):
@@ -288,9 +341,17 @@
 
 # Commands start here, listed alphabetically
 
-def add(ui, repo, file1, *files):
+def add(ui, repo, *pats, **opts):
     '''add the specified files on the next commit'''
-    repo.add(relpath(repo, (file1,) + files))
+    names = []
+    q = dict(zip(pats, pats))
+    for src, abs, rel in walk(repo, pats, opts):
+        if rel in q or abs in q:
+            names.append(abs)
+        elif repo.dirstate.state(abs) == '?':
+            ui.status('adding %s\n' % rel)
+            names.append(abs)
+    repo.add(names)
 
 def addremove(ui, repo, *files):
     """add all new files, delete all missing files"""
@@ -307,11 +368,11 @@
             elif s not in 'nmai' and isfile:
                 u.append(f)
     else:
-        (c, a, d, u) = repo.changes(None, None)
+        (c, a, d, u) = repo.changes()
     repo.add(u)
     repo.remove(d)
 
-def annotate(ui, repo, file1, *files, **opts):
+def annotate(ui, repo, *pats, **opts):
     """show changeset information per file line"""
     def getnode(rev):
         return hg.short(repo.changelog.node(rev))
@@ -342,8 +403,8 @@
         node = repo.dirstate.parents()[0]
     change = repo.changelog.read(node)
     mmap = repo.manifest.read(change[0])
-    for f in relpath(repo, (file1,) + files):
-        lines = repo.file(f).annotate(mmap[f])
+    for src, abs, rel in walk(repo, pats, opts, emptyok = False):
+        lines = repo.file(abs).annotate(mmap[abs])
         pieces = []
 
         for o, f in opmap:
@@ -362,16 +423,7 @@
         n = r.lookup(rev)
     else:
         n = r.tip()
-    if opts['output'] and opts['output'] != '-':
-        try:
-            outname = make_filename(repo, r, opts['output'], node=n)
-            fp = open(outname, 'wb')
-        except KeyError, inst:
-            ui.warn("error: invlaid format spec '%%%s' in output file name\n" %
-                    inst.args[0])
-            sys.exit(1);
-    else:
-        fp = sys.stdout
+    fp = make_file(repo, r, opts['output'], node=n)
     fp.write(r.read(n))
 
 def clone(ui, source, dest=None, **opts):
@@ -475,8 +527,7 @@
             ui.warn("%s in manifest1, but listed as state %s" % (f, state))
             errors += 1
     if errors:
-        ui.warn(".hg/dirstate inconsistent with current parent's manifest\n")
-        sys.exit(1)
+        raise Abort(".hg/dirstate inconsistent with current parent's manifest")
 
 def debugstate(ui, repo):
     """show the contents of the current dirstate"""
@@ -509,21 +560,18 @@
             ui.write("\t%d -> %d\n" % (r.rev(e[5]), i))
     ui.write("}\n")
 
-def diff(ui, repo, *files, **opts):
+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:
-        ui.warn("too many revisions to diff\n")
-        sys.exit(1)
+        raise Abort("too many revisions to diff")
 
-    if files:
-        files = relpath(repo, files)
-    else:
-        files = relpath(repo, [""])
-
+    files = []
+    for src, abs, rel in walk(repo, pats, opts):
+        files.append(abs)
     dodiff(sys.stdout, ui, repo, files, *revs)
 
 def doexport(ui, repo, changeset, seqno, total, revwidth, opts):
@@ -531,19 +579,11 @@
     prev, other = repo.changelog.parents(node)
     change = repo.changelog.read(node)
 
-    if opts['output'] and opts['output'] != '-':
-        try:
-            outname = make_filename(repo, repo.changelog, opts['output'],
-                                    node=node, total=total, seqno=seqno,
-                                    revwidth=revwidth)
-            ui.note("Exporting patch to '%s'.\n" % outname)
-            fp = open(outname, 'wb')
-        except KeyError, inst:
-            ui.warn("error: invalid format spec '%%%s' in output file name\n" %
-                    inst.args[0])
-            sys.exit(1)
-    else:
-        fp = sys.stdout
+    fp = make_file(repo, repo.changelog, opts['output'],
+                   node=node, total=total, seqno=seqno,
+                   revwidth=revwidth)
+    if fp != sys.stdout:
+        ui.note("Exporting patch to '%s'.\n" % fp.name)
 
     fp.write("# HG changeset patch\n")
     fp.write("# User %s\n" % change[1])
@@ -555,12 +595,12 @@
     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:
-        ui.warn("error: export requires at least one changeset\n")
-        sys.exit(1)
+        raise Abort("export requires at least one changeset")
     seqno = 0
     revs = list(revrange(ui, repo, changesets))
     total = len(revs)
@@ -586,7 +626,7 @@
         return
 
     hexfunc = ui.verbose and hg.hex or hg.short
-    (c, a, d, u) = repo.changes(None, None)
+    (c, a, d, u) = repo.changes()
     output = ["%s%s" % ('+'.join([hexfunc(parent) for parent in parents]),
                         (c or a or d) and "+" or "")]
 
@@ -654,8 +694,7 @@
                     files.append(pf)
         patcherr = f.close()
         if patcherr:
-            sys.stderr.write("patch failed")
-            sys.exit(1)
+            raise Abort("patch failed")
 
         if len(files) > 0:
             addremove(ui, repo, *files)
@@ -665,52 +704,20 @@
     """create a new repository in the current directory"""
 
     if source:
-        ui.warn("no longer supported: use \"hg clone\" instead\n")
-        sys.exit(1)
+        raise Abort("no longer supported: use \"hg clone\" instead")
     hg.repository(ui, ".", create=1)
 
 def locate(ui, repo, *pats, **opts):
     """locate files matching specific patterns"""
-    if [p for p in pats if os.sep in p]:
-        ui.warn("error: patterns may not contain '%s'\n" % os.sep)
-        ui.warn("use '-i <dir>' instead\n")
-        sys.exit(1)
-    def compile(pats, head='^', tail=os.sep, on_empty=True):
-        if not pats:
-            class c:
-                def match(self, x):
-                    return on_empty
-            return c()
-        fnpats = [fnmatch.translate(os.path.normpath(os.path.normcase(p)))[:-1]
-                  for p in pats]
-        regexp = r'%s(?:%s)%s' % (head, '|'.join(fnpats), tail)
-        return re.compile(regexp)
-    exclude = compile(opts['exclude'], on_empty=False)
-    include = compile(opts['include'])
-    pat = compile(pats, head='', tail='$')
-    end = opts['print0'] and '\0' or '\n'
-    if opts['rev']:
-        node = repo.manifest.lookup(opts['rev'])
-    else:
-        node = repo.manifest.tip()
-    manifest = repo.manifest.read(node)
-    cwd = repo.getcwd()
-    cwd_plus = cwd and (cwd + os.sep)
-    found = []
-    for f in manifest:
-        f = os.path.normcase(f)
-        if exclude.match(f) or not(include.match(f) and
-                                   f.startswith(cwd_plus) and
-                                   pat.match(os.path.basename(f))):
-            continue
+    if opts['print0']: end = '\0'
+    else: end = '\n'
+    opts['rootless'] = True
+    for src, abs, rel in walk(repo, pats, opts):
+        if repo.dirstate.state(abs) == '?': continue
         if opts['fullpath']:
-            f = os.path.join(repo.root, f)
-        elif cwd:
-            f = f[len(cwd_plus):]
-        found.append(f)
-    found.sort()
-    for f in found:
-        ui.write(f, end)
+            ui.write(os.path.join(repo.root, abs), end)
+        else:
+            ui.write(rel, end)
 
 def log(ui, repo, f=None, **opts):
     """show the revision history of the repository or a single file"""
@@ -746,6 +753,11 @@
             dodiff(sys.stdout, ui, repo, files, prev, changenode)
             ui.write("\n\n")
 
+def ls(ui, repo, *pats, **opts):
+    """list files"""
+    for src, abs, rel in walk(repo, pats, opts):
+        ui.write(rel, '\n')
+
 def manifest(ui, repo, rev=None):
     """output the latest or given revision of the project manifest"""
     if rev:
@@ -978,7 +990,7 @@
             ui.status('listening at http://%s/\n' % addr)
     httpd.serve_forever()
 
-def status(ui, repo):
+def status(ui, repo, *pats, **opts):
     '''show changed files in the working directory
 
     C = changed
@@ -986,7 +998,8 @@
     R = removed
     ? = not tracked'''
 
-    (c, a, d, u) = repo.changes(None, None)
+    (c, a, d, u) = repo.changes(match = matchpats(ui, repo.getcwd(),
+                                                  pats, opts))
     (c, a, d, u) = map(lambda x: relfilter(repo, x), (c, a, d, u))
 
     for f in c:
@@ -1017,7 +1030,7 @@
         repo.opener("localtags", "a").write("%s %s\n" % (r, name))
         return
 
-    (c, a, d, u) = repo.changes(None, None)
+    (c, a, d, u) = repo.changes()
     for x in (c, a, d, u):
         if ".hgtags" in x:
             ui.warn("abort: working copy of .hgtags is changed!\n")
@@ -1088,11 +1101,16 @@
 # Command options and aliases are listed here, alphabetically
 
 table = {
-    "^add": (add, [], "hg add FILE..."),
-    "addremove": (addremove, [], "hg addremove [FILE]..."),
+    "^add": (add,
+             [('I', 'include', [], 'include path in search'),
+              ('X', 'exclude', [], 'exclude path from search')],
+             "hg add [OPTIONS] [FILES]"),
+    "addremove": (addremove, [], "hg addremove [FILES]"),
     "^annotate":
         (annotate,
-         [('r', 'rev', '', 'revision'),
+         [('I', 'include', [], 'include path in search'),
+          ('X', 'exclude', [], 'exclude path from search'),
+          ('r', 'rev', '', 'revision'),
           ('u', 'user', None, 'show user'),
           ('n', 'number', None, 'show revision number'),
           ('c', 'changeset', None, 'show changeset')],
@@ -1120,7 +1138,9 @@
     "debugindexdot": (debugindexdot, [], 'debugindexdot FILE'),
     "^diff":
         (diff,
-         [('r', 'rev', [], 'revision')],
+         [('I', 'include', [], 'include path in search'),
+          ('X', 'exclude', [], 'exclude path from search'),
+          ('r', 'rev', [], 'revision')],
          'hg diff [-r REV1 [-r REV2]] [FILE]...'),
     "^export":
         (export,
@@ -1140,15 +1160,19 @@
         (locate,
          [('0', 'print0', None, 'end records with NUL'),
           ('f', 'fullpath', None, 'print complete paths'),
-          ('i', 'include', [], 'include path in search'),
+          ('I', 'include', [], 'include path in search'),
           ('r', 'rev', '', 'revision'),
-          ('x', 'exclude', [], 'exclude path from search')],
+          ('X', 'exclude', [], 'exclude path from search')],
          'hg locate [OPTION]... [PATTERN]...'),
     "^log|history":
         (log,
          [('r', 'rev', [], 'revision'),
           ('p', 'patch', None, 'show patch')],
          'hg log [-r REV1 [-r REV2]] [-p] [FILE]'),
+    "list|ls": (ls,
+                [('I', 'include', [], 'include path in search'),
+                 ('X', 'exclude', [], 'exclude path from search')],
+                "hg ls [OPTION]... [PATTERN]...."),
     "manifest": (manifest, [], 'hg manifest [REV]'),
     "parents": (parents, [], 'hg parents [REV]'),
     "^pull":
@@ -1183,7 +1207,10 @@
           ('', 'stdio', None, 'for remote clients'),
           ('t', 'templates', "", 'template map')],
          "hg serve [OPTION]..."),
-    "^status": (status, [], 'hg status'),
+    "^status": (status,
+                [('I', 'include', [], 'include path in search'),
+                 ('X', 'exclude', [], 'exclude path from search')],
+                'hg status [OPTION]... [FILE]...'),
     "tag":
         (tag,
          [('l', 'local', None, 'make the tag local'),
@@ -1344,6 +1371,9 @@
             u.warn("abort: %s: %s\n" % (inst.strerror, inst.filename))
         else:
             u.warn("abort: %s\n" % inst.strerror)
+    except 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])
--- a/mercurial/hg.py	Wed Jul 20 11:40:27 2005 -0500
+++ b/mercurial/hg.py	Wed Jul 20 20:00:29 2005 -0500
@@ -277,6 +277,36 @@
         self.map = None
         self.pl = None
         self.copies = {}
+        self.ignorefunc = None
+
+    def wjoin(self, f):
+        return os.path.join(self.root, f)
+
+    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])
+                        try:
+                            r = re.compile(p)
+                        except:
+                            self.ui.warn("ignoring invalid ignore"
+                                         + " regular expression '%s'\n" % p)
+                        else:
+                            bigpat.append(util.pconvert(pat[:-1]))
+            except IOError: pass
+
+            if bigpat:
+                s = "(?:%s)" % (")|(?:".join(bigpat))
+                r = re.compile(s)
+                self.ignorefunc = r.search
+            else:
+                self.ignorefunc = util.never
+
+        return self.ignorefunc(f)
 
     def __del__(self):
         if self.dirty:
@@ -298,8 +328,12 @@
             self.read()
         return self.pl
 
+    def markdirty(self):
+        if not self.dirty:
+            self.dirty = 1
+
     def setparents(self, p1, p2 = nullid):
-        self.dirty = 1
+        self.markdirty()
         self.pl = p1, p2
 
     def state(self, key):
@@ -334,7 +368,7 @@
 
     def copy(self, source, dest):
         self.read()
-        self.dirty = 1
+        self.markdirty()
         self.copies[dest] = source
 
     def copied(self, file):
@@ -349,7 +383,7 @@
 
         if not files: return
         self.read()
-        self.dirty = 1
+        self.markdirty()
         for f in files:
             if state == "r":
                 self.map[f] = ('r', 0, 0, 0)
@@ -360,7 +394,7 @@
     def forget(self, files):
         if not files: return
         self.read()
-        self.dirty = 1
+        self.markdirty()
         for f in files:
             try:
                 del self.map[f]
@@ -370,7 +404,7 @@
 
     def clear(self):
         self.map = {}
-        self.dirty = 1
+        self.markdirty()
 
     def write(self):
         st = self.opener("dirstate", "w")
@@ -383,34 +417,50 @@
             st.write(e + f)
         self.dirty = 0
 
-    def changes(self, files, ignore):
+    def walk(self, files = None, match = util.always):
         self.read()
         dc = self.map.copy()
-        lookup, changed, added, unknown = [], [], [], []
-
-        # compare all files by default
+        # walk all files by default
         if not files: files = [self.root]
-
-        # recursive generator of all files listed
-        def walk(files):
+        def traverse():
             for f in util.unique(files):
                 f = os.path.join(self.root, f)
                 if os.path.isdir(f):
                     for dir, subdirs, fl in os.walk(f):
                         d = dir[len(self.root) + 1:]
+                        if d == '.hg':
+                            subdirs[:] = []
+                            continue
                         for sd in subdirs:
-                            if ignore(os.path.join(d, sd +'/')):
+                            ds = os.path.join(d, sd +'/')
+                            if self.ignore(ds) or not match(ds):
                                 subdirs.remove(sd)
                         for fn in fl:
                             fn = util.pconvert(os.path.join(d, fn))
-                            yield fn
+                            yield 'f', fn
                 else:
-                    yield f[len(self.root) + 1:]
+                    yield 'f', f[len(self.root) + 1:]
 
             for k in dc.keys():
-                yield k
+                yield 'm', k
+
+        # yield only files that match: all in dirstate, others only if
+        # not in .hgignore
 
-        for fn in util.unique(walk(files)):
+        for src, fn in util.unique(traverse()):
+            if fn in dc:
+                del dc[fn]
+            elif self.ignore(fn):
+                continue
+            if match(fn):
+                yield src, fn
+
+    def changes(self, files = None, match = util.always):
+        self.read()
+        dc = self.map.copy()
+        lookup, changed, added, unknown = [], [], [], []
+
+        for src, fn in self.walk(files, match):
             try: s = os.stat(os.path.join(self.root, fn))
             except: continue
 
@@ -429,9 +479,9 @@
                 elif c[1] != s.st_mode or c[3] != s.st_mtime:
                     lookup.append(fn)
             else:
-                if not ignore(fn): unknown.append(fn)
+                if match(fn): unknown.append(fn)
 
-        return (lookup, changed, added, dc.keys(), unknown)
+        return (lookup, changed, added, filter(match, dc.keys()), unknown)
 
 # used to avoid circular references so destructors work
 def opener(base):
@@ -493,7 +543,6 @@
         self.wopener = opener(self.root)
         self.manifest = manifest(self.opener)
         self.changelog = changelog(self.opener)
-        self.ignorefunc = None
         self.tagscache = None
         self.nodetagscache = None
 
@@ -503,29 +552,6 @@
                 self.ui.readconfig(self.opener("hgrc"))
             except IOError: pass
 
-    def ignore(self, f):
-        if not self.ignorefunc:
-            bigpat = ["^.hg/$"]
-            try:
-                l = file(self.wjoin(".hgignore"))
-                for pat in l:
-                    if pat != "\n":
-                        p = util.pconvert(pat[:-1])
-                        try:
-                            r = re.compile(p)
-                        except:
-                            self.ui.warn("ignoring invalid ignore"
-                                         + " regular expression '%s'\n" % p)
-                        else:
-                            bigpat.append(util.pconvert(pat[:-1]))
-            except IOError: pass
-
-            s = "(?:%s)" % (")|(?:".join(bigpat))
-            r = re.compile(s)
-            self.ignorefunc = r.search
-
-        return self.ignorefunc(f)
-
     def hook(self, name, **args):
         s = self.ui.config("hooks", name)
         if s:
@@ -738,7 +764,7 @@
                 else:
                     self.ui.warn("%s not tracked!\n" % f)
         else:
-            (c, a, d, u) = self.changes(None, None)
+            (c, a, d, u) = self.changes()
             commit = c + a
             remove = d
 
@@ -815,7 +841,16 @@
         if not self.hook("commit", node=hex(n)):
             return 1
 
-    def changes(self, node1, node2, files=None):
+    def walk(self, node = None, files = [], match = util.always):
+        if node:
+            for fn in self.manifest.read(self.changelog.read(node)[0]):
+                yield 'm', fn
+        else:
+            for src, fn in self.dirstate.walk(files, match):
+                yield src, fn
+
+    def changes(self, node1 = None, node2 = None, files = [],
+                match = util.always):
         mf2, u = None, []
 
         def fcmp(fn, mf):
@@ -823,16 +858,23 @@
             t2 = self.file(fn).revision(mf[fn])
             return cmp(t1, t2)
 
+        def mfmatches(node):
+            mf = dict(self.manifest.read(node))
+            for fn in mf.keys():
+                if not match(fn):
+                    del mf[fn]
+            return mf
+            
         # are we comparing the working directory?
         if not node2:
-            l, c, a, d, u = self.dirstate.changes(files, self.ignore)
+            l, c, a, d, u = self.dirstate.changes(files, match)
 
             # are we comparing working dir against its parent?
             if not node1:
                 if l:
                     # do a full compare of any files that might have changed
                     change = self.changelog.read(self.dirstate.parents()[0])
-                    mf2 = self.manifest.read(change[0])
+                    mf2 = mfmatches(change[0])
                     for f in l:
                         if fcmp(f, mf2):
                             c.append(f)
@@ -847,20 +889,20 @@
         if not node2:
             if not mf2:
                 change = self.changelog.read(self.dirstate.parents()[0])
-                mf2 = self.manifest.read(change[0]).copy()
+                mf2 = mfmatches(change[0])
             for f in a + c + l:
                 mf2[f] = ""
             for f in d:
                 if f in mf2: del mf2[f]
         else:
             change = self.changelog.read(node2)
-            mf2 = self.manifest.read(change[0])
+            mf2 = mfmatches(change[0])
 
         # flush lists from dirstate before comparing manifests
         c, a = [], []
 
         change = self.changelog.read(node1)
-        mf1 = self.manifest.read(change[0]).copy()
+        mf1 = mfmatches(change[0])
 
         for fn in mf2:
             if mf1.has_key(fn):
@@ -885,7 +927,7 @@
                 self.ui.warn("%s does not exist!\n" % f)
             elif not os.path.isfile(p):
                 self.ui.warn("%s not added: mercurial only supports files currently\n" % f)
-            elif self.dirstate.state(f) == 'n':
+            elif self.dirstate.state(f) in 'an':
                 self.ui.warn("%s already tracked!\n" % f)
             else:
                 self.dirstate.update([f], "a")
@@ -1268,7 +1310,7 @@
         ma = self.manifest.read(man)
         mfa = self.manifest.readflags(man)
 
-        (c, a, d, u) = self.changes(None, None)
+        (c, a, d, u) = self.changes()
 
         # is this a jump, or a merge?  i.e. is there a linear path
         # from p1 to p2?
--- a/mercurial/util.py	Wed Jul 20 11:40:27 2005 -0500
+++ b/mercurial/util.py	Wed Jul 20 20:00:29 2005 -0500
@@ -6,6 +6,8 @@
 # of the GNU General Public License, incorporated herein by reference.
 
 import os, errno
+from demandload import *
+demandload(globals(), "re")
 
 def unique(g):
     seen = {}
@@ -29,6 +31,54 @@
         return "stopped by signal %d" % val, val
     raise ValueError("invalid exit code")
 
+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 = ''
+    group = False
+    def peek(): return i < n and pat[i]
+    while i < n:
+        c = pat[i]
+        i = i+1
+        if c == '*':
+            if peek() == '*':
+                i += 1
+                res += '.*'
+            else:
+                res += '[^/]*'
+        elif c == '?':
+            res += '.'
+        elif c == '[':
+            j = i
+            if j < n and pat[j] in '!]':
+                j += 1
+            while j < n and pat[j] != ']':
+                j += 1
+            if j >= n:
+                res += '\\['
+            else:
+                stuff = pat[i:j].replace('\\','\\\\')
+                i = j + 1
+                if stuff[0] == '!':
+                    stuff = '^' + stuff[1:]
+                elif stuff[0] == '^':
+                    stuff = '\\' + stuff
+                res = '%s[%s]' % (res, stuff)
+        elif c == '{':
+            group = True
+            res += '(?:'
+        elif c == '}' and group:
+            res += ')'
+            group = False
+        elif c == ',' and group:
+            res += '|'
+        else:
+            res += re.escape(c)
+    return head + res + tail
+
 def system(cmd, errprefix=None):
     """execute a shell command that must succeed"""
     rc = os.system(cmd)
--- a/tests/test-help.out	Wed Jul 20 11:40:27 2005 -0500
+++ b/tests/test-help.out	Wed Jul 20 20:00:29 2005 -0500
@@ -33,15 +33,29 @@
  status     show changed files in the working directory
  update     update or merge working directory
 hg add: option -h not recognized
-hg add FILE...
+hg add [OPTIONS] [FILES]
+
+ -I --include 
+   include path in search
+ -X --exclude 
+   exclude path from search
 
 add the specified files on the next commit
 hg add: option --skjdfks not recognized
-hg add FILE...
+hg add [OPTIONS] [FILES]
+
+ -I --include 
+   include path in search
+ -X --exclude 
+   exclude path from search
 
 add the specified files on the next commit
 hg diff [-r REV1 [-r REV2]] [FILE]...
 
+ -I --include 
+   include path in search
+ -X --exclude 
+   exclude path from search
  -r --rev 
    revision