*** added file 'bzrlib/patches.py'
--- /dev/null 
+++ bzrlib/patches.py 
@@ -0,0 +1,497 @@
+# Copyright (C) 2004, 2005 Aaron Bentley
+# <aaron.bentley@utoronto.ca>
+#
+#    This program is free software; you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License as published by
+#    the Free Software Foundation; either version 2 of the License, or
+#    (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    You should have received a copy of the GNU General Public License
+#    along with this program; if not, write to the Free Software
+#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+import sys
+import progress
+class PatchSyntax(Exception):
+    def __init__(self, msg):
+        Exception.__init__(self, msg)
+
+
+class MalformedPatchHeader(PatchSyntax):
+    def __init__(self, desc, line):
+        self.desc = desc
+        self.line = line
+        msg = "Malformed patch header.  %s\n%s" % (self.desc, self.line)
+        PatchSyntax.__init__(self, msg)
+
+class MalformedHunkHeader(PatchSyntax):
+    def __init__(self, desc, line):
+        self.desc = desc
+        self.line = line
+        msg = "Malformed hunk header.  %s\n%s" % (self.desc, self.line)
+        PatchSyntax.__init__(self, msg)
+
+class MalformedLine(PatchSyntax):
+    def __init__(self, desc, line):
+        self.desc = desc
+        self.line = line
+        msg = "Malformed line.  %s\n%s" % (self.desc, self.line)
+        PatchSyntax.__init__(self, msg)
+
+def get_patch_names(iter_lines):
+    try:
+        line = iter_lines.next()
+        if not line.startswith("--- "):
+            raise MalformedPatchHeader("No orig name", line)
+        else:
+            orig_name = line[4:].rstrip("\n")
+    except StopIteration:
+        raise MalformedPatchHeader("No orig line", "")
+    try:
+        line = iter_lines.next()
+        if not line.startswith("+++ "):
+            raise PatchSyntax("No mod name")
+        else:
+            mod_name = line[4:].rstrip("\n")
+    except StopIteration:
+        raise MalformedPatchHeader("No mod line", "")
+    return (orig_name, mod_name)
+
+def parse_range(textrange):
+    """Parse a patch range, handling the "1" special-case
+
+    :param textrange: The text to parse
+    :type textrange: str
+    :return: the position and range, as a tuple
+    :rtype: (int, int)
+    """
+    tmp = textrange.split(',')
+    if len(tmp) == 1:
+        pos = tmp[0]
+        range = "1"
+    else:
+        (pos, range) = tmp
+    pos = int(pos)
+    range = int(range)
+    return (pos, range)
+
+ 
+def hunk_from_header(line):
+    if not line.startswith("@@") or not line.endswith("@@\n") \
+        or not len(line) > 4:
+        raise MalformedHunkHeader("Does not start and end with @@.", line)
+    try:
+        (orig, mod) = line[3:-4].split(" ")
+    except Exception, e:
+        raise MalformedHunkHeader(str(e), line)
+    if not orig.startswith('-') or not mod.startswith('+'):
+        raise MalformedHunkHeader("Positions don't start with + or -.", line)
+    try:
+        (orig_pos, orig_range) = parse_range(orig[1:])
+        (mod_pos, mod_range) = parse_range(mod[1:])
+    except Exception, e:
+        raise MalformedHunkHeader(str(e), line)
+    if mod_range < 0 or orig_range < 0:
+        raise MalformedHunkHeader("Hunk range is negative", line)
+    return Hunk(orig_pos, orig_range, mod_pos, mod_range)
+
+
+class HunkLine:
+    def __init__(self, contents):
+        self.contents = contents
+
+    def get_str(self, leadchar):
+        if self.contents == "\n" and leadchar == " " and False:
+            return "\n"
+        return leadchar + self.contents
+
+class ContextLine(HunkLine):
+    def __init__(self, contents):
+        HunkLine.__init__(self, contents)
+
+    def __str__(self):
+        return self.get_str(" ")
+
+
+class InsertLine(HunkLine):
+    def __init__(self, contents):
+        HunkLine.__init__(self, contents)
+
+    def __str__(self):
+        return self.get_str("+")
+
+
+class RemoveLine(HunkLine):
+    def __init__(self, contents):
+        HunkLine.__init__(self, contents)
+
+    def __str__(self):
+        return self.get_str("-")
+
+__pychecker__="no-returnvalues"
+def parse_line(line):
+    if line.startswith("\n"):
+        return ContextLine(line)
+    elif line.startswith(" "):
+        return ContextLine(line[1:])
+    elif line.startswith("+"):
+        return InsertLine(line[1:])
+    elif line.startswith("-"):
+        return RemoveLine(line[1:])
+    else:
+        raise MalformedLine("Unknown line type", line)
+__pychecker__=""
+
+
+class Hunk:
+    def __init__(self, orig_pos, orig_range, mod_pos, mod_range):
+        self.orig_pos = orig_pos
+        self.orig_range = orig_range
+        self.mod_pos = mod_pos
+        self.mod_range = mod_range
+        self.lines = []
+
+    def get_header(self):
+        return "@@ -%s +%s @@\n" % (self.range_str(self.orig_pos, 
+                                                   self.orig_range),
+                                    self.range_str(self.mod_pos, 
+                                                   self.mod_range))
+
+    def range_str(self, pos, range):
+        """Return a file range, special-casing for 1-line files.
+
+        :param pos: The position in the file
+        :type pos: int
+        :range: The range in the file
+        :type range: int
+        :return: a string in the format 1,4 except when range == pos == 1
+        """
+        if range == 1:
+            return "%i" % pos
+        else:
+            return "%i,%i" % (pos, range)
+
+    def __str__(self):
+        lines = [self.get_header()]
+        for line in self.lines:
+            lines.append(str(line))
+        return "".join(lines)
+
+    def shift_to_mod(self, pos):
+        if pos < self.orig_pos-1:
+            return 0
+        elif pos > self.orig_pos+self.orig_range:
+            return self.mod_range - self.orig_range
+        else:
+            return self.shift_to_mod_lines(pos)
+
+    def shift_to_mod_lines(self, pos):
+        assert (pos >= self.orig_pos-1 and pos <= self.orig_pos+self.orig_range)
+        position = self.orig_pos-1
+        shift = 0
+        for line in self.lines:
+            if isinstance(line, InsertLine):
+                shift += 1
+            elif isinstance(line, RemoveLine):
+                if position == pos:
+                    return None
+                shift -= 1
+                position += 1
+            elif isinstance(line, ContextLine):
+                position += 1
+            if position > pos:
+                break
+        return shift
+
+def iter_hunks(iter_lines):
+    hunk = None
+    for line in iter_lines:
+        if line.startswith("@@"):
+            if hunk is not None:
+                yield hunk
+            hunk = hunk_from_header(line)
+        else:
+            hunk.lines.append(parse_line(line))
+
+    if hunk is not None:
+        yield hunk
+
+class Patch:
+    def __init__(self, oldname, newname):
+        self.oldname = oldname
+        self.newname = newname
+        self.hunks = []
+
+    def __str__(self):
+        ret =  "--- %s\n+++ %s\n" % (self.oldname, self.newname) 
+        ret += "".join([str(h) for h in self.hunks])
+        return ret
+
+    def stats_str(self):
+        """Return a string of patch statistics"""
+        removes = 0
+        inserts = 0
+        for hunk in self.hunks:
+            for line in hunk.lines:
+                if isinstance(line, InsertLine):
+                     inserts+=1;
+                elif isinstance(line, RemoveLine):
+                     removes+=1;
+        return "%i inserts, %i removes in %i hunks" % \
+            (inserts, removes, len(self.hunks))
+
+    def pos_in_mod(self, position):
+        newpos = position
+        for hunk in self.hunks:
+            shift = hunk.shift_to_mod(position)
+            if shift is None:
+                return None
+            newpos += shift
+        return newpos
+            
+    def iter_inserted(self):
+        """Iteraties through inserted lines
+        
+        :return: Pair of line number, line
+        :rtype: iterator of (int, InsertLine)
+        """
+        for hunk in self.hunks:
+            pos = hunk.mod_pos - 1;
+            for line in hunk.lines:
+                if isinstance(line, InsertLine):
+                    yield (pos, line)
+                    pos += 1
+                if isinstance(line, ContextLine):
+                    pos += 1
+
+def parse_patch(iter_lines):
+    (orig_name, mod_name) = get_patch_names(iter_lines)
+    patch = Patch(orig_name, mod_name)
+    for hunk in iter_hunks(iter_lines):
+        patch.hunks.append(hunk)
+    return patch
+
+
+class AnnotateLine:
+    """A line associated with the log that produced it"""
+    def __init__(self, text, log=None):
+        self.text = text
+        self.log = log
+
+class CantGetRevisionData(Exception):
+    def __init__(self, revision):
+        Exception.__init__(self, "Can't get data for revision %s" % revision)
+        
+def annotate_file2(file_lines, anno_iter):
+    for result in iter_annotate_file(file_lines, anno_iter):
+        pass
+    return result
+
+        
+def iter_annotate_file(file_lines, anno_iter):
+    lines = [AnnotateLine(f) for f in file_lines]
+    patches = []
+    try:
+        for result in anno_iter:
+            if isinstance(result, progress.Progress):
+                yield result
+                continue
+            log, iter_inserted, patch = result
+            for (num, line) in iter_inserted:
+                old_num = num
+
+                for cur_patch in patches:
+                    num = cur_patch.pos_in_mod(num)
+                    if num == None: 
+                        break
+
+                if num >= len(lines):
+                    continue
+                if num is not None and lines[num].log is None:
+                    lines[num].log = log
+            patches=[patch]+patches
+    except CantGetRevisionData:
+        pass
+    yield lines
+
+
+def difference_index(atext, btext):
+    """Find the indext of the first character that differs betweeen two texts
+
+    :param atext: The first text
+    :type atext: str
+    :param btext: The second text
+    :type str: str
+    :return: The index, or None if there are no differences within the range
+    :rtype: int or NoneType
+    """
+    length = len(atext)
+    if len(btext) < length:
+        length = len(btext)
+    for i in range(length):
+        if atext[i] != btext[i]:
+            return i;
+    return None
+
+
+def test():
+    import unittest
+    class PatchesTester(unittest.TestCase):
+        def testValidPatchHeader(self):
+            """Parse a valid patch header"""
+            lines = "--- orig/commands.py\n+++ mod/dommands.py\n".split('\n')
+            (orig, mod) = get_patch_names(lines.__iter__())
+            assert(orig == "orig/commands.py")
+            assert(mod == "mod/dommands.py")
+
+        def testInvalidPatchHeader(self):
+            """Parse an invalid patch header"""
+            lines = "-- orig/commands.py\n+++ mod/dommands.py".split('\n')
+            self.assertRaises(MalformedPatchHeader, get_patch_names,
+                              lines.__iter__())
+
+        def testValidHunkHeader(self):
+            """Parse a valid hunk header"""
+            header = "@@ -34,11 +50,6 @@\n"
+            hunk = hunk_from_header(header);
+            assert (hunk.orig_pos == 34)
+            assert (hunk.orig_range == 11)
+            assert (hunk.mod_pos == 50)
+            assert (hunk.mod_range == 6)
+            assert (str(hunk) == header)
+
+        def testValidHunkHeader2(self):
+            """Parse a tricky, valid hunk header"""
+            header = "@@ -1 +0,0 @@\n"
+            hunk = hunk_from_header(header);
+            assert (hunk.orig_pos == 1)
+            assert (hunk.orig_range == 1)
+            assert (hunk.mod_pos == 0)
+            assert (hunk.mod_range == 0)
+            assert (str(hunk) == header)
+
+        def makeMalformed(self, header):
+            self.assertRaises(MalformedHunkHeader, hunk_from_header, header)
+
+        def testInvalidHeader(self):
+            """Parse an invalid hunk header"""
+            self.makeMalformed(" -34,11 +50,6 \n")
+            self.makeMalformed("@@ +50,6 -34,11 @@\n")
+            self.makeMalformed("@@ -34,11 +50,6 @@")
+            self.makeMalformed("@@ -34.5,11 +50,6 @@\n")
+            self.makeMalformed("@@-34,11 +50,6@@\n")
+            self.makeMalformed("@@ 34,11 50,6 @@\n")
+            self.makeMalformed("@@ -34,11 @@\n")
+            self.makeMalformed("@@ -34,11 +50,6.5 @@\n")
+            self.makeMalformed("@@ -34,11 +50,-6 @@\n")
+
+        def lineThing(self,text, type):
+            line = parse_line(text)
+            assert(isinstance(line, type))
+            assert(str(line)==text)
+
+        def makeMalformedLine(self, text):
+            self.assertRaises(MalformedLine, parse_line, text)
+
+        def testValidLine(self):
+            """Parse a valid hunk line"""
+            self.lineThing(" hello\n", ContextLine)
+            self.lineThing("+hello\n", InsertLine)
+            self.lineThing("-hello\n", RemoveLine)
+        
+        def testMalformedLine(self):
+            """Parse invalid valid hunk lines"""
+            self.makeMalformedLine("hello\n")
+        
+        def compare_parsed(self, patchtext):
+            lines = patchtext.splitlines(True)
+            patch = parse_patch(lines.__iter__())
+            pstr = str(patch)
+            i = difference_index(patchtext, pstr)
+            if i is not None:
+                print "%i: \"%s\" != \"%s\"" % (i, patchtext[i], pstr[i])
+            assert (patchtext == str(patch))
+
+        def testAll(self):
+            """Test parsing a whole patch"""
+            patchtext = """--- orig/commands.py
++++ mod/commands.py
+@@ -1337,7 +1337,8 @@
+ 
+     def set_title(self, command=None):
+         try:
+-            version = self.tree.tree_version.nonarch
++            version = pylon.alias_or_version(self.tree.tree_version, self.tree,
++                                             full=False)
+         except:
+             version = "[no version]"
+         if command is None:
+@@ -1983,7 +1984,11 @@
+                                          version)
+         if len(new_merges) > 0:
+             if cmdutil.prompt("Log for merge"):
+-                mergestuff = cmdutil.log_for_merge(tree, comp_version)
++                if cmdutil.prompt("changelog for merge"):
++                    mergestuff = "Patches applied:\\n"
++                    mergestuff += pylon.changelog_for_merge(new_merges)
++                else:
++                    mergestuff = cmdutil.log_for_merge(tree, comp_version)
+                 log.description += mergestuff
+         log.save()
+     try:
+"""
+            self.compare_parsed(patchtext)
+
+        def testInit(self):
+            """Handle patches missing half the position, range tuple"""
+            patchtext = \
+"""--- orig/__init__.py
++++ mod/__init__.py
+@@ -1 +1,2 @@
+ __docformat__ = "restructuredtext en"
++__doc__ = An alternate Arch commandline interface"""
+            self.compare_parsed(patchtext)
+            
+
+
+        def testLineLookup(self):
+            """Make sure we can accurately look up mod line from orig"""
+            patch = parse_patch(open("testdata/diff"))
+            orig = list(open("testdata/orig"))
+            mod = list(open("testdata/mod"))
+            removals = []
+            for i in range(len(orig)):
+                mod_pos = patch.pos_in_mod(i)
+                if mod_pos is None:
+                    removals.append(orig[i])
+                    continue
+                assert(mod[mod_pos]==orig[i])
+            rem_iter = removals.__iter__()
+            for hunk in patch.hunks:
+                for line in hunk.lines:
+                    if isinstance(line, RemoveLine):
+                        next = rem_iter.next()
+                        if line.contents != next:
+                            sys.stdout.write(" orig:%spatch:%s" % (next,
+                                             line.contents))
+                        assert(line.contents == next)
+            self.assertRaises(StopIteration, rem_iter.next)
+
+        def testFirstLineRenumber(self):
+            """Make sure we handle lines at the beginning of the hunk"""
+            patch = parse_patch(open("testdata/insert_top.patch"))
+            assert (patch.pos_in_mod(0)==1)
+    
+            
+    patchesTestSuite = unittest.makeSuite(PatchesTester,'test')
+    runner = unittest.TextTestRunner(verbosity=0)
+    return runner.run(patchesTestSuite)
+    
+
+if __name__ == "__main__":
+    test()
+# arch-tag: d1541a25-eac5-4de9-a476-08a7cecd5683

*** added directory 'testdata'
*** added file 'testdata/diff'
--- /dev/null 
+++ testdata/diff 
@@ -0,0 +1,1154 @@
+--- orig/commands.py
++++ mod/commands.py
+@@ -19,25 +19,31 @@
+ import arch
+ import arch.util
+ import arch.arch
++
++import pylon.errors
++from pylon.errors import *
++from pylon import errors
++from pylon import util
++from pylon import arch_core
++from pylon import arch_compound
++from pylon import ancillary
++from pylon import misc
++from pylon import paths 
++
+ import abacmds
+ import cmdutil
+ import shutil
+ import os
+ import options
+-import paths 
+ import time
+ import cmd
+ import readline
+ import re
+ import string
+-import arch_core
+-from errors import *
+-import errors
+ import terminal
+-import ancillary
+-import misc
+ import email
+ import smtplib
++import textwrap
+ 
+ __docformat__ = "restructuredtext"
+ __doc__ = "Implementation of user (sub) commands"
+@@ -257,7 +263,7 @@
+ 
+         tree=arch.tree_root()
+         if len(args) == 0:
+-            a_spec = cmdutil.comp_revision(tree)
++            a_spec = ancillary.comp_revision(tree)
+         else:
+             a_spec = cmdutil.determine_revision_tree(tree, args[0])
+         cmdutil.ensure_archive_registered(a_spec.archive)
+@@ -284,7 +290,7 @@
+             changeset=options.changeset
+             tmpdir = None
+         else:
+-            tmpdir=cmdutil.tmpdir()
++            tmpdir=util.tmpdir()
+             changeset=tmpdir+"/changeset"
+         try:
+             delta=arch.iter_delta(a_spec, b_spec, changeset)
+@@ -304,14 +310,14 @@
+             if status > 1:
+                 return
+             if (options.perform_diff):
+-                chan = cmdutil.ChangesetMunger(changeset)
++                chan = arch_compound.ChangesetMunger(changeset)
+                 chan.read_indices()
+-                if isinstance(b_spec, arch.Revision):
+-                    b_dir = b_spec.library_find()
+-                else:
+-                    b_dir = b_spec
+-                a_dir = a_spec.library_find()
+                 if options.diffopts is not None:
++                    if isinstance(b_spec, arch.Revision):
++                        b_dir = b_spec.library_find()
++                    else:
++                        b_dir = b_spec
++                    a_dir = a_spec.library_find()
+                     diffopts = options.diffopts.split()
+                     cmdutil.show_custom_diffs(chan, diffopts, a_dir, b_dir)
+                 else:
+@@ -517,7 +523,7 @@
+         except arch.errors.TreeRootError, e:
+             print e
+             return
+-        from_revision=cmdutil.tree_latest(tree)
++        from_revision = arch_compound.tree_latest(tree)
+         if from_revision==to_revision:
+             print "Tree is already up to date with:\n"+str(to_revision)+"."
+             return
+@@ -592,6 +598,9 @@
+ 
+         if len(args) == 0:
+             args = None
++        if options.version is None:
++            return options, tree.tree_version, args
++
+         revision=cmdutil.determine_revision_arch(tree, options.version)
+         return options, revision.get_version(), args
+ 
+@@ -601,11 +610,16 @@
+         """
+         tree=arch.tree_root()
+         options, version, files = self.parse_commandline(cmdargs, tree)
++        ancestor = None
+         if options.__dict__.has_key("base") and options.base:
+             base = cmdutil.determine_revision_tree(tree, options.base)
++            ancestor = base
+         else:
+-            base = cmdutil.submit_revision(tree)
+-        
++            base = ancillary.submit_revision(tree)
++            ancestor = base
++        if ancestor is None:
++            ancestor = arch_compound.tree_latest(tree, version)
++
+         writeversion=version
+         archive=version.archive
+         source=cmdutil.get_mirror_source(archive)
+@@ -625,18 +639,26 @@
+         try:
+             last_revision=tree.iter_logs(version, True).next().revision
+         except StopIteration, e:
+-            if cmdutil.prompt("Import from commit"):
+-                return do_import(version)
+-            else:
+-                raise NoVersionLogs(version)
+-        if last_revision!=version.iter_revisions(True).next():
++            last_revision = None
++            if ancestor is None:
++                if cmdutil.prompt("Import from commit"):
++                    return do_import(version)
++                else:
++                    raise NoVersionLogs(version)
++        try:
++            arch_last_revision = version.iter_revisions(True).next()
++        except StopIteration, e:
++            arch_last_revision = None
++ 
++        if last_revision != arch_last_revision:
++            print "Tree is not up to date with %s" % str(version)
+             if not cmdutil.prompt("Out of date"):
+                 raise OutOfDate
+             else:
+                 allow_old=True
+ 
+         try:
+-            if not cmdutil.has_changed(version):
++            if not cmdutil.has_changed(ancestor):
+                 if not cmdutil.prompt("Empty commit"):
+                     raise EmptyCommit
+         except arch.util.ExecProblem, e:
+@@ -645,15 +667,15 @@
+                 raise MissingID(e)
+             else:
+                 raise
+-        log = tree.log_message(create=False)
++        log = tree.log_message(create=False, version=version)
+         if log is None:
+             try:
+                 if cmdutil.prompt("Create log"):
+-                    edit_log(tree)
++                    edit_log(tree, version)
+ 
+             except cmdutil.NoEditorSpecified, e:
+                 raise CommandFailed(e)
+-            log = tree.log_message(create=False)
++            log = tree.log_message(create=False, version=version)
+         if log is None: 
+             raise NoLogMessage
+         if log["Summary"] is None or len(log["Summary"].strip()) == 0:
+@@ -837,23 +859,24 @@
+             if spec is not None:
+                 revision = cmdutil.determine_revision_tree(tree, spec)
+             else:
+-                revision = cmdutil.comp_revision(tree)
++                revision = ancillary.comp_revision(tree)
+         except cmdutil.CantDetermineRevision, e:
+             raise CommandFailedWrapper(e)
+         munger = None
+ 
+         if options.file_contents or options.file_perms or options.deletions\
+             or options.additions or options.renames or options.hunk_prompt:
+-            munger = cmdutil.MungeOpts()
+-            munger.hunk_prompt = options.hunk_prompt
++            munger = arch_compound.MungeOpts()
++            munger.set_hunk_prompt(cmdutil.colorize, cmdutil.user_hunk_confirm,
++                                   options.hunk_prompt)
+ 
+         if len(args) > 0 or options.logs or options.pattern_files or \
+             options.control:
+             if munger is None:
+-                munger = cmdutil.MungeOpts(True)
++                munger = cmdutil.arch_compound.MungeOpts(True)
+                 munger.all_types(True)
+         if len(args) > 0:
+-            t_cwd = cmdutil.tree_cwd(tree)
++            t_cwd = arch_compound.tree_cwd(tree)
+             for name in args:
+                 if len(t_cwd) > 0:
+                     t_cwd += "/"
+@@ -878,7 +901,7 @@
+         if options.pattern_files:
+             munger.add_keep_pattern(options.pattern_files)
+                 
+-        for line in cmdutil.revert(tree, revision, munger, 
++        for line in arch_compound.revert(tree, revision, munger, 
+                                    not options.no_output):
+             cmdutil.colorize(line)
+ 
+@@ -1042,18 +1065,13 @@
+         help_tree_spec()
+         return
+ 
+-def require_version_exists(version, spec):
+-    if not version.exists():
+-        raise cmdutil.CantDetermineVersion(spec, 
+-                                           "The version %s does not exist." \
+-                                           % version)
+-
+ class Revisions(BaseCommand):
+     """
+     Print a revision name based on a revision specifier
+     """
+     def __init__(self):
+         self.description="Lists revisions"
++        self.cl_revisions = []
+     
+     def do_command(self, cmdargs):
+         """
+@@ -1066,224 +1084,68 @@
+             self.tree = arch.tree_root()
+         except arch.errors.TreeRootError:
+             self.tree = None
++        if options.type == "default":
++            options.type = "archive"
+         try:
+-            iter = self.get_iterator(options.type, args, options.reverse, 
+-                                     options.modified)
++            iter = cmdutil.revision_iterator(self.tree, options.type, args, 
++                                             options.reverse, options.modified,
++                                             options.shallow)
+         except cmdutil.CantDetermineRevision, e:
+             raise CommandFailedWrapper(e)
+-
++        except cmdutil.CantDetermineVersion, e:
++            raise CommandFailedWrapper(e)
+         if options.skip is not None:
+             iter = cmdutil.iter_skip(iter, int(options.skip))
+ 
+-        for revision in iter:
+-            log = None
+-            if isinstance(revision, arch.Patchlog):
+-                log = revision
+-                revision=revision.revision
+-            print options.display(revision)
+-            if log is None and (options.summary or options.creator or 
+-                                options.date or options.merges):
+-                log = revision.patchlog
+-            if options.creator:
+-                print "    %s" % log.creator
+-            if options.date:
+-                print "    %s" % time.strftime('%Y-%m-%d %H:%M:%S %Z', log.date)
+-            if options.summary:
+-                print "    %s" % log.summary
+-            if options.merges:
+-                showed_title = False
+-                for revision in log.merged_patches:
+-                    if not showed_title:
+-                        print "    Merged:"
+-                        showed_title = True
+-                    print "    %s" % revision
+-
+-    def get_iterator(self, type, args, reverse, modified):
+-        if len(args) > 0:
+-            spec = args[0]
+-        else:
+-            spec = None
+-        if modified is not None:
+-            iter = cmdutil.modified_iter(modified, self.tree)
+-            if reverse:
+-                return iter
+-            else:
+-                return cmdutil.iter_reverse(iter)
+-        elif type == "archive":
+-            if spec is None:
+-                if self.tree is None:
+-                    raise cmdutil.CantDetermineRevision("", 
+-                                                        "Not in a project tree")
+-                version = cmdutil.determine_version_tree(spec, self.tree)
+-            else:
+-                version = cmdutil.determine_version_arch(spec, self.tree)
+-                cmdutil.ensure_archive_registered(version.archive)
+-                require_version_exists(version, spec)
+-            return version.iter_revisions(reverse)
+-        elif type == "cacherevs":
+-            if spec is None:
+-                if self.tree is None:
+-                    raise cmdutil.CantDetermineRevision("", 
+-                                                        "Not in a project tree")
+-                version = cmdutil.determine_version_tree(spec, self.tree)
+-            else:
+-                version = cmdutil.determine_version_arch(spec, self.tree)
+-                cmdutil.ensure_archive_registered(version.archive)
+-                require_version_exists(version, spec)
+-            return cmdutil.iter_cacherevs(version, reverse)
+-        elif type == "library":
+-            if spec is None:
+-                if self.tree is None:
+-                    raise cmdutil.CantDetermineRevision("", 
+-                                                        "Not in a project tree")
+-                version = cmdutil.determine_version_tree(spec, self.tree)
+-            else:
+-                version = cmdutil.determine_version_arch(spec, self.tree)
+-            return version.iter_library_revisions(reverse)
+-        elif type == "logs":
+-            if self.tree is None:
+-                raise cmdutil.CantDetermineRevision("", "Not in a project tree")
+-            return self.tree.iter_logs(cmdutil.determine_version_tree(spec, \
+-                                  self.tree), reverse)
+-        elif type == "missing" or type == "skip-present":
+-            if self.tree is None:
+-                raise cmdutil.CantDetermineRevision("", "Not in a project tree")
+-            skip = (type == "skip-present")
+-            version = cmdutil.determine_version_tree(spec, self.tree)
+-            cmdutil.ensure_archive_registered(version.archive)
+-            require_version_exists(version, spec)
+-            return cmdutil.iter_missing(self.tree, version, reverse,
+-                                        skip_present=skip)
+-
+-        elif type == "present":
+-            if self.tree is None:
+-                raise cmdutil.CantDetermineRevision("", "Not in a project tree")
+-            version = cmdutil.determine_version_tree(spec, self.tree)
+-            cmdutil.ensure_archive_registered(version.archive)
+-            require_version_exists(version, spec)
+-            return cmdutil.iter_present(self.tree, version, reverse)
+-
+-        elif type == "new-merges" or type == "direct-merges":
+-            if self.tree is None:
+-                raise cmdutil.CantDetermineRevision("", "Not in a project tree")
+-            version = cmdutil.determine_version_tree(spec, self.tree)
+-            cmdutil.ensure_archive_registered(version.archive)
+-            require_version_exists(version, spec)
+-            iter = cmdutil.iter_new_merges(self.tree, version, reverse)
+-            if type == "new-merges":
+-                return iter
+-            elif type == "direct-merges":
+-                return cmdutil.direct_merges(iter)
+-
+-        elif type == "missing-from":
+-            if self.tree is None:
+-                raise cmdutil.CantDetermineRevision("", "Not in a project tree")
+-            revision = cmdutil.determine_revision_tree(self.tree, spec)
+-            libtree = cmdutil.find_or_make_local_revision(revision)
+-            return cmdutil.iter_missing(libtree, self.tree.tree_version,
+-                                        reverse)
+-
+-        elif type == "partner-missing":
+-            return cmdutil.iter_partner_missing(self.tree, reverse)
+-
+-        elif type == "ancestry":
+-            revision = cmdutil.determine_revision_tree(self.tree, spec)
+-            iter = cmdutil._iter_ancestry(self.tree, revision)
+-            if reverse:
+-                return iter
+-            else:
+-                return cmdutil.iter_reverse(iter)
+-
+-        elif type == "dependencies" or type == "non-dependencies":
+-            nondeps = (type == "non-dependencies")
+-            revision = cmdutil.determine_revision_tree(self.tree, spec)
+-            anc_iter = cmdutil._iter_ancestry(self.tree, revision)
+-            iter_depends = cmdutil.iter_depends(anc_iter, nondeps)
+-            if reverse:
+-                return iter_depends
+-            else:
+-                return cmdutil.iter_reverse(iter_depends)
+-        elif type == "micro":
+-            return cmdutil.iter_micro(self.tree)
+-
+-    
++        try:
++            for revision in iter:
++                log = None
++                if isinstance(revision, arch.Patchlog):
++                    log = revision
++                    revision=revision.revision
++                out = options.display(revision)
++                if out is not None:
++                    print out
++                if log is None and (options.summary or options.creator or 
++                                    options.date or options.merges):
++                    log = revision.patchlog
++                if options.creator:
++                    print "    %s" % log.creator
++                if options.date:
++                    print "    %s" % time.strftime('%Y-%m-%d %H:%M:%S %Z', log.date)
++                if options.summary:
++                    print "    %s" % log.summary
++                if options.merges:
++                    showed_title = False
++                    for revision in log.merged_patches:
++                        if not showed_title:
++                            print "    Merged:"
++                            showed_title = True
++                        print "    %s" % revision
++            if len(self.cl_revisions) > 0:
++                print pylon.changelog_for_merge(self.cl_revisions)
++        except pylon.errors.TreeRootNone:
++            raise CommandFailedWrapper(
++                Exception("This option can only be used in a project tree."))
++
++    def changelog_append(self, revision):
++        if isinstance(revision, arch.Revision):
++            revision=arch.Patchlog(revision)
++        self.cl_revisions.append(revision)
++   
+     def get_parser(self):
+         """
+         Returns the options parser to use for the "revision" command.
+ 
+         :rtype: cmdutil.CmdOptionParser
+         """
+-        parser=cmdutil.CmdOptionParser("fai revisions [revision]")
++        parser=cmdutil.CmdOptionParser("fai revisions [version/revision]")
+         select = cmdutil.OptionGroup(parser, "Selection options",
+                           "Control which revisions are listed.  These options"
+                           " are mutually exclusive.  If more than one is"
+                           " specified, the last is used.")
+-        select.add_option("", "--archive", action="store_const", 
+-                          const="archive", dest="type", default="archive",
+-                          help="List all revisions in the archive")
+-        select.add_option("", "--cacherevs", action="store_const", 
+-                          const="cacherevs", dest="type",
+-                          help="List all revisions stored in the archive as "
+-                          "complete copies")
+-        select.add_option("", "--logs", action="store_const", 
+-                          const="logs", dest="type",
+-                          help="List revisions that have a patchlog in the "
+-                          "tree")
+-        select.add_option("", "--missing", action="store_const", 
+-                          const="missing", dest="type",
+-                          help="List revisions from the specified version that"
+-                          " have no patchlog in the tree")
+-        select.add_option("", "--skip-present", action="store_const", 
+-                          const="skip-present", dest="type",
+-                          help="List revisions from the specified version that"
+-                          " have no patchlogs at all in the tree")
+-        select.add_option("", "--present", action="store_const", 
+-                          const="present", dest="type",
+-                          help="List revisions from the specified version that"
+-                          " have no patchlog in the tree, but can't be merged")
+-        select.add_option("", "--missing-from", action="store_const", 
+-                          const="missing-from", dest="type",
+-                          help="List revisions from the specified revision "
+-                          "that have no patchlog for the tree version")
+-        select.add_option("", "--partner-missing", action="store_const", 
+-                          const="partner-missing", dest="type",
+-                          help="List revisions in partner versions that are"
+-                          " missing")
+-        select.add_option("", "--new-merges", action="store_const", 
+-                          const="new-merges", dest="type",
+-                          help="List revisions that have had patchlogs added"
+-                          " to the tree since the last commit")
+-        select.add_option("", "--direct-merges", action="store_const", 
+-                          const="direct-merges", dest="type",
+-                          help="List revisions that have been directly added"
+-                          " to tree since the last commit ")
+-        select.add_option("", "--library", action="store_const", 
+-                          const="library", dest="type",
+-                          help="List revisions in the revision library")
+-        select.add_option("", "--ancestry", action="store_const", 
+-                          const="ancestry", dest="type",
+-                          help="List revisions that are ancestors of the "
+-                          "current tree version")
+-
+-        select.add_option("", "--dependencies", action="store_const", 
+-                          const="dependencies", dest="type",
+-                          help="List revisions that the given revision "
+-                          "depends on")
+-
+-        select.add_option("", "--non-dependencies", action="store_const", 
+-                          const="non-dependencies", dest="type",
+-                          help="List revisions that the given revision "
+-                          "does not depend on")
+-
+-        select.add_option("--micro", action="store_const", 
+-                          const="micro", dest="type",
+-                          help="List partner revisions aimed for this "
+-                          "micro-branch")
+-
+-        select.add_option("", "--modified", dest="modified", 
+-                          help="List tree ancestor revisions that modified a "
+-                          "given file", metavar="FILE[:LINE]")
+ 
++        cmdutil.add_revision_iter_options(select)
+         parser.add_option("", "--skip", dest="skip", 
+                           help="Skip revisions.  Positive numbers skip from "
+                           "beginning, negative skip from end.",
+@@ -1312,6 +1174,9 @@
+         format.add_option("--cacherev", action="store_const", 
+                          const=paths.determine_cacherev_path, dest="display",
+                          help="Show location of cacherev file")
++        format.add_option("--changelog", action="store_const", 
++                         const=self.changelog_append, dest="display",
++                         help="Show location of cacherev file")
+         parser.add_option_group(format)
+         display = cmdutil.OptionGroup(parser, "Display format options",
+                           "These control the display of data")
+@@ -1448,6 +1313,7 @@
+         if os.access(self.history_file, os.R_OK) and \
+             os.path.isfile(self.history_file):
+             readline.read_history_file(self.history_file)
++        self.cwd = os.getcwd()
+ 
+     def write_history(self):
+         readline.write_history_file(self.history_file)
+@@ -1470,16 +1336,21 @@
+     def set_prompt(self):
+         if self.tree is not None:
+             try:
+-                version = " "+self.tree.tree_version.nonarch
++                prompt = pylon.alias_or_version(self.tree.tree_version, 
++                                                self.tree, 
++                                                full=False)
++                if prompt is not None:
++                    prompt = " " + prompt
+             except:
+-                version = ""
++                prompt = ""
+         else:
+-            version = ""
+-        self.prompt = "Fai%s> " % version
++            prompt = ""
++        self.prompt = "Fai%s> " % prompt
+ 
+     def set_title(self, command=None):
+         try:
+-            version = self.tree.tree_version.nonarch
++            version = pylon.alias_or_version(self.tree.tree_version, self.tree, 
++                                             full=False)
+         except:
+             version = "[no version]"
+         if command is None:
+@@ -1489,8 +1360,15 @@
+     def do_cd(self, line):
+         if line == "":
+             line = "~"
++        line = os.path.expanduser(line)
++        if os.path.isabs(line):
++            newcwd = line
++        else:
++            newcwd = self.cwd+'/'+line
++        newcwd = os.path.normpath(newcwd)
+         try:
+-            os.chdir(os.path.expanduser(line))
++            os.chdir(newcwd)
++            self.cwd = newcwd
+         except Exception, e:
+             print e
+         try:
+@@ -1523,7 +1401,7 @@
+             except cmdutil.CantDetermineRevision, e:
+                 print e
+             except Exception, e:
+-                print "Unhandled error:\n%s" % cmdutil.exception_str(e)
++                print "Unhandled error:\n%s" % errors.exception_str(e)
+ 
+         elif suggestions.has_key(args[0]):
+             print suggestions[args[0]]
+@@ -1574,7 +1452,7 @@
+                 arg = line.split()[-1]
+             else:
+                 arg = ""
+-            iter = iter_munged_completions(iter, arg, text)
++            iter = cmdutil.iter_munged_completions(iter, arg, text)
+         except Exception, e:
+             print e
+         return list(iter)
+@@ -1604,10 +1482,11 @@
+                 else:
+                     arg = ""
+                 if arg.startswith("-"):
+-                    return list(iter_munged_completions(iter, arg, text))
++                    return list(cmdutil.iter_munged_completions(iter, arg, 
++                                                                text))
+                 else:
+-                    return list(iter_munged_completions(
+-                        iter_file_completions(arg), arg, text))
++                    return list(cmdutil.iter_munged_completions(
++                        cmdutil.iter_file_completions(arg), arg, text))
+ 
+ 
+             elif cmd == "cd":
+@@ -1615,13 +1494,13 @@
+                     arg = args.split()[-1]
+                 else:
+                     arg = ""
+-                iter = iter_dir_completions(arg)
+-                iter = iter_munged_completions(iter, arg, text)
++                iter = cmdutil.iter_dir_completions(arg)
++                iter = cmdutil.iter_munged_completions(iter, arg, text)
+                 return list(iter)
+             elif len(args)>0:
+                 arg = args.split()[-1]
+-                return list(iter_munged_completions(iter_file_completions(arg),
+-                                                    arg, text))
++                iter = cmdutil.iter_file_completions(arg)
++                return list(cmdutil.iter_munged_completions(iter, arg, text))
+             else:
+                 return self.completenames(text, line, begidx, endidx)
+         except Exception, e:
+@@ -1636,44 +1515,8 @@
+             yield entry
+ 
+ 
+-def iter_file_completions(arg, only_dirs = False):
+-    """Generate an iterator that iterates through filename completions.
+-
+-    :param arg: The filename fragment to match
+-    :type arg: str
+-    :param only_dirs: If true, match only directories
+-    :type only_dirs: bool
+-    """
+-    cwd = os.getcwd()
+-    if cwd != "/":
+-        extras = [".", ".."]
+-    else:
+-        extras = []
+-    (dir, file) = os.path.split(arg)
+-    if dir != "":
+-        listingdir = os.path.expanduser(dir)
+-    else:
+-        listingdir = cwd
+-    for file in cmdutil.iter_combine([os.listdir(listingdir), extras]):
+-        if dir != "":
+-            userfile = dir+'/'+file
+-        else:
+-            userfile = file
+-        if userfile.startswith(arg):
+-            if os.path.isdir(listingdir+'/'+file):
+-                userfile+='/'
+-                yield userfile
+-            elif not only_dirs:
+-                yield userfile
+-
+-def iter_munged_completions(iter, arg, text):
+-    for completion in iter:
+-        completion = str(completion)
+-        if completion.startswith(arg):
+-            yield completion[len(arg)-len(text):]
+-
+ def iter_source_file_completions(tree, arg):
+-    treepath = cmdutil.tree_cwd(tree)
++    treepath = arch_compound.tree_cwd(tree)
+     if len(treepath) > 0:
+         dirs = [treepath]
+     else:
+@@ -1701,7 +1544,7 @@
+     :return: An iterator of all matching untagged files
+     :rtype: iterator of str
+     """
+-    treepath = cmdutil.tree_cwd(tree)
++    treepath = arch_compound.tree_cwd(tree)
+     if len(treepath) > 0:
+         dirs = [treepath]
+     else:
+@@ -1743,8 +1586,8 @@
+     :param arg: The prefix to match
+     :type arg: str
+     """
+-    treepath = cmdutil.tree_cwd(tree)
+-    tmpdir = cmdutil.tmpdir()
++    treepath = arch_compound.tree_cwd(tree)
++    tmpdir = util.tmpdir()
+     changeset = tmpdir+"/changeset"
+     completions = []
+     revision = cmdutil.determine_revision_tree(tree)
+@@ -1756,14 +1599,6 @@
+     shutil.rmtree(tmpdir)
+     return completions
+ 
+-def iter_dir_completions(arg):
+-    """Generate an iterator that iterates through directory name completions.
+-
+-    :param arg: The directory name fragment to match
+-    :type arg: str
+-    """
+-    return iter_file_completions(arg, True)
+-
+ class Shell(BaseCommand):
+     def __init__(self):
+         self.description = "Runs Fai as a shell"
+@@ -1795,7 +1630,11 @@
+         parser=self.get_parser()
+         (options, args) = parser.parse_args(cmdargs)
+ 
+-        tree = arch.tree_root()
++        try:
++            tree = arch.tree_root()
++        except arch.errors.TreeRootError, e:
++            raise pylon.errors.CommandFailedWrapper(e)
++            
+ 
+         if (len(args) == 0) == (options.untagged == False):
+             raise cmdutil.GetHelp
+@@ -1809,13 +1648,22 @@
+         if options.id_type == "tagline":
+             if method != "tagline":
+                 if not cmdutil.prompt("Tagline in other tree"):
+-                    if method == "explicit":
+-                        options.id_type == explicit
++                    if method == "explicit" or method == "implicit":
++                        options.id_type == method
+                     else:
+                         print "add-id not supported for \"%s\" tagging method"\
+                             % method 
+                         return
+         
++        elif options.id_type == "implicit":
++            if method != "implicit":
++                if not cmdutil.prompt("Implicit in other tree"):
++                    if method == "explicit" or method == "tagline":
++                        options.id_type == method
++                    else:
++                        print "add-id not supported for \"%s\" tagging method"\
++                            % method 
++                        return
+         elif options.id_type == "explicit":
+             if method != "tagline" and method != explicit:
+                 if not prompt("Explicit in other tree"):
+@@ -1824,7 +1672,8 @@
+                     return
+         
+         if options.id_type == "auto":
+-            if method != "tagline" and method != "explicit":
++            if method != "tagline" and method != "explicit" \
++                and method !="implicit":
+                 print "add-id not supported for \"%s\" tagging method" % method
+                 return
+             else:
+@@ -1852,10 +1701,12 @@
+             previous_files.extend(files)
+             if id_type == "explicit":
+                 cmdutil.add_id(files)
+-            elif id_type == "tagline":
++            elif id_type == "tagline" or id_type == "implicit":
+                 for file in files:
+                     try:
+-                        cmdutil.add_tagline_or_explicit_id(file)
++                        implicit = (id_type == "implicit")
++                        cmdutil.add_tagline_or_explicit_id(file, False,
++                                                           implicit)
+                     except cmdutil.AlreadyTagged:
+                         print "\"%s\" already has a tagline." % file
+                     except cmdutil.NoCommentSyntax:
+@@ -1888,6 +1739,9 @@
+         parser.add_option("--tagline", action="store_const", 
+                          const="tagline", dest="id_type", 
+                          help="Use a tagline id")
++        parser.add_option("--implicit", action="store_const", 
++                         const="implicit", dest="id_type", 
++                         help="Use an implicit id (deprecated)")
+         parser.add_option("--untagged", action="store_true", 
+                          dest="untagged", default=False, 
+                          help="tag all untagged files")
+@@ -1926,27 +1780,7 @@
+     def get_completer(self, arg, index):
+         if self.tree is None:
+             raise arch.errors.TreeRootError
+-        completions = list(ancillary.iter_partners(self.tree, 
+-                                                   self.tree.tree_version))
+-        if len(completions) == 0:
+-            completions = list(self.tree.iter_log_versions())
+-
+-        aliases = []
+-        try:
+-            for completion in completions:
+-                alias = ancillary.compact_alias(str(completion), self.tree)
+-                if alias:
+-                    aliases.extend(alias)
+-
+-            for completion in completions:
+-                if completion.archive == self.tree.tree_version.archive:
+-                    aliases.append(completion.nonarch)
+-
+-        except Exception, e:
+-            print e
+-            
+-        completions.extend(aliases)
+-        return completions
++        return cmdutil.merge_completions(self.tree, arg, index)
+ 
+     def do_command(self, cmdargs):
+         """
+@@ -1961,7 +1795,7 @@
+         
+         if self.tree is None:
+             raise arch.errors.TreeRootError(os.getcwd())
+-        if cmdutil.has_changed(self.tree.tree_version):
++        if cmdutil.has_changed(ancillary.comp_revision(self.tree)):
+             raise UncommittedChanges(self.tree)
+ 
+         if len(args) > 0:
+@@ -2027,14 +1861,14 @@
+         :type other_revision: `arch.Revision`
+         :return: 0 if the merge was skipped, 1 if it was applied
+         """
+-        other_tree = cmdutil.find_or_make_local_revision(other_revision)
++        other_tree = arch_compound.find_or_make_local_revision(other_revision)
+         try:
+             if action == "native-merge":
+-                ancestor = cmdutil.merge_ancestor2(self.tree, other_tree, 
+-                                                   other_revision)
++                ancestor = arch_compound.merge_ancestor2(self.tree, other_tree, 
++                                                         other_revision)
+             elif action == "update":
+-                ancestor = cmdutil.tree_latest(self.tree, 
+-                                               other_revision.version)
++                ancestor = arch_compound.tree_latest(self.tree, 
++                                                     other_revision.version)
+         except CantDetermineRevision, e:
+             raise CommandFailedWrapper(e)
+         cmdutil.colorize(arch.Chatter("* Found common ancestor %s" % ancestor))
+@@ -2104,7 +1938,10 @@
+         if self.tree is None:
+             raise arch.errors.TreeRootError
+ 
+-        edit_log(self.tree)
++        try:
++            edit_log(self.tree, self.tree.tree_version)
++        except pylon.errors.NoEditorSpecified, e:
++            raise pylon.errors.CommandFailedWrapper(e)
+ 
+     def get_parser(self):
+         """
+@@ -2132,7 +1969,7 @@
+         """
+         return
+ 
+-def edit_log(tree):
++def edit_log(tree, version):
+     """Makes and edits the log for a tree.  Does all kinds of fancy things
+     like log templates and merge summaries and log-for-merge
+     
+@@ -2141,28 +1978,29 @@
+     """
+     #ensure we have an editor before preparing the log
+     cmdutil.find_editor()
+-    log = tree.log_message(create=False)
++    log = tree.log_message(create=False, version=version)
+     log_is_new = False
+     if log is None or cmdutil.prompt("Overwrite log"):
+         if log is not None:
+            os.remove(log.name)
+-        log = tree.log_message(create=True)
++        log = tree.log_message(create=True, version=version)
+         log_is_new = True
+         tmplog = log.name
+-        template = tree+"/{arch}/=log-template"
+-        if not os.path.exists(template):
+-            template = os.path.expanduser("~/.arch-params/=log-template")
+-            if not os.path.exists(template):
+-                template = None
++        template = pylon.log_template_path(tree)
+         if template:
+             shutil.copyfile(template, tmplog)
+-        
+-        new_merges = list(cmdutil.iter_new_merges(tree, 
+-                                                  tree.tree_version))
+-        log["Summary"] = merge_summary(new_merges, tree.tree_version)
++        comp_version = ancillary.comp_revision(tree).version
++        new_merges = cmdutil.iter_new_merges(tree, comp_version)
++        new_merges = cmdutil.direct_merges(new_merges)
++        log["Summary"] = pylon.merge_summary(new_merges, 
++                                         version)
+         if len(new_merges) > 0:   
+             if cmdutil.prompt("Log for merge"):
+-                mergestuff = cmdutil.log_for_merge(tree)
++                if cmdutil.prompt("changelog for merge"):
++                    mergestuff = "Patches applied:\n"
++                    mergestuff += pylon.changelog_for_merge(new_merges)
++                else:
++                    mergestuff = cmdutil.log_for_merge(tree, comp_version)
+                 log.description += mergestuff
+         log.save()
+     try:
+@@ -2172,29 +2010,6 @@
+             os.remove(log.name)
+         raise
+ 
+-def merge_summary(new_merges, tree_version):
+-    if len(new_merges) == 0:
+-        return ""
+-    if len(new_merges) == 1:
+-        summary = new_merges[0].summary
+-    else:
+-        summary = "Merge"
+-
+-    credits = []
+-    for merge in new_merges:
+-        if arch.my_id() != merge.creator:
+-            name = re.sub("<.*>", "", merge.creator).rstrip(" ");
+-            if not name in credits:
+-                credits.append(name)
+-        else:
+-            version = merge.revision.version
+-            if version.archive == tree_version.archive:
+-                if not version.nonarch in credits:
+-                    credits.append(version.nonarch)
+-            elif not str(version) in credits:
+-                credits.append(str(version))
+-
+-    return ("%s (%s)") % (summary, ", ".join(credits))
+ 
+ class MirrorArchive(BaseCommand):
+     """
+@@ -2268,31 +2083,73 @@
+ 
+ Use "alias" to list available (user and automatic) aliases."""
+ 
++auto_alias = [
++"acur", 
++"The latest revision in the archive of the tree-version.  You can specify \
++a different version like so: acur:foo--bar--0 (aliases can be used)",
++"tcur",
++"""(tree current) The latest revision in the tree of the tree-version. \
++You can specify a different version like so: tcur:foo--bar--0 (aliases can be \
++used).""",
++"tprev" , 
++"""(tree previous) The previous revision in the tree of the tree-version.  To \
++specify an older revision, use a number, e.g. "tprev:4" """,
++"tanc" , 
++"""(tree ancestor) The ancestor revision of the tree To specify an older \
++revision, use a number, e.g. "tanc:4".""",
++"tdate" , 
++"""(tree date) The latest revision from a given date, e.g. "tdate:July 6".""",
++"tmod" , 
++""" (tree modified) The latest revision to modify a given file, e.g. \
++"tmod:engine.cpp" or "tmod:engine.cpp:16".""",
++"ttag" , 
++"""(tree tag) The revision that was tagged into the current tree revision, \
++according to the tree""",
++"tagcur", 
++"""(tag current) The latest revision of the version that the current tree \
++was tagged from.""",
++"mergeanc" , 
++"""The common ancestor of the current tree and the specified revision. \
++Defaults to the first partner-version's latest revision or to tagcur.""",
++]
++
++
++def is_auto_alias(name):
++    """Determine whether a name is an auto alias name
++
++    :param name: the name to check
++    :type name: str
++    :return: True if the name is an auto alias, false if not
++    :rtype: bool
++    """
++    return name in [f for (f, v) in pylon.util.iter_pairs(auto_alias)]
++
++
++def display_def(iter, wrap = 80):
++    """Display a list of definitions
++
++    :param iter: iter of name, definition pairs
++    :type iter: iter of (str, str)
++    :param wrap: The width for text wrapping
++    :type wrap: int
++    """
++    vals = list(iter)
++    maxlen = 0
++    for (key, value) in vals:
++        if len(key) > maxlen:
++            maxlen = len(key)
++    for (key, value) in vals:
++        tw=textwrap.TextWrapper(width=wrap, 
++                                initial_indent=key.rjust(maxlen)+" : ",
++                                subsequent_indent="".rjust(maxlen+3))
++        print tw.fill(value)
++
++
+ def help_aliases(tree):
+-    print """Auto-generated aliases
+- acur : The latest revision in the archive of the tree-version.  You can specfy
+-        a different version like so: acur:foo--bar--0 (aliases can be used)
+- tcur : (tree current) The latest revision in the tree of the tree-version.
+-        You can specify a different version like so: tcur:foo--bar--0 (aliases
+-        can be used).
+-tprev : (tree previous) The previous revision in the tree of the tree-version.
+-        To specify an older revision, use a number, e.g. "tprev:4"
+- tanc : (tree ancestor) The ancestor revision of the tree
+-        To specify an older revision, use a number, e.g. "tanc:4"
+-tdate : (tree date) The latest revision from a given date (e.g. "tdate:July 6")
+- tmod : (tree modified) The latest revision to modify a given file 
+-        (e.g. "tmod:engine.cpp" or "tmod:engine.cpp:16")
+- ttag : (tree tag) The revision that was tagged into the current tree revision,
+-        according to the tree.
+-tagcur: (tag current) The latest revision of the version that the current tree
+-        was tagged from.
+-mergeanc : The common ancestor of the current tree and the specified revision.
+-        Defaults to the first partner-version's latest revision or to tagcur.
+-   """
++    print """Auto-generated aliases"""
++    display_def(pylon.util.iter_pairs(auto_alias))
+     print "User aliases"
+-    for parts in ancillary.iter_all_alias(tree):
+-        print parts[0].rjust(10)+" : "+parts[1]
+-
++    display_def(ancillary.iter_all_alias(tree))
+ 
+ class Inventory(BaseCommand):
+     """List the status of files in the tree"""
+@@ -2428,6 +2285,11 @@
+         except cmdutil.ForbiddenAliasSyntax, e:
+             raise CommandFailedWrapper(e)
+ 
++    def no_prefix(self, alias):
++        if alias.startswith("^"):
++            alias = alias[1:]
++        return alias
++        
+     def arg_dispatch(self, args, options):
+         """Add, modify, or list aliases, depending on number of arguments
+ 
+@@ -2438,15 +2300,20 @@
+         if len(args) == 0:
+             help_aliases(self.tree)
+             return
+-        elif len(args) == 1:
+-            self.print_alias(args[0])
+-        elif (len(args)) == 2:
+-            self.add(args[0], args[1], options)
+         else:
+-            raise cmdutil.GetHelp
++            alias = self.no_prefix(args[0])
++            if len(args) == 1:
++                self.print_alias(alias)
++            elif (len(args)) == 2:
++                self.add(alias, args[1], options)
++            else:
++                raise cmdutil.GetHelp
+ 
+     def print_alias(self, alias):
+         answer = None
++        if is_auto_alias(alias):
++            raise pylon.errors.IsAutoAlias(alias, "\"%s\" is an auto alias."
++                "  Use \"revision\" to expand auto aliases." % alias)
+         for pair in ancillary.iter_all_alias(self.tree):
+             if pair[0] == alias:
+                 answer = pair[1]
+@@ -2464,6 +2331,8 @@
+         :type expansion: str
+         :param options: The commandline options
+         """
++        if is_auto_alias(alias):
++            raise IsAutoAlias(alias)
+         newlist = ""
+         written = False
+         new_line = "%s=%s\n" % (alias, cmdutil.expand_alias(expansion, 
+@@ -2490,14 +2359,17 @@
+         deleted = False
+         if len(args) != 1:
+             raise cmdutil.GetHelp
++        alias = self.no_prefix(args[0])
++        if is_auto_alias(alias):
++            raise IsAutoAlias(alias)
+         newlist = ""
+         for pair in self.get_iterator(options):
+-            if pair[0] != args[0]:
++            if pair[0] != alias:
+                 newlist+="%s=%s\n" % (pair[0], pair[1])
+             else:
+                 deleted = True
+         if not deleted:
+-            raise errors.NoSuchAlias(args[0])
++            raise errors.NoSuchAlias(alias)
+         self.write_aliases(newlist, options)
+ 
+     def get_alias_file(self, options):
+@@ -2526,7 +2398,7 @@
+         :param options: The commandline options
+         """
+         filename = os.path.expanduser(self.get_alias_file(options))
+-        file = cmdutil.NewFileVersion(filename)
++        file = util.NewFileVersion(filename)
+         file.write(newlist)
+         file.commit()
+ 
+@@ -2588,10 +2460,13 @@
+         :param cmdargs: The commandline arguments
+         :type cmdargs: list of str
+         """
+-        cmdutil.find_editor()
+         parser = self.get_parser()
+         (options, args) = parser.parse_args(cmdargs)
+         try:
++            cmdutil.find_editor()
++        except pylon.errors.NoEditorSpecified, e:
++            raise pylon.errors.CommandFailedWrapper(e)
++        try:
+             self.tree=arch.tree_root()
+         except:
+             self.tree=None
+@@ -2655,7 +2530,7 @@
+             target_revision = cmdutil.determine_revision_arch(self.tree, 
+                                                               args[0])
+         else:
+-            target_revision = cmdutil.tree_latest(self.tree)
++            target_revision = arch_compound.tree_latest(self.tree)
+         if len(args) > 1:
+             merges = [ arch.Patchlog(cmdutil.determine_revision_arch(
+                        self.tree, f)) for f in args[1:] ]
+@@ -2711,7 +2586,7 @@
+ 
+         :param message: The message to send
+         :type message: `email.Message`"""
+-        server = smtplib.SMTP()
++        server = smtplib.SMTP("localhost")
+         server.sendmail(message['From'], message['To'], message.as_string())
+         server.quit()
+ 
+@@ -2763,6 +2638,22 @@
+ 'alias' : Alias,
+ 'request-merge': RequestMerge,
+ }
++
++def my_import(mod_name):
++    module = __import__(mod_name)
++    components = mod_name.split('.')
++    for comp in components[1:]:
++        module = getattr(module, comp)
++    return module
++
++def plugin(mod_name):
++    module = my_import(mod_name)
++    module.add_command(commands)
++
++for file in os.listdir(sys.path[0]+"/command"):
++    if len(file) > 3 and file[-3:] == ".py" and file != "__init__.py":
++        plugin("command."+file[:-3])
++
+ suggestions = {
+ 'apply-delta' : "Try \"apply-changes\".",
+ 'delta' : "To compare two revisions, use \"changes\".",
+@@ -2784,6 +2675,7 @@
+ 'tagline' : "Use add-id.  It uses taglines in tagline trees",
+ 'emlog' : "Use elog.  It automatically adds log-for-merge text, if any",
+ 'library-revisions' : "Use revisions --library",
+-'file-revert' : "Use revert FILE"
++'file-revert' : "Use revert FILE",
++'join-branch' : "Use replay --logs-only"
+ }
+ # arch-tag: 19d5739d-3708-486c-93ba-deecc3027fc7

*** added file 'testdata/insert_top.patch'
--- /dev/null 
+++ testdata/insert_top.patch 
@@ -0,0 +1,7 @@
+--- orig/pylon/patches.py
++++ mod/pylon/patches.py
+@@ -1,3 +1,4 @@
++#test
+ import util
+ import sys
+ class PatchSyntax(Exception):

*** added file 'testdata/mod'
--- /dev/null 
+++ testdata/mod 
@@ -0,0 +1,2681 @@
+# Copyright (C) 2004 Aaron Bentley
+# <aaron.bentley@utoronto.ca>
+#
+#    This program is free software; you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License as published by
+#    the Free Software Foundation; either version 2 of the License, or
+#    (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    You should have received a copy of the GNU General Public License
+#    along with this program; if not, write to the Free Software
+#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+import sys
+import arch
+import arch.util
+import arch.arch
+
+import pylon.errors
+from pylon.errors import *
+from pylon import errors
+from pylon import util
+from pylon import arch_core
+from pylon import arch_compound
+from pylon import ancillary
+from pylon import misc
+from pylon import paths 
+
+import abacmds
+import cmdutil
+import shutil
+import os
+import options
+import time
+import cmd
+import readline
+import re
+import string
+import terminal
+import email
+import smtplib
+import textwrap
+
+__docformat__ = "restructuredtext"
+__doc__ = "Implementation of user (sub) commands"
+commands = {}
+
+def find_command(cmd):
+    """
+    Return an instance of a command type.  Return None if the type isn't
+    registered.
+
+    :param cmd: the name of the command to look for
+    :type cmd: the type of the command
+    """
+    if commands.has_key(cmd):
+        return commands[cmd]()
+    else:
+        return None
+
+class BaseCommand:
+    def __call__(self, cmdline):
+        try:
+            self.do_command(cmdline.split())
+        except cmdutil.GetHelp, e:
+            self.help()
+        except Exception, e:
+            print e
+
+    def get_completer(index):
+        return None
+
+    def complete(self, args, text):
+        """
+        Returns a list of possible completions for the given text.
+
+        :param args: The complete list of arguments
+        :type args: List of str
+        :param text: text to complete (may be shorter than args[-1])
+        :type text: str
+        :rtype: list of str
+        """
+        matches = []
+        candidates = None
+
+        if len(args) > 0: 
+            realtext = args[-1]
+        else:
+            realtext = ""
+
+        try:
+            parser=self.get_parser()
+            if realtext.startswith('-'):
+                candidates = parser.iter_options()
+            else:
+                (options, parsed_args) = parser.parse_args(args)
+
+                if len (parsed_args) > 0:
+                    candidates = self.get_completer(parsed_args[-1], len(parsed_args) -1)
+                else:
+                    candidates = self.get_completer("", 0)
+        except:
+            pass
+        if candidates is None:
+            return
+        for candidate in candidates:
+            candidate = str(candidate)
+            if candidate.startswith(realtext):
+                matches.append(candidate[len(realtext)- len(text):])
+        return matches
+
+
+class Help(BaseCommand):
+    """
+    Lists commands, prints help messages.
+    """
+    def __init__(self):
+        self.description="Prints help mesages"
+        self.parser = None
+
+    def do_command(self, cmdargs):
+        """
+        Prints a help message.
+        """
+        options, args = self.get_parser().parse_args(cmdargs)
+        if len(args) > 1:
+            raise cmdutil.GetHelp
+
+        if options.native or options.suggestions or options.external:
+            native = options.native
+            suggestions = options.suggestions
+            external = options.external
+        else:
+            native = True
+            suggestions = False
+            external = True
+        
+        if len(args) == 0:
+            self.list_commands(native, suggestions, external)
+            return
+        elif len(args) == 1:
+            command_help(args[0])
+            return
+
+    def help(self):
+        self.get_parser().print_help()
+        print """
+If no command is specified, commands are listed.  If a command is
+specified, help for that command is listed.
+        """
+
+    def get_parser(self):
+        """
+        Returns the options parser to use for the "revision" command.
+
+        :rtype: cmdutil.CmdOptionParser
+        """
+        if self.parser is not None:
+            return self.parser
+        parser=cmdutil.CmdOptionParser("fai help [command]")
+        parser.add_option("-n", "--native", action="store_true", 
+                         dest="native", help="Show native commands")
+        parser.add_option("-e", "--external", action="store_true", 
+                         dest="external", help="Show external commands")
+        parser.add_option("-s", "--suggest", action="store_true", 
+                         dest="suggestions", help="Show suggestions")
+        self.parser = parser
+        return parser 
+      
+    def list_commands(self, native=True, suggest=False, external=True):
+        """
+        Lists supported commands.
+
+        :param native: list native, python-based commands
+        :type native: bool
+        :param external: list external aba-style commands
+        :type external: bool
+        """
+        if native:
+            print "Native Fai commands"
+            keys=commands.keys()
+            keys.sort()
+            for k in keys:
+                space=""
+                for i in range(28-len(k)):
+                    space+=" "
+                print space+k+" : "+commands[k]().description
+            print
+        if suggest:
+            print "Unavailable commands and suggested alternatives"
+            key_list = suggestions.keys()
+            key_list.sort()
+            for key in key_list:
+                print "%28s : %s" % (key, suggestions[key])
+            print
+        if external:
+            fake_aba = abacmds.AbaCmds()
+            if (fake_aba.abadir == ""):
+                return
+            print "External commands"
+            fake_aba.list_commands()
+            print
+        if not suggest:
+            print "Use help --suggest to list alternatives to tla and aba"\
+                " commands."
+        if options.tla_fallthrough and (native or external):
+            print "Fai also supports tla commands."
+
+def command_help(cmd):
+    """
+    Prints help for a command.
+
+    :param cmd: The name of the command to print help for
+    :type cmd: str
+    """
+    fake_aba = abacmds.AbaCmds()
+    cmdobj = find_command(cmd)
+    if cmdobj != None:
+        cmdobj.help()
+    elif suggestions.has_key(cmd):
+        print "Not available\n" + suggestions[cmd]
+    else:
+        abacmd = fake_aba.is_command(cmd)
+        if abacmd:
+            abacmd.help()
+        else:
+            print "No help is available for \""+cmd+"\". Maybe try \"tla "+cmd+" -H\"?"
+
+
+
+class Changes(BaseCommand):
+    """
+    the "changes" command: lists differences between trees/revisions:
+    """
+    
+    def __init__(self):
+        self.description="Lists what files have changed in the project tree"
+
+    def get_completer(self, arg, index):
+        if index > 1:
+            return None
+        try:
+            tree = arch.tree_root()
+        except:
+            tree = None
+        return cmdutil.iter_revision_completions(arg, tree)
+    
+    def parse_commandline(self, cmdline):
+        """
+        Parse commandline arguments.  Raises cmdutil.GetHelp if help is needed.
+        
+        :param cmdline: A list of arguments to parse
+        :rtype: (options, Revision, Revision/WorkingTree)
+        """
+        parser=self.get_parser()
+        (options, args) = parser.parse_args(cmdline)
+        if len(args) > 2:
+            raise cmdutil.GetHelp
+
+        tree=arch.tree_root()
+        if len(args) == 0:
+            a_spec = ancillary.comp_revision(tree)
+        else:
+            a_spec = cmdutil.determine_revision_tree(tree, args[0])
+        cmdutil.ensure_archive_registered(a_spec.archive)
+        if len(args) == 2:
+            b_spec = cmdutil.determine_revision_tree(tree, args[1])
+            cmdutil.ensure_archive_registered(b_spec.archive)
+        else:
+            b_spec=tree
+        return options, a_spec, b_spec
+
+    def do_command(self, cmdargs):
+        """
+        Master function that perfoms the "changes" command.
+        """
+        try:
+            options, a_spec, b_spec = self.parse_commandline(cmdargs);
+        except cmdutil.CantDetermineRevision, e:
+            print e
+            return
+        except arch.errors.TreeRootError, e:
+            print e
+            return
+        if options.changeset:
+            changeset=options.changeset
+            tmpdir = None
+        else:
+            tmpdir=util.tmpdir()
+            changeset=tmpdir+"/changeset"
+        try:
+            delta=arch.iter_delta(a_spec, b_spec, changeset)
+            try:
+                for line in delta:
+                    if cmdutil.chattermatch(line, "changeset:"):
+                        pass
+                    else:
+                        cmdutil.colorize(line, options.suppress_chatter)
+            except arch.util.ExecProblem, e:
+                if e.proc.error and e.proc.error.startswith(
+                    "missing explicit id for file"):
+                    raise MissingID(e)
+                else:
+                    raise
+            status=delta.status
+            if status > 1:
+                return
+            if (options.perform_diff):
+                chan = arch_compound.ChangesetMunger(changeset)
+                chan.read_indices()
+                if options.diffopts is not None:
+                    if isinstance(b_spec, arch.Revision):
+                        b_dir = b_spec.library_find()
+                    else:
+                        b_dir = b_spec
+                    a_dir = a_spec.library_find()
+                    diffopts = options.diffopts.split()
+                    cmdutil.show_custom_diffs(chan, diffopts, a_dir, b_dir)
+                else:
+                    cmdutil.show_diffs(delta.changeset)
+        finally:
+            if tmpdir and (os.access(tmpdir, os.X_OK)):
+                shutil.rmtree(tmpdir)
+
+    def get_parser(self):
+        """
+        Returns the options parser to use for the "changes" command.
+
+        :rtype: cmdutil.CmdOptionParser
+        """
+        parser=cmdutil.CmdOptionParser("fai changes [options] [revision]"
+                                       " [revision]")
+        parser.add_option("-d", "--diff", action="store_true", 
+                          dest="perform_diff", default=False, 
+                          help="Show diffs in summary")
+        parser.add_option("-c", "--changeset", dest="changeset", 
+                          help="Store a changeset in the given directory", 
+                          metavar="DIRECTORY")
+        parser.add_option("-s", "--silent", action="store_true", 
+                          dest="suppress_chatter", default=False, 
+                          help="Suppress chatter messages")
+        parser.add_option("--diffopts", dest="diffopts", 
+                          help="Use the specified diff options", 
+                          metavar="OPTIONS")
+
+        return parser
+
+    def help(self, parser=None):
+        """
+        Prints a help message.
+
+        :param parser: If supplied, the parser to use for generating help.  If \
+        not supplied, it is retrieved.
+        :type parser: cmdutil.CmdOptionParser
+        """
+        if parser is None:
+            parser=self.get_parser()
+        parser.print_help()
+        print """
+Performs source-tree comparisons
+
+If no revision is specified, the current project tree is compared to the
+last-committed revision.  If one revision is specified, the current project
+tree is compared to that revision.  If two revisions are specified, they are
+compared to each other.
+        """
+        help_tree_spec() 
+        return
+
+
+class ApplyChanges(BaseCommand):
+    """
+    Apply differences between two revisions to a tree
+    """
+    
+    def __init__(self):
+        self.description="Applies changes to a project tree"
+    
+    def get_completer(self, arg, index):
+        if index > 1:
+            return None
+        try:
+            tree = arch.tree_root()
+        except:
+            tree = None
+        return cmdutil.iter_revision_completions(arg, tree)
+
+    def parse_commandline(self, cmdline, tree):
+        """
+        Parse commandline arguments.  Raises cmdutil.GetHelp if help is needed.
+        
+        :param cmdline: A list of arguments to parse
+        :rtype: (options, Revision, Revision/WorkingTree)
+        """
+        parser=self.get_parser()
+        (options, args) = parser.parse_args(cmdline)
+        if len(args) != 2:
+            raise cmdutil.GetHelp
+
+        a_spec = cmdutil.determine_revision_tree(tree, args[0])
+        cmdutil.ensure_archive_registered(a_spec.archive)
+        b_spec = cmdutil.determine_revision_tree(tree, args[1])
+        cmdutil.ensure_archive_registered(b_spec.archive)
+        return options, a_spec, b_spec
+
+    def do_command(self, cmdargs):
+        """
+        Master function that performs "apply-changes".
+        """
+        try:
+            tree = arch.tree_root()
+            options, a_spec, b_spec = self.parse_commandline(cmdargs, tree);
+        except cmdutil.CantDetermineRevision, e:
+            print e
+            return
+        except arch.errors.TreeRootError, e:
+            print e
+            return
+        delta=cmdutil.apply_delta(a_spec, b_spec, tree)
+        for line in cmdutil.iter_apply_delta_filter(delta):
+            cmdutil.colorize(line, options.suppress_chatter)
+
+    def get_parser(self):
+        """
+        Returns the options parser to use for the "apply-changes" command.
+
+        :rtype: cmdutil.CmdOptionParser
+        """
+        parser=cmdutil.CmdOptionParser("fai apply-changes [options] revision"
+                                       " revision")
+        parser.add_option("-d", "--diff", action="store_true", 
+                          dest="perform_diff", default=False, 
+                          help="Show diffs in summary")
+        parser.add_option("-c", "--changeset", dest="changeset", 
+                          help="Store a changeset in the given directory", 
+                          metavar="DIRECTORY")
+        parser.add_option("-s", "--silent", action="store_true", 
+                          dest="suppress_chatter", default=False, 
+                          help="Suppress chatter messages")
+        return parser
+
+    def help(self, parser=None):
+        """
+        Prints a help message.
+
+        :param parser: If supplied, the parser to use for generating help.  If \
+        not supplied, it is retrieved.
+        :type parser: cmdutil.CmdOptionParser
+        """
+        if parser is None:
+            parser=self.get_parser()
+        parser.print_help()
+        print """
+Applies changes to a project tree
+
+Compares two revisions and applies the difference between them to the current
+tree.
+        """
+        help_tree_spec() 
+        return
+
+class Update(BaseCommand):
+    """
+    Updates a project tree to a given revision, preserving un-committed hanges. 
+    """
+    
+    def __init__(self):
+        self.description="Apply the latest changes to the current directory"
+
+    def get_completer(self, arg, index):
+        if index > 0:
+            return None
+        try:
+            tree = arch.tree_root()
+        except:
+            tree = None
+        return cmdutil.iter_revision_completions(arg, tree)
+    
+    def parse_commandline(self, cmdline, tree):
+        """
+        Parse commandline arguments.  Raises cmdutil.GetHelp if help is needed.
+        
+        :param cmdline: A list of arguments to parse
+        :rtype: (options, Revision, Revision/WorkingTree)
+        """
+        parser=self.get_parser()
+        (options, args) = parser.parse_args(cmdline)
+        if len(args) > 2:
+            raise cmdutil.GetHelp
+
+        spec=None
+        if len(args)>0:
+            spec=args[0]
+        revision=cmdutil.determine_revision_arch(tree, spec)
+        cmdutil.ensure_archive_registered(revision.archive)
+
+        mirror_source = cmdutil.get_mirror_source(revision.archive)
+        if mirror_source != None:
+            if cmdutil.prompt("Mirror update"):
+                cmd=cmdutil.mirror_archive(mirror_source, 
+                    revision.archive, arch.NameParser(revision).get_package_version())
+                for line in arch.chatter_classifier(cmd):
+                    cmdutil.colorize(line, options.suppress_chatter)
+
+                revision=cmdutil.determine_revision_arch(tree, spec)
+
+        return options, revision 
+
+    def do_command(self, cmdargs):
+        """
+        Master function that perfoms the "update" command.
+        """
+        tree=arch.tree_root()
+        try:
+            options, to_revision = self.parse_commandline(cmdargs, tree);
+        except cmdutil.CantDetermineRevision, e:
+            print e
+            return
+        except arch.errors.TreeRootError, e:
+            print e
+            return
+        from_revision = arch_compound.tree_latest(tree)
+        if from_revision==to_revision:
+            print "Tree is already up to date with:\n"+str(to_revision)+"."
+            return
+        cmdutil.ensure_archive_registered(from_revision.archive)
+        cmd=cmdutil.apply_delta(from_revision, to_revision, tree,
+            options.patch_forward)
+        for line in cmdutil.iter_apply_delta_filter(cmd):
+            cmdutil.colorize(line)
+        if to_revision.version != tree.tree_version:
+            if cmdutil.prompt("Update version"):
+                tree.tree_version = to_revision.version
+
+    def get_parser(self):
+        """
+        Returns the options parser to use for the "update" command.
+
+        :rtype: cmdutil.CmdOptionParser
+        """
+        parser=cmdutil.CmdOptionParser("fai update [options]"
+                                       " [revision/version]")
+        parser.add_option("-f", "--forward", action="store_true", 
+                          dest="patch_forward", default=False, 
+                          help="pass the --forward option to 'patch'")
+        parser.add_option("-s", "--silent", action="store_true", 
+                          dest="suppress_chatter", default=False, 
+                          help="Suppress chatter messages")
+        return parser
+
+    def help(self, parser=None):
+        """
+        Prints a help message.
+
+        :param parser: If supplied, the parser to use for generating help.  If \
+        not supplied, it is retrieved.
+        :type parser: cmdutil.CmdOptionParser
+        """
+        if parser is None:
+            parser=self.get_parser()
+        parser.print_help()
+        print """
+Updates a working tree to the current archive revision
+
+If a revision or version is specified, that is used instead 
+        """
+        help_tree_spec() 
+        return
+
+
+class Commit(BaseCommand):
+    """
+    Create a revision based on the changes in the current tree.
+    """
+    
+    def __init__(self):
+        self.description="Write local changes to the archive"
+
+    def get_completer(self, arg, index):
+        if arg is None:
+            arg = ""
+        return iter_modified_file_completions(arch.tree_root(), arg)
+#        return iter_source_file_completions(arch.tree_root(), arg)
+    
+    def parse_commandline(self, cmdline, tree):
+        """
+        Parse commandline arguments.  Raise cmtutil.GetHelp if help is needed.
+        
+        :param cmdline: A list of arguments to parse
+        :rtype: (options, Revision, Revision/WorkingTree)
+        """
+        parser=self.get_parser()
+        (options, args) = parser.parse_args(cmdline)
+
+        if len(args) == 0:
+            args = None
+        if options.version is None:
+            return options, tree.tree_version, args
+
+        revision=cmdutil.determine_revision_arch(tree, options.version)
+        return options, revision.get_version(), args
+
+    def do_command(self, cmdargs):
+        """
+        Master function that perfoms the "commit" command.
+        """
+        tree=arch.tree_root()
+        options, version, files = self.parse_commandline(cmdargs, tree)
+        ancestor = None
+        if options.__dict__.has_key("base") and options.base:
+            base = cmdutil.determine_revision_tree(tree, options.base)
+            ancestor = base
+        else:
+            base = ancillary.submit_revision(tree)
+            ancestor = base
+        if ancestor is None:
+            ancestor = arch_compound.tree_latest(tree, version)
+
+        writeversion=version
+        archive=version.archive
+        source=cmdutil.get_mirror_source(archive)
+        allow_old=False
+        writethrough="implicit"
+
+        if source!=None:
+            if writethrough=="explicit" and \
+                cmdutil.prompt("Writethrough"):
+                writeversion=arch.Version(str(source)+"/"+str(version.get_nonarch()))
+            elif writethrough=="none":
+                raise CommitToMirror(archive)
+
+        elif archive.is_mirror:
+            raise CommitToMirror(archive)
+
+        try:
+            last_revision=tree.iter_logs(version, True).next().revision
+        except StopIteration, e:
+            last_revision = None
+            if ancestor is None:
+                if cmdutil.prompt("Import from commit"):
+                    return do_import(version)
+                else:
+                    raise NoVersionLogs(version)
+        try:
+            arch_last_revision = version.iter_revisions(True).next()
+        except StopIteration, e:
+            arch_last_revision = None
+ 
+        if last_revision != arch_last_revision:
+            print "Tree is not up to date with %s" % str(version)
+            if not cmdutil.prompt("Out of date"):
+                raise OutOfDate
+            else:
+                allow_old=True
+
+        try:
+            if not cmdutil.has_changed(ancestor):
+                if not cmdutil.prompt("Empty commit"):
+                    raise EmptyCommit
+        except arch.util.ExecProblem, e:
+            if e.proc.error and e.proc.error.startswith(
+                "missing explicit id for file"):
+                raise MissingID(e)
+            else:
+                raise
+        log = tree.log_message(create=False, version=version)
+        if log is None:
+            try:
+                if cmdutil.prompt("Create log"):
+                    edit_log(tree, version)
+
+            except cmdutil.NoEditorSpecified, e:
+                raise CommandFailed(e)
+            log = tree.log_message(create=False, version=version)
+        if log is None: 
+            raise NoLogMessage
+        if log["Summary"] is None or len(log["Summary"].strip()) == 0:
+            if not cmdutil.prompt("Omit log summary"):
+                raise errors.NoLogSummary
+        try:
+            for line in tree.iter_commit(version, seal=options.seal_version,
+                base=base, out_of_date_ok=allow_old, file_list=files):
+                cmdutil.colorize(line, options.suppress_chatter)
+
+        except arch.util.ExecProblem, e:
+            if e.proc.error and e.proc.error.startswith(
+                "These files violate naming conventions:"):
+                raise LintFailure(e.proc.error)
+            else:
+                raise
+
+    def get_parser(self):
+        """
+        Returns the options parser to use for the "commit" command.
+
+        :rtype: cmdutil.CmdOptionParser
+        """
+
+        parser=cmdutil.CmdOptionParser("fai commit [options] [file1]"
+                                       " [file2...]")
+        parser.add_option("--seal", action="store_true", 
+                          dest="seal_version", default=False, 
+                          help="seal this version")
+        parser.add_option("-v", "--version", dest="version", 
+                          help="Use the specified version", 
+                          metavar="VERSION")
+        parser.add_option("-s", "--silent", action="store_true", 
+                          dest="suppress_chatter", default=False, 
+                          help="Suppress chatter messages")
+        if cmdutil.supports_switch("commit", "--base"):
+            parser.add_option("--base", dest="base", help="", 
+                              metavar="REVISION")
+        return parser
+
+    def help(self, parser=None):
+        """
+        Prints a help message.
+
+        :param parser: If supplied, the parser to use for generating help.  If \
+        not supplied, it is retrieved.
+        :type parser: cmdutil.CmdOptionParser
+        """
+        if parser is None:
+            parser=self.get_parser()
+        parser.print_help()
+        print """
+Updates a working tree to the current archive revision
+
+If a version is specified, that is used instead 
+        """
+#        help_tree_spec() 
+        return
+
+
+
+class CatLog(BaseCommand):
+    """
+    Print the log of a given file (from current tree)
+    """
+    def __init__(self):
+        self.description="Prints the patch log for a revision"
+
+    def get_completer(self, arg, index):
+        if index > 0:
+            return None
+        try:
+            tree = arch.tree_root()
+        except:
+            tree = None
+        return cmdutil.iter_revision_completions(arg, tree)
+
+    def do_command(self, cmdargs):
+        """
+        Master function that perfoms the "cat-log" command.
+        """
+        parser=self.get_parser()
+        (options, args) = parser.parse_args(cmdargs)
+        try:
+            tree = arch.tree_root()
+        except arch.errors.TreeRootError, e:
+            tree = None
+        spec=None
+        if len(args) > 0:
+            spec=args[0]
+        if len(args) > 1:
+            raise cmdutil.GetHelp()
+        try:
+            if tree:
+                revision = cmdutil.determine_revision_tree(tree, spec)
+            else:
+                revision = cmdutil.determine_revision_arch(tree, spec)
+        except cmdutil.CantDetermineRevision, e:
+            raise CommandFailedWrapper(e)
+        log = None
+        
+        use_tree = (options.source == "tree" or \
+            (options.source == "any" and tree))
+        use_arch = (options.source == "archive" or options.source == "any")
+        
+        log = None
+        if use_tree:
+            for log in tree.iter_logs(revision.get_version()):
+                if log.revision == revision:
+                    break
+                else:
+                    log = None
+        if log is None and use_arch:
+            cmdutil.ensure_revision_exists(revision)
+            log = arch.Patchlog(revision)
+        if log is not None:
+            for item in log.items():
+                print "%s: %s" % item
+            print log.description
+
+    def get_parser(self):
+        """
+        Returns the options parser to use for the "cat-log" command.
+
+        :rtype: cmdutil.CmdOptionParser
+        """
+        parser=cmdutil.CmdOptionParser("fai cat-log [revision]")
+        parser.add_option("--archive", action="store_const", dest="source",
+                          const="archive", default="any",
+                          help="Always get the log from the archive")
+        parser.add_option("--tree", action="store_const", dest="source",
+                          const="tree", help="Always get the log from the tree")
+        return parser 
+
+    def help(self, parser=None):
+        """
+        Prints a help message.
+
+        :param parser: If supplied, the parser to use for generating help.  If \
+        not supplied, it is retrieved.
+        :type parser: cmdutil.CmdOptionParser
+        """
+        if parser==None:
+            parser=self.get_parser()
+        parser.print_help()
+        print """
+Prints the log for the specified revision
+        """
+        help_tree_spec()
+        return
+
+class Revert(BaseCommand):
+    """ Reverts a tree (or aspects of it) to a revision
+    """
+    def __init__(self):
+        self.description="Reverts a tree (or aspects of it) to a revision "
+
+    def get_completer(self, arg, index):
+        if index > 0:
+            return None
+        try:
+            tree = arch.tree_root()
+        except:
+            tree = None
+        return iter_modified_file_completions(tree, arg)
+
+    def do_command(self, cmdargs):
+        """
+        Master function that perfoms the "revert" command.
+        """
+        parser=self.get_parser()
+        (options, args) = parser.parse_args(cmdargs)
+        try:
+            tree = arch.tree_root()
+        except arch.errors.TreeRootError, e:
+            raise CommandFailed(e)
+        spec=None
+        if options.revision is not None:
+            spec=options.revision
+        try:
+            if spec is not None:
+                revision = cmdutil.determine_revision_tree(tree, spec)
+            else:
+                revision = ancillary.comp_revision(tree)
+        except cmdutil.CantDetermineRevision, e:
+            raise CommandFailedWrapper(e)
+        munger = None
+
+        if options.file_contents or options.file_perms or options.deletions\
+            or options.additions or options.renames or options.hunk_prompt:
+            munger = arch_compound.MungeOpts()
+            munger.set_hunk_prompt(cmdutil.colorize, cmdutil.user_hunk_confirm,
+                                   options.hunk_prompt)
+
+        if len(args) > 0 or options.logs or options.pattern_files or \
+            options.control:
+            if munger is None:
+                munger = cmdutil.arch_compound.MungeOpts(True)
+                munger.all_types(True)
+        if len(args) > 0:
+            t_cwd = arch_compound.tree_cwd(tree)
+            for name in args:
+                if len(t_cwd) > 0:
+                    t_cwd += "/"
+                name = "./" + t_cwd + name
+                munger.add_keep_file(name);
+
+        if options.file_perms:
+            munger.file_perms = True
+        if options.file_contents:
+            munger.file_contents = True
+        if options.deletions:
+            munger.deletions = True
+        if options.additions:
+            munger.additions = True
+        if options.renames:
+            munger.renames = True
+        if options.logs:
+            munger.add_keep_pattern('^\./\{arch\}/[^=].*')
+        if options.control:
+            munger.add_keep_pattern("/\.arch-ids|^\./\{arch\}|"\
+                                    "/\.arch-inventory$")
+        if options.pattern_files:
+            munger.add_keep_pattern(options.pattern_files)
+                
+        for line in arch_compound.revert(tree, revision, munger, 
+                                   not options.no_output):
+            cmdutil.colorize(line)
+
+
+    def get_parser(self):
+        """
+        Returns the options parser to use for the "cat-log" command.
+
+        :rtype: cmdutil.CmdOptionParser
+        """
+        parser=cmdutil.CmdOptionParser("fai revert [options] [FILE...]")
+        parser.add_option("", "--contents", action="store_true", 
+                          dest="file_contents", 
+                          help="Revert file content changes")
+        parser.add_option("", "--permissions", action="store_true", 
+                          dest="file_perms", 
+                          help="Revert file permissions changes")
+        parser.add_option("", "--deletions", action="store_true", 
+                          dest="deletions", 
+                          help="Restore deleted files")
+        parser.add_option("", "--additions", action="store_true", 
+                          dest="additions", 
+                          help="Remove added files")
+        parser.add_option("", "--renames", action="store_true", 
+                          dest="renames", 
+                          help="Revert file names")
+        parser.add_option("--hunks", action="store_true", 
+                          dest="hunk_prompt", default=False,
+                          help="Prompt which hunks to revert")
+        parser.add_option("--pattern-files", dest="pattern_files", 
+                          help="Revert files that match this pattern", 
+                          metavar="REGEX")
+        parser.add_option("--logs", action="store_true", 
+                          dest="logs", default=False,
+                          help="Revert only logs")
+        parser.add_option("--control-files", action="store_true", 
+                          dest="control", default=False,
+                          help="Revert logs and other control files")
+        parser.add_option("-n", "--no-output", action="store_true", 
+                          dest="no_output", 
+                          help="Don't keep an undo changeset")
+        parser.add_option("--revision", dest="revision", 
+                          help="Revert to the specified revision", 
+                          metavar="REVISION")
+        return parser 
+
+    def help(self, parser=None):
+        """
+        Prints a help message.
+
+        :param parser: If supplied, the parser to use for generating help.  If \
+        not supplied, it is retrieved.
+        :type parser: cmdutil.CmdOptionParser
+        """
+        if parser==None:
+            parser=self.get_parser()
+        parser.print_help()
+        print """
+Reverts changes in the current working tree.  If no flags are specified, all
+types of changes are reverted.  Otherwise, only selected types of changes are
+reverted.  
+
+If a revision is specified on the commandline, differences between the current
+tree and that revision are reverted.  If a version is specified, the current
+tree is used to determine the revision.
+
+If files are specified, only those files listed will have any changes applied.
+To specify a renamed file, you can use either the old or new name. (or both!)
+
+Unless "-n" is specified, reversions can be undone with "redo".
+        """
+        return
+
+class Revision(BaseCommand):
+    """
+    Print a revision name based on a revision specifier
+    """
+    def __init__(self):
+        self.description="Prints the name of a revision"
+
+    def get_completer(self, arg, index):
+        if index > 0:
+            return None
+        try:
+            tree = arch.tree_root()
+        except:
+            tree = None
+        return cmdutil.iter_revision_completions(arg, tree)
+
+    def do_command(self, cmdargs):
+        """
+        Master function that perfoms the "revision" command.
+        """
+        parser=self.get_parser()
+        (options, args) = parser.parse_args(cmdargs)
+
+        try:
+            tree = arch.tree_root()
+        except arch.errors.TreeRootError:
+            tree = None
+
+        spec=None
+        if len(args) > 0:
+            spec=args[0]
+        if len(args) > 1:
+            raise cmdutil.GetHelp
+        try:
+            if tree:
+                revision = cmdutil.determine_revision_tree(tree, spec)
+            else:
+                revision = cmdutil.determine_revision_arch(tree, spec)
+        except cmdutil.CantDetermineRevision, e:
+            print str(e)
+            return
+        print options.display(revision)
+
+    def get_parser(self):
+        """
+        Returns the options parser to use for the "revision" command.
+
+        :rtype: cmdutil.CmdOptionParser
+        """
+        parser=cmdutil.CmdOptionParser("fai revision [revision]")
+        parser.add_option("", "--location", action="store_const", 
+                         const=paths.determine_path, dest="display", 
+                         help="Show location instead of name", default=str)
+        parser.add_option("--import", action="store_const", 
+                         const=paths.determine_import_path, dest="display",  
+                         help="Show location of import file")
+        parser.add_option("--log", action="store_const", 
+                         const=paths.determine_log_path, dest="display", 
+                         help="Show location of log file")
+        parser.add_option("--patch", action="store_const", 
+                         dest="display", const=paths.determine_patch_path,
+                         help="Show location of patchfile")
+        parser.add_option("--continuation", action="store_const", 
+                         const=paths.determine_continuation_path, 
+                         dest="display",
+                         help="Show location of continuation file")
+        parser.add_option("--cacherev", action="store_const", 
+                         const=paths.determine_cacherev_path, dest="display",
+                         help="Show location of cacherev file")
+        return parser 
+
+    def help(self, parser=None):
+        """
+        Prints a help message.
+
+        :param parser: If supplied, the parser to use for generating help.  If \
+        not supplied, it is retrieved.
+        :type parser: cmdutil.CmdOptionParser
+        """
+        if parser==None:
+            parser=self.get_parser()
+        parser.print_help()
+        print """
+Expands aliases and prints the name of the specified revision.  Instead of
+the name, several options can be used to print locations.  If more than one is
+specified, the last one is used.
+        """
+        help_tree_spec()
+        return
+
+class Revisions(BaseCommand):
+    """
+    Print a revision name based on a revision specifier
+    """
+    def __init__(self):
+        self.description="Lists revisions"
+        self.cl_revisions = []
+    
+    def do_command(self, cmdargs):
+        """
+        Master function that perfoms the "revision" command.
+        """
+        (options, args) = self.get_parser().parse_args(cmdargs)
+        if len(args) > 1:
+            raise cmdutil.GetHelp
+        try:
+            self.tree = arch.tree_root()
+        except arch.errors.TreeRootError:
+            self.tree = None
+        if options.type == "default":
+            options.type = "archive"
+        try:
+            iter = cmdutil.revision_iterator(self.tree, options.type, args, 
+                                             options.reverse, options.modified,
+                                             options.shallow)
+        except cmdutil.CantDetermineRevision, e:
+            raise CommandFailedWrapper(e)
+        except cmdutil.CantDetermineVersion, e:
+            raise CommandFailedWrapper(e)
+        if options.skip is not None:
+            iter = cmdutil.iter_skip(iter, int(options.skip))
+
+        try:
+            for revision in iter:
+                log = None
+                if isinstance(revision, arch.Patchlog):
+                    log = revision
+                    revision=revision.revision
+                out = options.display(revision)
+                if out is not None:
+                    print out
+                if log is None and (options.summary or options.creator or 
+                                    options.date or options.merges):
+                    log = revision.patchlog
+                if options.creator:
+                    print "    %s" % log.creator
+                if options.date:
+                    print "    %s" % time.strftime('%Y-%m-%d %H:%M:%S %Z', log.date)
+                if options.summary:
+                    print "    %s" % log.summary
+                if options.merges:
+                    showed_title = False
+                    for revision in log.merged_patches:
+                        if not showed_title:
+                            print "    Merged:"
+                            showed_title = True
+                        print "    %s" % revision
+            if len(self.cl_revisions) > 0:
+                print pylon.changelog_for_merge(self.cl_revisions)
+        except pylon.errors.TreeRootNone:
+            raise CommandFailedWrapper(
+                Exception("This option can only be used in a project tree."))
+
+    def changelog_append(self, revision):
+        if isinstance(revision, arch.Revision):
+            revision=arch.Patchlog(revision)
+        self.cl_revisions.append(revision)
+   
+    def get_parser(self):
+        """
+        Returns the options parser to use for the "revision" command.
+
+        :rtype: cmdutil.CmdOptionParser
+        """
+        parser=cmdutil.CmdOptionParser("fai revisions [version/revision]")
+        select = cmdutil.OptionGroup(parser, "Selection options",
+                          "Control which revisions are listed.  These options"
+                          " are mutually exclusive.  If more than one is"
+                          " specified, the last is used.")
+
+        cmdutil.add_revision_iter_options(select)
+        parser.add_option("", "--skip", dest="skip", 
+                          help="Skip revisions.  Positive numbers skip from "
+                          "beginning, negative skip from end.",
+                          metavar="NUMBER")
+
+        parser.add_option_group(select)
+
+        format = cmdutil.OptionGroup(parser, "Revision format options",
+                          "These control the appearance of listed revisions")
+        format.add_option("", "--location", action="store_const", 
+                         const=paths.determine_path, dest="display", 
+                         help="Show location instead of name", default=str)
+        format.add_option("--import", action="store_const", 
+                         const=paths.determine_import_path, dest="display",  
+                         help="Show location of import file")
+        format.add_option("--log", action="store_const", 
+                         const=paths.determine_log_path, dest="display", 
+                         help="Show location of log file")
+        format.add_option("--patch", action="store_const", 
+                         dest="display", const=paths.determine_patch_path,
+                         help="Show location of patchfile")
+        format.add_option("--continuation", action="store_const", 
+                         const=paths.determine_continuation_path, 
+                         dest="display",
+                         help="Show location of continuation file")
+        format.add_option("--cacherev", action="store_const", 
+                         const=paths.determine_cacherev_path, dest="display",
+                         help="Show location of cacherev file")
+        format.add_option("--changelog", action="store_const", 
+                         const=self.changelog_append, dest="display",
+                         help="Show location of cacherev file")
+        parser.add_option_group(format)
+        display = cmdutil.OptionGroup(parser, "Display format options",
+                          "These control the display of data")
+        display.add_option("-r", "--reverse", action="store_true", 
+                          dest="reverse", help="Sort from newest to oldest")
+        display.add_option("-s", "--summary", action="store_true", 
+                          dest="summary", help="Show patchlog summary")
+        display.add_option("-D", "--date", action="store_true", 
+                          dest="date", help="Show patchlog date")
+        display.add_option("-c", "--creator", action="store_true", 
+                          dest="creator", help="Show the id that committed the"
+                          " revision")
+        display.add_option("-m", "--merges", action="store_true", 
+                          dest="merges", help="Show the revisions that were"
+                          " merged")
+        parser.add_option_group(display)
+        return parser 
+    def help(self, parser=None):
+        """Attempt to explain the revisions command
+        
+        :param parser: If supplied, used to determine options
+        """
+        if parser==None:
+            parser=self.get_parser()
+        parser.print_help()
+        print """List revisions.
+        """
+        help_tree_spec()
+
+
+class Get(BaseCommand):
+    """
+    Retrieve a revision from the archive
+    """
+    def __init__(self):
+        self.description="Retrieve a revision from the archive"
+        self.parser=self.get_parser()
+
+
+    def get_completer(self, arg, index):
+        if index > 0:
+            return None
+        try:
+            tree = arch.tree_root()
+        except:
+            tree = None
+        return cmdutil.iter_revision_completions(arg, tree)
+
+
+    def do_command(self, cmdargs):
+        """
+        Master function that perfoms the "get" command.
+        """
+        (options, args) = self.parser.parse_args(cmdargs)
+        if len(args) < 1:
+            return self.help()            
+        try:
+            tree = arch.tree_root()
+        except arch.errors.TreeRootError:
+            tree = None
+        
+        arch_loc = None
+        try:
+            revision, arch_loc = paths.full_path_decode(args[0])
+        except Exception, e:
+            revision = cmdutil.determine_revision_arch(tree, args[0], 
+                check_existence=False, allow_package=True)
+        if len(args) > 1:
+            directory = args[1]
+        else:
+            directory = str(revision.nonarch)
+        if os.path.exists(directory):
+            raise DirectoryExists(directory)
+        cmdutil.ensure_archive_registered(revision.archive, arch_loc)
+        try:
+            cmdutil.ensure_revision_exists(revision)
+        except cmdutil.NoSuchRevision, e:
+            raise CommandFailedWrapper(e)
+
+        link = cmdutil.prompt ("get link")
+        for line in cmdutil.iter_get(revision, directory, link,
+                                     options.no_pristine,
+                                     options.no_greedy_add):
+            cmdutil.colorize(line)
+
+    def get_parser(self):
+        """
+        Returns the options parser to use for the "get" command.
+
+        :rtype: cmdutil.CmdOptionParser
+        """
+        parser=cmdutil.CmdOptionParser("fai get revision [dir]")
+        parser.add_option("--no-pristine", action="store_true", 
+                         dest="no_pristine", 
+                         help="Do not make pristine copy for reference")
+        parser.add_option("--no-greedy-add", action="store_true", 
+                         dest="no_greedy_add", 
+                         help="Never add to greedy libraries")
+
+        return parser 
+
+    def help(self, parser=None):
+        """
+        Prints a help message.
+
+        :param parser: If supplied, the parser to use for generating help.  If \
+        not supplied, it is retrieved.
+        :type parser: cmdutil.CmdOptionParser
+        """
+        if parser==None:
+            parser=self.get_parser()
+        parser.print_help()
+        print """
+Expands aliases and constructs a project tree for a revision.  If the optional
+"dir" argument is provided, the project tree will be stored in this directory.
+        """
+        help_tree_spec()
+        return
+
+class PromptCmd(cmd.Cmd):
+    def __init__(self):
+        cmd.Cmd.__init__(self)
+        self.prompt = "Fai> "
+        try:
+            self.tree = arch.tree_root()
+        except:
+            self.tree = None
+        self.set_title()
+        self.set_prompt()
+        self.fake_aba = abacmds.AbaCmds()
+        self.identchars += '-'
+        self.history_file = os.path.expanduser("~/.fai-history")
+        readline.set_completer_delims(string.whitespace)
+        if os.access(self.history_file, os.R_OK) and \
+            os.path.isfile(self.history_file):
+            readline.read_history_file(self.history_file)
+        self.cwd = os.getcwd()
+
+    def write_history(self):
+        readline.write_history_file(self.history_file)
+
+    def do_quit(self, args):
+        self.write_history()
+        sys.exit(0)
+
+    def do_exit(self, args):
+        self.do_quit(args)
+
+    def do_EOF(self, args):
+        print
+        self.do_quit(args)
+
+    def postcmd(self, line, bar):
+        self.set_title()
+        self.set_prompt()
+
+    def set_prompt(self):
+        if self.tree is not None:
+            try:
+                prompt = pylon.alias_or_version(self.tree.tree_version, 
+                                                self.tree, 
+                                                full=False)
+                if prompt is not None:
+                    prompt = " " + prompt
+            except:
+                prompt = ""
+        else:
+            prompt = ""
+        self.prompt = "Fai%s> " % prompt
+
+    def set_title(self, command=None):
+        try:
+            version = pylon.alias_or_version(self.tree.tree_version, self.tree, 
+                                             full=False)
+        except:
+            version = "[no version]"
+        if command is None:
+            command = ""
+        sys.stdout.write(terminal.term_title("Fai %s %s" % (command, version)))
+
+    def do_cd(self, line):
+        if line == "":
+            line = "~"
+        line = os.path.expanduser(line)
+        if os.path.isabs(line):
+            newcwd = line
+        else:
+            newcwd = self.cwd+'/'+line
+        newcwd = os.path.normpath(newcwd)
+        try:
+            os.chdir(newcwd)
+            self.cwd = newcwd
+        except Exception, e:
+            print e
+        try:
+            self.tree = arch.tree_root()
+        except:
+            self.tree = None
+
+    def do_help(self, line):
+        Help()(line)
+
+    def default(self, line):
+        args = line.split()
+        if find_command(args[0]):
+            try:
+                find_command(args[0]).do_command(args[1:])
+            except cmdutil.BadCommandOption, e:
+                print e
+            except cmdutil.GetHelp, e:
+                find_command(args[0]).help()
+            except CommandFailed, e:
+                print e
+            except arch.errors.ArchiveNotRegistered, e:
+                print e
+            except KeyboardInterrupt, e:
+                print "Interrupted"
+            except arch.util.ExecProblem, e:
+                print e.proc.error.rstrip('\n')
+            except cmdutil.CantDetermineVersion, e:
+                print e
+            except cmdutil.CantDetermineRevision, e:
+                print e
+            except Exception, e:
+                print "Unhandled error:\n%s" % errors.exception_str(e)
+
+        elif suggestions.has_key(args[0]):
+            print suggestions[args[0]]
+
+        elif self.fake_aba.is_command(args[0]):
+            tree = None
+            try:
+                tree = arch.tree_root()
+            except arch.errors.TreeRootError:
+                pass
+            cmd = self.fake_aba.is_command(args[0])
+            try:
+                cmd.run(cmdutil.expand_prefix_alias(args[1:], tree))
+            except KeyboardInterrupt, e:
+                print "Interrupted"
+
+        elif options.tla_fallthrough and args[0] != "rm" and \
+            cmdutil.is_tla_command(args[0]):
+            try:
+                tree = None
+                try:
+                    tree = arch.tree_root()
+                except arch.errors.TreeRootError:
+                    pass
+                args = cmdutil.expand_prefix_alias(args, tree)
+                arch.util.exec_safe('tla', args, stderr=sys.stderr,
+                expected=(0, 1))
+            except arch.util.ExecProblem, e:
+                pass
+            except KeyboardInterrupt, e:
+                print "Interrupted"
+        else:
+            try:
+                try:
+                    tree = arch.tree_root()
+                except arch.errors.TreeRootError:
+                    tree = None
+                args=line.split()
+                os.system(" ".join(cmdutil.expand_prefix_alias(args, tree)))
+            except KeyboardInterrupt, e:
+                print "Interrupted"
+
+    def completenames(self, text, line, begidx, endidx):
+        completions = []
+        iter = iter_command_names(self.fake_aba)
+        try:
+            if len(line) > 0:
+                arg = line.split()[-1]
+            else:
+                arg = ""
+            iter = cmdutil.iter_munged_completions(iter, arg, text)
+        except Exception, e:
+            print e
+        return list(iter)
+
+    def completedefault(self, text, line, begidx, endidx):
+        """Perform completion for native commands.
+        
+        :param text: The text to complete
+        :type text: str
+        :param line: The entire line to complete
+        :type line: str
+        :param begidx: The start of the text in the line
+        :type begidx: int
+        :param endidx: The end of the text in the line
+        :type endidx: int
+        """
+        try:
+            (cmd, args, foo) = self.parseline(line)
+            command_obj=find_command(cmd)
+            if command_obj is not None:
+                return command_obj.complete(args.split(), text)
+            elif not self.fake_aba.is_command(cmd) and \
+                cmdutil.is_tla_command(cmd):
+                iter = cmdutil.iter_supported_switches(cmd)
+                if len(args) > 0:
+                    arg = args.split()[-1]
+                else:
+                    arg = ""
+                if arg.startswith("-"):
+                    return list(cmdutil.iter_munged_completions(iter, arg, 
+                                                                text))
+                else:
+                    return list(cmdutil.iter_munged_completions(
+                        cmdutil.iter_file_completions(arg), arg, text))
+
+
+            elif cmd == "cd":
+                if len(args) > 0:
+                    arg = args.split()[-1]
+                else:
+                    arg = ""
+                iter = cmdutil.iter_dir_completions(arg)
+                iter = cmdutil.iter_munged_completions(iter, arg, text)
+                return list(iter)
+            elif len(args)>0:
+                arg = args.split()[-1]
+                iter = cmdutil.iter_file_completions(arg)
+                return list(cmdutil.iter_munged_completions(iter, arg, text))
+            else:
+                return self.completenames(text, line, begidx, endidx)
+        except Exception, e:
+            print e
+
+
+def iter_command_names(fake_aba):
+    for entry in cmdutil.iter_combine([commands.iterkeys(), 
+                                     fake_aba.get_commands(), 
+                                     cmdutil.iter_tla_commands(False)]):
+        if not suggestions.has_key(str(entry)):
+            yield entry
+
+
+def iter_source_file_completions(tree, arg):
+    treepath = arch_compound.tree_cwd(tree)
+    if len(treepath) > 0:
+        dirs = [treepath]
+    else:
+        dirs = None
+    for file in tree.iter_inventory(dirs, source=True, both=True):
+        file = file_completion_match(file, treepath, arg)
+        if file is not None:
+            yield file
+
+
+def iter_untagged(tree, dirs):
+    for file in arch_core.iter_inventory_filter(tree, dirs, tagged=False, 
+                                                categories=arch_core.non_root,
+                                                control_files=True):
+        yield file.name 
+
+
+def iter_untagged_completions(tree, arg):
+    """Generate an iterator for all visible untagged files that match arg.
+
+    :param tree: The tree to look for untagged files in
+    :type tree: `arch.WorkingTree`
+    :param arg: The argument to match
+    :type arg: str
+    :return: An iterator of all matching untagged files
+    :rtype: iterator of str
+    """
+    treepath = arch_compound.tree_cwd(tree)
+    if len(treepath) > 0:
+        dirs = [treepath]
+    else:
+        dirs = None
+
+    for file in iter_untagged(tree, dirs):
+        file = file_completion_match(file, treepath, arg)
+        if file is not None:
+            yield file
+
+
+def file_completion_match(file, treepath, arg):
+    """Determines whether a file within an arch tree matches the argument.
+
+    :param file: The rooted filename
+    :type file: str
+    :param treepath: The path to the cwd within the tree
+    :type treepath: str
+    :param arg: The prefix to match
+    :return: The completion name, or None if not a match
+    :rtype: str
+    """
+    if not file.startswith(treepath):
+        return None
+    if treepath != "":
+        file = file[len(treepath)+1:]
+
+    if not file.startswith(arg):
+        return None 
+    if os.path.isdir(file):
+        file += '/'
+    return file
+
+def iter_modified_file_completions(tree, arg):
+    """Returns a list of modified files that match the specified prefix.
+
+    :param tree: The current tree
+    :type tree: `arch.WorkingTree`
+    :param arg: The prefix to match
+    :type arg: str
+    """
+    treepath = arch_compound.tree_cwd(tree)
+    tmpdir = util.tmpdir()
+    changeset = tmpdir+"/changeset"
+    completions = []
+    revision = cmdutil.determine_revision_tree(tree)
+    for line in arch.iter_delta(revision, tree, changeset):
+        if isinstance(line, arch.FileModification):
+            file = file_completion_match(line.name[1:], treepath, arg)
+            if file is not None:
+                completions.append(file)
+    shutil.rmtree(tmpdir)
+    return completions
+
+class Shell(BaseCommand):
+    def __init__(self):
+        self.description = "Runs Fai as a shell"
+
+    def do_command(self, cmdargs):
+        if len(cmdargs)!=0:
+            raise cmdutil.GetHelp
+        prompt = PromptCmd()
+        try:
+            prompt.cmdloop()
+        finally:
+            prompt.write_history()
+
+class AddID(BaseCommand):
+    """
+    Adds an inventory id for the given file
+    """
+    def __init__(self):
+        self.description="Add an inventory id for a given file"
+
+    def get_completer(self, arg, index):
+        tree = arch.tree_root()
+        return iter_untagged_completions(tree, arg)
+
+    def do_command(self, cmdargs):
+        """
+        Master function that perfoms the "revision" command.
+        """
+        parser=self.get_parser()
+        (options, args) = parser.parse_args(cmdargs)
+
+        try:
+            tree = arch.tree_root()
+        except arch.errors.TreeRootError, e:
+            raise pylon.errors.CommandFailedWrapper(e)
+            
+
+        if (len(args) == 0) == (options.untagged == False):
+            raise cmdutil.GetHelp
+
+       #if options.id and len(args) != 1:
+       #    print "If --id is specified, only one file can be named."
+       #    return
+        
+        method = tree.tagging_method
+        
+        if options.id_type == "tagline":
+            if method != "tagline":
+                if not cmdutil.prompt("Tagline in other tree"):
+                    if method == "explicit" or method == "implicit":
+                        options.id_type == method
+                    else:
+                        print "add-id not supported for \"%s\" tagging method"\
+                            % method 
+                        return
+        
+        elif options.id_type == "implicit":
+            if method != "implicit":
+                if not cmdutil.prompt("Implicit in other tree"):
+                    if method == "explicit" or method == "tagline":
+                        options.id_type == method
+                    else:
+                        print "add-id not supported for \"%s\" tagging method"\
+                            % method 
+                        return
+        elif options.id_type == "explicit":
+            if method != "tagline" and method != explicit:
+                if not prompt("Explicit in other tree"):
+                    print "add-id not supported for \"%s\" tagging method" % \
+                        method
+                    return
+        
+        if options.id_type == "auto":
+            if method != "tagline" and method != "explicit" \
+                and method !="implicit":
+                print "add-id not supported for \"%s\" tagging method" % method
+                return
+            else:
+                options.id_type = method
+        if options.untagged:
+            args = None
+        self.add_ids(tree, options.id_type, args)
+
+    def add_ids(self, tree, id_type, files=()):
+        """Add inventory ids to files.
+        
+        :param tree: the tree the files are in
+        :type tree: `arch.WorkingTree`
+        :param id_type: the type of id to add: "explicit" or "tagline"
+        :type id_type: str
+        :param files: The list of files to add.  If None do all untagged.
+        :type files: tuple of str
+        """
+
+        untagged = (files is None)
+        if untagged:
+            files = list(iter_untagged(tree, None))
+        previous_files = []
+        while len(files) > 0:
+            previous_files.extend(files)
+            if id_type == "explicit":
+                cmdutil.add_id(files)
+            elif id_type == "tagline" or id_type == "implicit":
+                for file in files:
+                    try:
+                        implicit = (id_type == "implicit")
+                        cmdutil.add_tagline_or_explicit_id(file, False,
+                                                           implicit)
+                    except cmdutil.AlreadyTagged:
+                        print "\"%s\" already has a tagline." % file
+                    except cmdutil.NoCommentSyntax:
+                        pass
+            #do inventory after tagging until no untagged files are encountered
+            if untagged:
+                files = []
+                for file in iter_untagged(tree, None):
+                    if not file in previous_files:
+                        files.append(file)
+
+            else:
+                break
+
+    def get_parser(self):
+        """
+        Returns the options parser to use for the "revision" command.
+
+        :rtype: cmdutil.CmdOptionParser
+        """
+        parser=cmdutil.CmdOptionParser("fai add-id file1 [file2] [file3]...")
+# ddaa suggests removing this to promote GUIDs.  Let's see who squalks.
+#        parser.add_option("-i", "--id", dest="id", 
+#                         help="Specify id for a single file", default=None)
+        parser.add_option("--tltl", action="store_true", 
+                         dest="lord_style",  help="Use Tom Lord's style of id.")
+        parser.add_option("--explicit", action="store_const", 
+                         const="explicit", dest="id_type", 
+                         help="Use an explicit id", default="auto")
+        parser.add_option("--tagline", action="store_const", 
+                         const="tagline", dest="id_type", 
+                         help="Use a tagline id")
+        parser.add_option("--implicit", action="store_const", 
+                         const="implicit", dest="id_type", 
+                         help="Use an implicit id (deprecated)")
+        parser.add_option("--untagged", action="store_true", 
+                         dest="untagged", default=False, 
+                         help="tag all untagged files")
+        return parser 
+
+    def help(self, parser=None):
+        """
+        Prints a help message.
+
+        :param parser: If supplied, the parser to use for generating help.  If \
+        not supplied, it is retrieved.
+        :type parser: cmdutil.CmdOptionParser
+        """
+        if parser==None:
+            parser=self.get_parser()
+        parser.print_help()
+        print """
+Adds an inventory to the specified file(s) and directories.  If --untagged is
+specified, adds inventory to all untagged files and directories.
+        """
+        return
+
+
+class Merge(BaseCommand):
+    """
+    Merges changes from other versions into the current tree
+    """
+    def __init__(self):
+        self.description="Merges changes from other versions"
+        try:
+            self.tree = arch.tree_root()
+        except:
+            self.tree = None
+
+
+    def get_completer(self, arg, index):
+        if self.tree is None:
+            raise arch.errors.TreeRootError
+        return cmdutil.merge_completions(self.tree, arg, index)
+
+    def do_command(self, cmdargs):
+        """
+        Master function that perfoms the "merge" command.
+        """
+        parser=self.get_parser()
+        (options, args) = parser.parse_args(cmdargs)
+        if options.diff3:
+            action="star-merge"
+        else:
+            action = options.action
+        
+        if self.tree is None:
+            raise arch.errors.TreeRootError(os.getcwd())
+        if cmdutil.has_changed(ancillary.comp_revision(self.tree)):
+            raise UncommittedChanges(self.tree)
+
+        if len(args) > 0:
+            revisions = []
+            for arg in args:
+                revisions.append(cmdutil.determine_revision_arch(self.tree, 
+                                                                 arg))
+            source = "from commandline"
+        else:
+            revisions = ancillary.iter_partner_revisions(self.tree, 
+                                                         self.tree.tree_version)
+            source = "from partner version"
+        revisions = misc.rewind_iterator(revisions)
+        try:
+            revisions.next()
+            revisions.rewind()
+        except StopIteration, e:
+            revision = cmdutil.tag_cur(self.tree)
+            if revision is None:
+                raise CantDetermineRevision("", "No version specified, no "
+                                            "partner-versions, and no tag"
+                                            " source")
+            revisions = [revision]
+            source = "from tag source"
+        for revision in revisions:
+            cmdutil.ensure_archive_registered(revision.archive)
+            cmdutil.colorize(arch.Chatter("* Merging %s [%s]" % 
+                             (revision, source)))
+            if action=="native-merge" or action=="update":
+                if self.native_merge(revision, action) == 0:
+                    continue
+            elif action=="star-merge":
+                try: 
+                    self.star_merge(revision, options.diff3)
+                except errors.MergeProblem, e:
+                    break
+            if cmdutil.has_changed(self.tree.tree_version):
+                break
+
+    def star_merge(self, revision, diff3):
+        """Perform a star-merge on the current tree.
+        
+        :param revision: The revision to use for the merge
+        :type revision: `arch.Revision`
+        :param diff3: If true, do a diff3 merge
+        :type diff3: bool
+        """
+        try:
+            for line in self.tree.iter_star_merge(revision, diff3=diff3):
+                cmdutil.colorize(line)
+        except arch.util.ExecProblem, e:
+            if e.proc.status is not None and e.proc.status == 1:
+                if e.proc.error:
+                    print e.proc.error
+                raise MergeProblem
+            else:
+                raise
+
+    def native_merge(self, other_revision, action):
+        """Perform a native-merge on the current tree.
+        
+        :param other_revision: The revision to use for the merge
+        :type other_revision: `arch.Revision`
+        :return: 0 if the merge was skipped, 1 if it was applied
+        """
+        other_tree = arch_compound.find_or_make_local_revision(other_revision)
+        try:
+            if action == "native-merge":
+                ancestor = arch_compound.merge_ancestor2(self.tree, other_tree, 
+                                                         other_revision)
+            elif action == "update":
+                ancestor = arch_compound.tree_latest(self.tree, 
+                                                     other_revision.version)
+        except CantDetermineRevision, e:
+            raise CommandFailedWrapper(e)
+        cmdutil.colorize(arch.Chatter("* Found common ancestor %s" % ancestor))
+        if (ancestor == other_revision):
+            cmdutil.colorize(arch.Chatter("* Skipping redundant merge" 
+                                          % ancestor))
+            return 0
+        delta = cmdutil.apply_delta(ancestor, other_tree, self.tree)    
+        for line in cmdutil.iter_apply_delta_filter(delta):
+            cmdutil.colorize(line)
+        return 1
+
+
+
+    def get_parser(self):
+        """
+        Returns the options parser to use for the "merge" command.
+
+        :rtype: cmdutil.CmdOptionParser
+        """
+        parser=cmdutil.CmdOptionParser("fai merge [VERSION]")
+        parser.add_option("-s", "--star-merge", action="store_const",
+                          dest="action", help="Use star-merge",
+                          const="star-merge", default="native-merge")
+        parser.add_option("--update", action="store_const",
+                          dest="action", help="Use update picker",
+                          const="update")
+        parser.add_option("--diff3", action="store_true", 
+                         dest="diff3",  
+                         help="Use diff3 for merge (implies star-merge)")
+        return parser 
+
+    def help(self, parser=None):
+        """
+        Prints a help message.
+
+        :param parser: If supplied, the parser to use for generating help.  If \
+        not supplied, it is retrieved.
+        :type parser: cmdutil.CmdOptionParser
+        """
+        if parser==None:
+            parser=self.get_parser()
+        parser.print_help()
+        print """
+Performs a merge operation using the specified version.
+        """
+        return
+
+class ELog(BaseCommand):
+    """
+    Produces a raw patchlog and invokes the user's editor
+    """
+    def __init__(self):
+        self.description="Edit a patchlog to commit"
+        try:
+            self.tree = arch.tree_root()
+        except:
+            self.tree = None
+
+
+    def do_command(self, cmdargs):
+        """
+        Master function that perfoms the "elog" command.
+        """
+        parser=self.get_parser()
+        (options, args) = parser.parse_args(cmdargs)
+        if self.tree is None:
+            raise arch.errors.TreeRootError
+
+        try:
+            edit_log(self.tree, self.tree.tree_version)
+        except pylon.errors.NoEditorSpecified, e:
+            raise pylon.errors.CommandFailedWrapper(e)
+
+    def get_parser(self):
+        """
+        Returns the options parser to use for the "merge" command.
+
+        :rtype: cmdutil.CmdOptionParser
+        """
+        parser=cmdutil.CmdOptionParser("fai elog")
+        return parser 
+
+
+    def help(self, parser=None):
+        """
+        Invokes $EDITOR to produce a log for committing.
+
+        :param parser: If supplied, the parser to use for generating help.  If \
+        not supplied, it is retrieved.
+        :type parser: cmdutil.CmdOptionParser
+        """
+        if parser==None:
+            parser=self.get_parser()
+        parser.print_help()
+        print """
+Invokes $EDITOR to produce a log for committing.
+        """
+        return
+
+def edit_log(tree, version):
+    """Makes and edits the log for a tree.  Does all kinds of fancy things
+    like log templates and merge summaries and log-for-merge
+    
+    :param tree: The tree to edit the log for
+    :type tree: `arch.WorkingTree`
+    """
+    #ensure we have an editor before preparing the log
+    cmdutil.find_editor()
+    log = tree.log_message(create=False, version=version)
+    log_is_new = False
+    if log is None or cmdutil.prompt("Overwrite log"):
+        if log is not None:
+           os.remove(log.name)
+        log = tree.log_message(create=True, version=version)
+        log_is_new = True
+        tmplog = log.name
+        template = pylon.log_template_path(tree)
+        if template:
+            shutil.copyfile(template, tmplog)
+        comp_version = ancillary.comp_revision(tree).version
+        new_merges = cmdutil.iter_new_merges(tree, comp_version)
+        new_merges = cmdutil.direct_merges(new_merges)
+        log["Summary"] = pylon.merge_summary(new_merges, 
+                                         version)
+        if len(new_merges) > 0:   
+            if cmdutil.prompt("Log for merge"):
+                if cmdutil.prompt("changelog for merge"):
+                    mergestuff = "Patches applied:\n"
+                    mergestuff += pylon.changelog_for_merge(new_merges)
+                else:
+                    mergestuff = cmdutil.log_for_merge(tree, comp_version)
+                log.description += mergestuff
+        log.save()
+    try:
+        cmdutil.invoke_editor(log.name)
+    except:
+        if log_is_new:
+            os.remove(log.name)
+        raise
+
+
+class MirrorArchive(BaseCommand):
+    """
+    Updates a mirror from an archive
+    """
+    def __init__(self):
+        self.description="Update a mirror from an archive"
+
+    def do_command(self, cmdargs):
+        """
+        Master function that perfoms the "revision" command.
+        """
+
+        parser=self.get_parser()
+        (options, args) = parser.parse_args(cmdargs)
+        if len(args) > 1:
+            raise GetHelp
+        try:
+            tree = arch.tree_root()
+        except:
+            tree = None
+
+        if len(args) == 0:
+            if tree is not None:
+                name = tree.tree_version()
+        else:
+            name = cmdutil.expand_alias(args[0], tree)
+            name = arch.NameParser(name)
+
+        to_arch = name.get_archive()
+        from_arch = cmdutil.get_mirror_source(arch.Archive(to_arch))
+        limit = name.get_nonarch()
+
+        iter = arch_core.mirror_archive(from_arch,to_arch, limit)
+        for line in arch.chatter_classifier(iter):
+            cmdutil.colorize(line)
+
+    def get_parser(self):
+        """
+        Returns the options parser to use for the "revision" command.
+
+        :rtype: cmdutil.CmdOptionParser
+        """
+        parser=cmdutil.CmdOptionParser("fai mirror-archive ARCHIVE")
+        return parser 
+
+    def help(self, parser=None):
+        """
+        Prints a help message.
+
+        :param parser: If supplied, the parser to use for generating help.  If \
+        not supplied, it is retrieved.
+        :type parser: cmdutil.CmdOptionParser
+        """
+        if parser==None:
+            parser=self.get_parser()
+        parser.print_help()
+        print """
+Updates a mirror from an archive.  If a branch, package, or version is
+supplied, only changes under it are mirrored.
+        """
+        return
+
+def help_tree_spec():
+    print """Specifying revisions (default: tree)
+Revisions may be specified by alias, revision, version or patchlevel.
+Revisions or versions may be fully qualified.  Unqualified revisions, versions, 
+or patchlevels use the archive of the current project tree.  Versions will
+use the latest patchlevel in the tree.  Patchlevels will use the current tree-
+version.
+
+Use "alias" to list available (user and automatic) aliases."""
+
+auto_alias = [
+"acur", 
+"The latest revision in the archive of the tree-version.  You can specify \
+a different version like so: acur:foo--bar--0 (aliases can be used)",
+"tcur",
+"""(tree current) The latest revision in the tree of the tree-version. \
+You can specify a different version like so: tcur:foo--bar--0 (aliases can be \
+used).""",
+"tprev" , 
+"""(tree previous) The previous revision in the tree of the tree-version.  To \
+specify an older revision, use a number, e.g. "tprev:4" """,
+"tanc" , 
+"""(tree ancestor) The ancestor revision of the tree To specify an older \
+revision, use a number, e.g. "tanc:4".""",
+"tdate" , 
+"""(tree date) The latest revision from a given date, e.g. "tdate:July 6".""",
+"tmod" , 
+""" (tree modified) The latest revision to modify a given file, e.g. \
+"tmod:engine.cpp" or "tmod:engine.cpp:16".""",
+"ttag" , 
+"""(tree tag) The revision that was tagged into the current tree revision, \
+according to the tree""",
+"tagcur", 
+"""(tag current) The latest revision of the version that the current tree \
+was tagged from.""",
+"mergeanc" , 
+"""The common ancestor of the current tree and the specified revision. \
+Defaults to the first partner-version's latest revision or to tagcur.""",
+]
+
+
+def is_auto_alias(name):
+    """Determine whether a name is an auto alias name
+
+    :param name: the name to check
+    :type name: str
+    :return: True if the name is an auto alias, false if not
+    :rtype: bool
+    """
+    return name in [f for (f, v) in pylon.util.iter_pairs(auto_alias)]
+
+
+def display_def(iter, wrap = 80):
+    """Display a list of definitions
+
+    :param iter: iter of name, definition pairs
+    :type iter: iter of (str, str)
+    :param wrap: The width for text wrapping
+    :type wrap: int
+    """
+    vals = list(iter)
+    maxlen = 0
+    for (key, value) in vals:
+        if len(key) > maxlen:
+            maxlen = len(key)
+    for (key, value) in vals:
+        tw=textwrap.TextWrapper(width=wrap, 
+                                initial_indent=key.rjust(maxlen)+" : ",
+                                subsequent_indent="".rjust(maxlen+3))
+        print tw.fill(value)
+
+
+def help_aliases(tree):
+    print """Auto-generated aliases"""
+    display_def(pylon.util.iter_pairs(auto_alias))
+    print "User aliases"
+    display_def(ancillary.iter_all_alias(tree))
+
+class Inventory(BaseCommand):
+    """List the status of files in the tree"""
+    def __init__(self):
+        self.description=self.__doc__
+
+    def do_command(self, cmdargs):
+        """
+        Master function that perfoms the "revision" command.
+        """
+
+        parser=self.get_parser()
+        (options, args) = parser.parse_args(cmdargs)
+        tree = arch.tree_root()
+        categories = []
+
+        if (options.source):
+            categories.append(arch_core.SourceFile)
+        if (options.precious):
+            categories.append(arch_core.PreciousFile)
+        if (options.backup):
+            categories.append(arch_core.BackupFile)
+        if (options.junk):
+            categories.append(arch_core.JunkFile)
+
+        if len(categories) == 1:
+            show_leading = False
+        else:
+            show_leading = True
+
+        if len(categories) == 0:
+            categories = None
+
+        if options.untagged:
+            categories = arch_core.non_root
+            show_leading = False
+            tagged = False
+        else:
+            tagged = None
+        
+        for file in arch_core.iter_inventory_filter(tree, None, 
+            control_files=options.control_files, 
+            categories = categories, tagged=tagged):
+            print arch_core.file_line(file, 
+                                      category = show_leading, 
+                                      untagged = show_leading,
+                                      id = options.ids)
+
+    def get_parser(self):
+        """
+        Returns the options parser to use for the "revision" command.
+
+        :rtype: cmdutil.CmdOptionParser
+        """
+        parser=cmdutil.CmdOptionParser("fai inventory [options]")
+        parser.add_option("--ids", action="store_true", dest="ids", 
+                          help="Show file ids")
+        parser.add_option("--control", action="store_true", 
+                          dest="control_files", help="include control files")
+        parser.add_option("--source", action="store_true", dest="source",
+                          help="List source files")
+        parser.add_option("--backup", action="store_true", dest="backup",
+                          help="List backup files")
+        parser.add_option("--precious", action="store_true", dest="precious",
+                          help="List precious files")
+        parser.add_option("--junk", action="store_true", dest="junk",
+                          help="List junk files")
+        parser.add_option("--unrecognized", action="store_true", 
+                          dest="unrecognized", help="List unrecognized files")
+        parser.add_option("--untagged", action="store_true", 
+                          dest="untagged", help="List only untagged files")
+        return parser 
+
+    def help(self, parser=None):
+        """
+        Prints a help message.
+
+        :param parser: If supplied, the parser to use for generating help.  If \
+        not supplied, it is retrieved.
+        :type parser: cmdutil.CmdOptionParser
+        """
+        if parser==None:
+            parser=self.get_parser()
+        parser.print_help()
+        print """
+Lists the status of files in the archive:
+S source
+P precious
+B backup
+J junk
+U unrecognized
+T tree root
+? untagged-source
+Leading letter are not displayed if only one kind of file is shown
+        """
+        return
+
+
+class Alias(BaseCommand):
+    """List or adjust aliases"""
+    def __init__(self):
+        self.description=self.__doc__
+
+    def get_completer(self, arg, index):
+        if index > 2:
+            return ()
+        try:
+            self.tree = arch.tree_root()
+        except:
+            self.tree = None
+
+        if index == 0:
+            return [part[0]+" " for part in ancillary.iter_all_alias(self.tree)]
+        elif index == 1:
+            return cmdutil.iter_revision_completions(arg, self.tree)
+
+
+    def do_command(self, cmdargs):
+        """
+        Master function that perfoms the "revision" command.
+        """
+
+        parser=self.get_parser()
+        (options, args) = parser.parse_args(cmdargs)
+        try:
+            self.tree =  arch.tree_root()
+        except:
+            self.tree = None
+
+
+        try:
+            options.action(args, options)
+        except cmdutil.ForbiddenAliasSyntax, e:
+            raise CommandFailedWrapper(e)
+
+    def no_prefix(self, alias):
+        if alias.startswith("^"):
+            alias = alias[1:]
+        return alias
+        
+    def arg_dispatch(self, args, options):
+        """Add, modify, or list aliases, depending on number of arguments
+
+        :param args: The list of commandline arguments
+        :type args: list of str
+        :param options: The commandline options
+        """
+        if len(args) == 0:
+            help_aliases(self.tree)
+            return
+        else:
+            alias = self.no_prefix(args[0])
+            if len(args) == 1:
+                self.print_alias(alias)
+            elif (len(args)) == 2:
+                self.add(alias, args[1], options)
+            else:
+                raise cmdutil.GetHelp
+
+    def print_alias(self, alias):
+        answer = None
+        if is_auto_alias(alias):
+            raise pylon.errors.IsAutoAlias(alias, "\"%s\" is an auto alias."
+                "  Use \"revision\" to expand auto aliases." % alias)
+        for pair in ancillary.iter_all_alias(self.tree):
+            if pair[0] == alias:
+                answer = pair[1]
+        if answer is not None:
+            print answer
+        else:
+            print "The alias %s is not assigned." % alias
+
+    def add(self, alias, expansion, options):
+        """Add or modify aliases
+
+        :param alias: The alias name to create/modify
+        :type alias: str
+        :param expansion: The expansion to assign to the alias name
+        :type expansion: str
+        :param options: The commandline options
+        """
+        if is_auto_alias(alias):
+            raise IsAutoAlias(alias)
+        newlist = ""
+        written = False
+        new_line = "%s=%s\n" % (alias, cmdutil.expand_alias(expansion, 
+            self.tree))
+        ancillary.check_alias(new_line.rstrip("\n"), [alias, expansion])
+
+        for pair in self.get_iterator(options):
+            if pair[0] != alias:
+                newlist+="%s=%s\n" % (pair[0], pair[1])
+            elif not written:
+                newlist+=new_line
+                written = True
+        if not written:
+            newlist+=new_line
+        self.write_aliases(newlist, options)
+            
+    def delete(self, args, options):
+        """Delete the specified alias
+
+        :param args: The list of arguments
+        :type args: list of str
+        :param options: The commandline options
+        """
+        deleted = False
+        if len(args) != 1:
+            raise cmdutil.GetHelp
+        alias = self.no_prefix(args[0])
+        if is_auto_alias(alias):
+            raise IsAutoAlias(alias)
+        newlist = ""
+        for pair in self.get_iterator(options):
+            if pair[0] != alias:
+                newlist+="%s=%s\n" % (pair[0], pair[1])
+            else:
+                deleted = True
+        if not deleted:
+            raise errors.NoSuchAlias(alias)
+        self.write_aliases(newlist, options)
+
+    def get_alias_file(self, options):
+        """Return the name of the alias file to use
+
+        :param options: The commandline options
+        """
+        if options.tree:
+            if self.tree is None:
+                self.tree == arch.tree_root()
+            return str(self.tree)+"/{arch}/+aliases"
+        else:
+            return "~/.aba/aliases"
+
+    def get_iterator(self, options):
+        """Return the alias iterator to use
+
+        :param options: The commandline options
+        """
+        return ancillary.iter_alias(self.get_alias_file(options))
+
+    def write_aliases(self, newlist, options):
+        """Safely rewrite the alias file
+        :param newlist: The new list of aliases
+        :type newlist: str
+        :param options: The commandline options
+        """
+        filename = os.path.expanduser(self.get_alias_file(options))
+        file = util.NewFileVersion(filename)
+        file.write(newlist)
+        file.commit()
+
+
+    def get_parser(self):
+        """
+        Returns the options parser to use for the "alias" command.
+
+        :rtype: cmdutil.CmdOptionParser
+        """
+        parser=cmdutil.CmdOptionParser("fai alias [ALIAS] [NAME]")
+        parser.add_option("-d", "--delete", action="store_const", dest="action",
+                          const=self.delete, default=self.arg_dispatch, 
+                          help="Delete an alias")
+        parser.add_option("--tree", action="store_true", dest="tree", 
+                          help="Create a per-tree alias", default=False)
+        return parser 
+
+    def help(self, parser=None):
+        """
+        Prints a help message.
+
+        :param parser: If supplied, the parser to use for generating help.  If \
+        not supplied, it is retrieved.
+        :type parser: cmdutil.CmdOptionParser
+        """
+        if parser==None:
+            parser=self.get_parser()
+        parser.print_help()
+        print """
+Lists current aliases or modifies the list of aliases.
+
+If no arguments are supplied, aliases will be listed.  If two arguments are
+supplied, the specified alias will be created or modified.  If -d or --delete
+is supplied, the specified alias will be deleted.
+
+You can create aliases that refer to any fully-qualified part of the
+Arch namespace, e.g. 
+archive, 
+archive/category, 
+archive/category--branch, 
+archive/category--branch--version (my favourite)
+archive/category--branch--version--patchlevel
+
+Aliases can be used automatically by native commands.  To use them
+with external or tla commands, prefix them with ^ (you can do this
+with native commands, too).
+"""
+
+
+class RequestMerge(BaseCommand):
+    """Submit a merge request to Bug Goo"""
+    def __init__(self):
+        self.description=self.__doc__
+
+    def do_command(self, cmdargs):
+        """Submit a merge request
+
+        :param cmdargs: The commandline arguments
+        :type cmdargs: list of str
+        """
+        parser = self.get_parser()
+        (options, args) = parser.parse_args(cmdargs)
+        try:
+            cmdutil.find_editor()
+        except pylon.errors.NoEditorSpecified, e:
+            raise pylon.errors.CommandFailedWrapper(e)
+        try:
+            self.tree=arch.tree_root()
+        except:
+            self.tree=None
+        base, revisions = self.revision_specs(args)
+        message = self.make_headers(base, revisions)
+        message += self.make_summary(revisions)
+        path = self.edit_message(message)
+        message = self.tidy_message(path)
+        if cmdutil.prompt("Send merge"):
+            self.send_message(message)
+            print "Merge request sent"
+
+    def make_headers(self, base, revisions):
+        """Produce email and Bug Goo header strings
+
+        :param base: The base revision to apply merges to
+        :type base: `arch.Revision`
+        :param revisions: The revisions to replay into the base
+        :type revisions: list of `arch.Patchlog`
+        :return: The headers
+        :rtype: str
+        """
+        headers = "To: gnu-arch-users@gnu.org\n"
+        headers += "From: %s\n" % options.fromaddr
+        if len(revisions) == 1:
+            headers += "Subject: [MERGE REQUEST] %s\n" % revisions[0].summary
+        else:
+            headers += "Subject: [MERGE REQUEST]\n"
+        headers += "\n"
+        headers += "Base-Revision: %s\n" % base
+        for revision in revisions:
+            headers += "Revision: %s\n" % revision.revision
+        headers += "Bug: \n\n"
+        return headers
+
+    def make_summary(self, logs):
+        """Generate a summary of merges
+
+        :param logs: the patchlogs that were directly added by the merges
+        :type logs: list of `arch.Patchlog`
+        :return: the summary
+        :rtype: str
+        """ 
+        summary = ""
+        for log in logs:
+            summary+=str(log.revision)+"\n"
+            summary+=log.summary+"\n"
+            if log.description.strip():
+                summary+=log.description.strip('\n')+"\n\n"
+        return summary
+
+    def revision_specs(self, args):
+        """Determine the base and merge revisions from tree and arguments.
+
+        :param args: The parsed arguments
+        :type args: list of str
+        :return: The base revision and merge revisions 
+        :rtype: `arch.Revision`, list of `arch.Patchlog`
+        """
+        if len(args) > 0:
+            target_revision = cmdutil.determine_revision_arch(self.tree, 
+                                                              args[0])
+        else:
+            target_revision = arch_compound.tree_latest(self.tree)
+        if len(args) > 1:
+            merges = [ arch.Patchlog(cmdutil.determine_revision_arch(
+                       self.tree, f)) for f in args[1:] ]
+        else:
+            if self.tree is None:
+                raise CantDetermineRevision("", "Not in a project tree")
+            merge_iter = cmdutil.iter_new_merges(self.tree, 
+                                                 target_revision.version, 
+                                                 False)
+            merges = [f for f in cmdutil.direct_merges(merge_iter)]
+        return (target_revision, merges)
+
+    def edit_message(self, message):
+        """Edit an email message in the user's standard editor
+
+        :param message: The message to edit
+        :type message: str
+        :return: the path of the edited message
+        :rtype: str
+        """
+        if self.tree is None:
+            path = os.get_cwd()
+        else:
+            path = self.tree
+        path += "/,merge-request"
+        file = open(path, 'w')
+        file.write(message)
+        file.flush()
+        cmdutil.invoke_editor(path)
+        return path
+
+    def tidy_message(self, path):
+        """Validate and clean up message.
+
+        :param path: The path to the message to clean up
+        :type path: str
+        :return: The parsed message
+        :rtype: `email.Message`
+        """
+        mail = email.message_from_file(open(path))
+        if mail["Subject"].strip() == "[MERGE REQUEST]":
+            raise BlandSubject
+        
+        request = email.message_from_string(mail.get_payload())
+        if request.has_key("Bug"):
+            if request["Bug"].strip()=="":
+                del request["Bug"]
+        mail.set_payload(request.as_string())
+        return mail
+
+    def send_message(self, message):
+        """Send a message, using its headers to address it.
+
+        :param message: The message to send
+        :type message: `email.Message`"""
+        server = smtplib.SMTP("localhost")
+        server.sendmail(message['From'], message['To'], message.as_string())
+        server.quit()
+
+    def help(self, parser=None):
+        """Print a usage message
+
+        :param parser: The options parser to use
+        :type parser: `cmdutil.CmdOptionParser`
+        """
+        if parser is None:
+            parser = self.get_parser()
+        parser.print_help()
+        print """
+Sends a merge request formatted for Bug Goo.  Intended use: get the tree
+you'd like to merge into.  Apply the merges you want.  Invoke request-merge.
+The merge request will open in your $EDITOR.
+
+When no TARGET is specified, it uses the current tree revision.  When
+no MERGE is specified, it uses the direct merges (as in "revisions
+--direct-merges").  But you can specify just the TARGET, or all the MERGE
+revisions.
+"""
+
+    def get_parser(self):
+        """Produce a commandline parser for this command.
+
+        :rtype: `cmdutil.CmdOptionParser`
+        """
+        parser=cmdutil.CmdOptionParser("request-merge [TARGET] [MERGE1...]")
+        return parser
+
+commands = { 
+'changes' : Changes,
+'help' : Help,
+'update': Update,
+'apply-changes':ApplyChanges,
+'cat-log': CatLog,
+'commit': Commit,
+'revision': Revision,
+'revisions': Revisions,
+'get': Get,
+'revert': Revert,
+'shell': Shell,
+'add-id': AddID,
+'merge': Merge,
+'elog': ELog,
+'mirror-archive': MirrorArchive,
+'ninventory': Inventory,
+'alias' : Alias,
+'request-merge': RequestMerge,
+}
+
+def my_import(mod_name):
+    module = __import__(mod_name)
+    components = mod_name.split('.')
+    for comp in components[1:]:
+        module = getattr(module, comp)
+    return module
+
+def plugin(mod_name):
+    module = my_import(mod_name)
+    module.add_command(commands)
+
+for file in os.listdir(sys.path[0]+"/command"):
+    if len(file) > 3 and file[-3:] == ".py" and file != "__init__.py":
+        plugin("command."+file[:-3])
+
+suggestions = {
+'apply-delta' : "Try \"apply-changes\".",
+'delta' : "To compare two revisions, use \"changes\".",
+'diff-rev' : "To compare two revisions, use \"changes\".",
+'undo' : "To undo local changes, use \"revert\".",
+'undelete' : "To undo only deletions, use \"revert --deletions\"",
+'missing-from' : "Try \"revisions --missing-from\".",
+'missing' : "Try \"revisions --missing\".",
+'missing-merge' : "Try \"revisions --partner-missing\".",
+'new-merges' : "Try \"revisions --new-merges\".",
+'cachedrevs' : "Try \"revisions --cacherevs\". (no 'd')",
+'logs' : "Try \"revisions --logs\"",
+'tree-source' : "Use the \"^ttag\" alias (\"revision ^ttag\")",
+'latest-revision' : "Use the \"^acur\" alias (\"revision ^acur\")",
+'change-version' : "Try \"update REVISION\"",
+'tree-revision' : "Use the \"^tcur\" alias (\"revision ^tcur\")",
+'rev-depends' : "Use revisions --dependencies",
+'auto-get' : "Plain get will do archive lookups",
+'tagline' : "Use add-id.  It uses taglines in tagline trees",
+'emlog' : "Use elog.  It automatically adds log-for-merge text, if any",
+'library-revisions' : "Use revisions --library",
+'file-revert' : "Use revert FILE",
+'join-branch' : "Use replay --logs-only"
+}
+# arch-tag: 19d5739d-3708-486c-93ba-deecc3027fc7

*** added file 'testdata/orig'
--- /dev/null 
+++ testdata/orig 
@@ -0,0 +1,2789 @@
+# Copyright (C) 2004 Aaron Bentley
+# <aaron.bentley@utoronto.ca>
+#
+#    This program is free software; you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License as published by
+#    the Free Software Foundation; either version 2 of the License, or
+#    (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    You should have received a copy of the GNU General Public License
+#    along with this program; if not, write to the Free Software
+#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+import sys
+import arch
+import arch.util
+import arch.arch
+import abacmds
+import cmdutil
+import shutil
+import os
+import options
+import paths 
+import time
+import cmd
+import readline
+import re
+import string
+import arch_core
+from errors import *
+import errors
+import terminal
+import ancillary
+import misc
+import email
+import smtplib
+
+__docformat__ = "restructuredtext"
+__doc__ = "Implementation of user (sub) commands"
+commands = {}
+
+def find_command(cmd):
+    """
+    Return an instance of a command type.  Return None if the type isn't
+    registered.
+
+    :param cmd: the name of the command to look for
+    :type cmd: the type of the command
+    """
+    if commands.has_key(cmd):
+        return commands[cmd]()
+    else:
+        return None
+
+class BaseCommand:
+    def __call__(self, cmdline):
+        try:
+            self.do_command(cmdline.split())
+        except cmdutil.GetHelp, e:
+            self.help()
+        except Exception, e:
+            print e
+
+    def get_completer(index):
+        return None
+
+    def complete(self, args, text):
+        """
+        Returns a list of possible completions for the given text.
+
+        :param args: The complete list of arguments
+        :type args: List of str
+        :param text: text to complete (may be shorter than args[-1])
+        :type text: str
+        :rtype: list of str
+        """
+        matches = []
+        candidates = None
+
+        if len(args) > 0: 
+            realtext = args[-1]
+        else:
+            realtext = ""
+
+        try:
+            parser=self.get_parser()
+            if realtext.startswith('-'):
+                candidates = parser.iter_options()
+            else:
+                (options, parsed_args) = parser.parse_args(args)
+
+                if len (parsed_args) > 0:
+                    candidates = self.get_completer(parsed_args[-1], len(parsed_args) -1)
+                else:
+                    candidates = self.get_completer("", 0)
+        except:
+            pass
+        if candidates is None:
+            return
+        for candidate in candidates:
+            candidate = str(candidate)
+            if candidate.startswith(realtext):
+                matches.append(candidate[len(realtext)- len(text):])
+        return matches
+
+
+class Help(BaseCommand):
+    """
+    Lists commands, prints help messages.
+    """
+    def __init__(self):
+        self.description="Prints help mesages"
+        self.parser = None
+
+    def do_command(self, cmdargs):
+        """
+        Prints a help message.
+        """
+        options, args = self.get_parser().parse_args(cmdargs)
+        if len(args) > 1:
+            raise cmdutil.GetHelp
+
+        if options.native or options.suggestions or options.external:
+            native = options.native
+            suggestions = options.suggestions
+            external = options.external
+        else:
+            native = True
+            suggestions = False
+            external = True
+        
+        if len(args) == 0:
+            self.list_commands(native, suggestions, external)
+            return
+        elif len(args) == 1:
+            command_help(args[0])
+            return
+
+    def help(self):
+        self.get_parser().print_help()
+        print """
+If no command is specified, commands are listed.  If a command is
+specified, help for that command is listed.
+        """
+
+    def get_parser(self):
+        """
+        Returns the options parser to use for the "revision" command.
+
+        :rtype: cmdutil.CmdOptionParser
+        """
+        if self.parser is not None:
+            return self.parser
+        parser=cmdutil.CmdOptionParser("fai help [command]")
+        parser.add_option("-n", "--native", action="store_true", 
+                         dest="native", help="Show native commands")
+        parser.add_option("-e", "--external", action="store_true", 
+                         dest="external", help="Show external commands")
+        parser.add_option("-s", "--suggest", action="store_true", 
+                         dest="suggestions", help="Show suggestions")
+        self.parser = parser
+        return parser 
+      
+    def list_commands(self, native=True, suggest=False, external=True):
+        """
+        Lists supported commands.
+
+        :param native: list native, python-based commands
+        :type native: bool
+        :param external: list external aba-style commands
+        :type external: bool
+        """
+        if native:
+            print "Native Fai commands"
+            keys=commands.keys()
+            keys.sort()
+            for k in keys:
+                space=""
+                for i in range(28-len(k)):
+                    space+=" "
+                print space+k+" : "+commands[k]().description
+            print
+        if suggest:
+            print "Unavailable commands and suggested alternatives"
+            key_list = suggestions.keys()
+            key_list.sort()
+            for key in key_list:
+                print "%28s : %s" % (key, suggestions[key])
+            print
+        if external:
+            fake_aba = abacmds.AbaCmds()
+            if (fake_aba.abadir == ""):
+                return
+            print "External commands"
+            fake_aba.list_commands()
+            print
+        if not suggest:
+            print "Use help --suggest to list alternatives to tla and aba"\
+                " commands."
+        if options.tla_fallthrough and (native or external):
+            print "Fai also supports tla commands."
+
+def command_help(cmd):
+    """
+    Prints help for a command.
+
+    :param cmd: The name of the command to print help for
+    :type cmd: str
+    """
+    fake_aba = abacmds.AbaCmds()
+    cmdobj = find_command(cmd)
+    if cmdobj != None:
+        cmdobj.help()
+    elif suggestions.has_key(cmd):
+        print "Not available\n" + suggestions[cmd]
+    else:
+        abacmd = fake_aba.is_command(cmd)
+        if abacmd:
+            abacmd.help()
+        else:
+            print "No help is available for \""+cmd+"\". Maybe try \"tla "+cmd+" -H\"?"
+
+
+
+class Changes(BaseCommand):
+    """
+    the "changes" command: lists differences between trees/revisions:
+    """
+    
+    def __init__(self):
+        self.description="Lists what files have changed in the project tree"
+
+    def get_completer(self, arg, index):
+        if index > 1:
+            return None
+        try:
+            tree = arch.tree_root()
+        except:
+            tree = None
+        return cmdutil.iter_revision_completions(arg, tree)
+    
+    def parse_commandline(self, cmdline):
+        """
+        Parse commandline arguments.  Raises cmdutil.GetHelp if help is needed.
+        
+        :param cmdline: A list of arguments to parse
+        :rtype: (options, Revision, Revision/WorkingTree)
+        """
+        parser=self.get_parser()
+        (options, args) = parser.parse_args(cmdline)
+        if len(args) > 2:
+            raise cmdutil.GetHelp
+
+        tree=arch.tree_root()
+        if len(args) == 0:
+            a_spec = cmdutil.comp_revision(tree)
+        else:
+            a_spec = cmdutil.determine_revision_tree(tree, args[0])
+        cmdutil.ensure_archive_registered(a_spec.archive)
+        if len(args) == 2:
+            b_spec = cmdutil.determine_revision_tree(tree, args[1])
+            cmdutil.ensure_archive_registered(b_spec.archive)
+        else:
+            b_spec=tree
+        return options, a_spec, b_spec
+
+    def do_command(self, cmdargs):
+        """
+        Master function that perfoms the "changes" command.
+        """
+        try:
+            options, a_spec, b_spec = self.parse_commandline(cmdargs);
+        except cmdutil.CantDetermineRevision, e:
+            print e
+            return
+        except arch.errors.TreeRootError, e:
+            print e
+            return
+        if options.changeset:
+            changeset=options.changeset
+            tmpdir = None
+        else:
+            tmpdir=cmdutil.tmpdir()
+            changeset=tmpdir+"/changeset"
+        try:
+            delta=arch.iter_delta(a_spec, b_spec, changeset)
+            try:
+                for line in delta:
+                    if cmdutil.chattermatch(line, "changeset:"):
+                        pass
+                    else:
+                        cmdutil.colorize(line, options.suppress_chatter)
+            except arch.util.ExecProblem, e:
+                if e.proc.error and e.proc.error.startswith(
+                    "missing explicit id for file"):
+                    raise MissingID(e)
+                else:
+                    raise
+            status=delta.status
+            if status > 1:
+                return
+            if (options.perform_diff):
+                chan = cmdutil.ChangesetMunger(changeset)
+                chan.read_indices()
+                if isinstance(b_spec, arch.Revision):
+                    b_dir = b_spec.library_find()
+                else:
+                    b_dir = b_spec
+                a_dir = a_spec.library_find()
+                if options.diffopts is not None:
+                    diffopts = options.diffopts.split()
+                    cmdutil.show_custom_diffs(chan, diffopts, a_dir, b_dir)
+                else:
+                    cmdutil.show_diffs(delta.changeset)
+        finally:
+            if tmpdir and (os.access(tmpdir, os.X_OK)):
+                shutil.rmtree(tmpdir)
+
+    def get_parser(self):
+        """
+        Returns the options parser to use for the "changes" command.
+
+        :rtype: cmdutil.CmdOptionParser
+        """
+        parser=cmdutil.CmdOptionParser("fai changes [options] [revision]"
+                                       " [revision]")
+        parser.add_option("-d", "--diff", action="store_true", 
+                          dest="perform_diff", default=False, 
+                          help="Show diffs in summary")
+        parser.add_option("-c", "--changeset", dest="changeset", 
+                          help="Store a changeset in the given directory", 
+                          metavar="DIRECTORY")
+        parser.add_option("-s", "--silent", action="store_true", 
+                          dest="suppress_chatter", default=False, 
+                          help="Suppress chatter messages")
+        parser.add_option("--diffopts", dest="diffopts", 
+                          help="Use the specified diff options", 
+                          metavar="OPTIONS")
+
+        return parser
+
+    def help(self, parser=None):
+        """
+        Prints a help message.
+
+        :param parser: If supplied, the parser to use for generating help.  If \
+        not supplied, it is retrieved.
+        :type parser: cmdutil.CmdOptionParser
+        """
+        if parser is None:
+            parser=self.get_parser()
+        parser.print_help()
+        print """
+Performs source-tree comparisons
+
+If no revision is specified, the current project tree is compared to the
+last-committed revision.  If one revision is specified, the current project
+tree is compared to that revision.  If two revisions are specified, they are
+compared to each other.
+        """
+        help_tree_spec() 
+        return
+
+
+class ApplyChanges(BaseCommand):
+    """
+    Apply differences between two revisions to a tree
+    """
+    
+    def __init__(self):
+        self.description="Applies changes to a project tree"
+    
+    def get_completer(self, arg, index):
+        if index > 1:
+            return None
+        try:
+            tree = arch.tree_root()
+        except:
+            tree = None
+        return cmdutil.iter_revision_completions(arg, tree)
+
+    def parse_commandline(self, cmdline, tree):
+        """
+        Parse commandline arguments.  Raises cmdutil.GetHelp if help is needed.
+        
+        :param cmdline: A list of arguments to parse
+        :rtype: (options, Revision, Revision/WorkingTree)
+        """
+        parser=self.get_parser()
+        (options, args) = parser.parse_args(cmdline)
+        if len(args) != 2:
+            raise cmdutil.GetHelp
+
+        a_spec = cmdutil.determine_revision_tree(tree, args[0])
+        cmdutil.ensure_archive_registered(a_spec.archive)
+        b_spec = cmdutil.determine_revision_tree(tree, args[1])
+        cmdutil.ensure_archive_registered(b_spec.archive)
+        return options, a_spec, b_spec
+
+    def do_command(self, cmdargs):
+        """
+        Master function that performs "apply-changes".
+        """
+        try:
+            tree = arch.tree_root()
+            options, a_spec, b_spec = self.parse_commandline(cmdargs, tree);
+        except cmdutil.CantDetermineRevision, e:
+            print e
+            return
+        except arch.errors.TreeRootError, e:
+            print e
+            return
+        delta=cmdutil.apply_delta(a_spec, b_spec, tree)
+        for line in cmdutil.iter_apply_delta_filter(delta):
+            cmdutil.colorize(line, options.suppress_chatter)
+
+    def get_parser(self):
+        """
+        Returns the options parser to use for the "apply-changes" command.
+
+        :rtype: cmdutil.CmdOptionParser
+        """
+        parser=cmdutil.CmdOptionParser("fai apply-changes [options] revision"
+                                       " revision")
+        parser.add_option("-d", "--diff", action="store_true", 
+                          dest="perform_diff", default=False, 
+                          help="Show diffs in summary")
+        parser.add_option("-c", "--changeset", dest="changeset", 
+                          help="Store a changeset in the given directory", 
+                          metavar="DIRECTORY")
+        parser.add_option("-s", "--silent", action="store_true", 
+                          dest="suppress_chatter", default=False, 
+                          help="Suppress chatter messages")
+        return parser
+
+    def help(self, parser=None):
+        """
+        Prints a help message.
+
+        :param parser: If supplied, the parser to use for generating help.  If \
+        not supplied, it is retrieved.
+        :type parser: cmdutil.CmdOptionParser
+        """
+        if parser is None:
+            parser=self.get_parser()
+        parser.print_help()
+        print """
+Applies changes to a project tree
+
+Compares two revisions and applies the difference between them to the current
+tree.
+        """
+        help_tree_spec() 
+        return
+
+class Update(BaseCommand):
+    """
+    Updates a project tree to a given revision, preserving un-committed hanges. 
+    """
+    
+    def __init__(self):
+        self.description="Apply the latest changes to the current directory"
+
+    def get_completer(self, arg, index):
+        if index > 0:
+            return None
+        try:
+            tree = arch.tree_root()
+        except:
+            tree = None
+        return cmdutil.iter_revision_completions(arg, tree)
+    
+    def parse_commandline(self, cmdline, tree):
+        """
+        Parse commandline arguments.  Raises cmdutil.GetHelp if help is needed.
+        
+        :param cmdline: A list of arguments to parse
+        :rtype: (options, Revision, Revision/WorkingTree)
+        """
+        parser=self.get_parser()
+        (options, args) = parser.parse_args(cmdline)
+        if len(args) > 2:
+            raise cmdutil.GetHelp
+
+        spec=None
+        if len(args)>0:
+            spec=args[0]
+        revision=cmdutil.determine_revision_arch(tree, spec)
+        cmdutil.ensure_archive_registered(revision.archive)
+
+        mirror_source = cmdutil.get_mirror_source(revision.archive)
+        if mirror_source != None:
+            if cmdutil.prompt("Mirror update"):
+                cmd=cmdutil.mirror_archive(mirror_source, 
+                    revision.archive, arch.NameParser(revision).get_package_version())
+                for line in arch.chatter_classifier(cmd):
+                    cmdutil.colorize(line, options.suppress_chatter)
+
+                revision=cmdutil.determine_revision_arch(tree, spec)
+
+        return options, revision 
+
+    def do_command(self, cmdargs):
+        """
+        Master function that perfoms the "update" command.
+        """
+        tree=arch.tree_root()
+        try:
+            options, to_revision = self.parse_commandline(cmdargs, tree);
+        except cmdutil.CantDetermineRevision, e:
+            print e
+            return
+        except arch.errors.TreeRootError, e:
+            print e
+            return
+        from_revision=cmdutil.tree_latest(tree)
+        if from_revision==to_revision:
+            print "Tree is already up to date with:\n"+str(to_revision)+"."
+            return
+        cmdutil.ensure_archive_registered(from_revision.archive)
+        cmd=cmdutil.apply_delta(from_revision, to_revision, tree,
+            options.patch_forward)
+        for line in cmdutil.iter_apply_delta_filter(cmd):
+            cmdutil.colorize(line)
+        if to_revision.version != tree.tree_version:
+            if cmdutil.prompt("Update version"):
+                tree.tree_version = to_revision.version
+
+    def get_parser(self):
+        """
+        Returns the options parser to use for the "update" command.
+
+        :rtype: cmdutil.CmdOptionParser
+        """
+        parser=cmdutil.CmdOptionParser("fai update [options]"
+                                       " [revision/version]")
+        parser.add_option("-f", "--forward", action="store_true", 
+                          dest="patch_forward", default=False, 
+                          help="pass the --forward option to 'patch'")
+        parser.add_option("-s", "--silent", action="store_true", 
+                          dest="suppress_chatter", default=False, 
+                          help="Suppress chatter messages")
+        return parser
+
+    def help(self, parser=None):
+        """
+        Prints a help message.
+
+        :param parser: If supplied, the parser to use for generating help.  If \
+        not supplied, it is retrieved.
+        :type parser: cmdutil.CmdOptionParser
+        """
+        if parser is None:
+            parser=self.get_parser()
+        parser.print_help()
+        print """
+Updates a working tree to the current archive revision
+
+If a revision or version is specified, that is used instead 
+        """
+        help_tree_spec() 
+        return
+
+
+class Commit(BaseCommand):
+    """
+    Create a revision based on the changes in the current tree.
+    """
+    
+    def __init__(self):
+        self.description="Write local changes to the archive"
+
+    def get_completer(self, arg, index):
+        if arg is None:
+            arg = ""
+        return iter_modified_file_completions(arch.tree_root(), arg)
+#        return iter_source_file_completions(arch.tree_root(), arg)
+    
+    def parse_commandline(self, cmdline, tree):
+        """
+        Parse commandline arguments.  Raise cmtutil.GetHelp if help is needed.
+        
+        :param cmdline: A list of arguments to parse
+        :rtype: (options, Revision, Revision/WorkingTree)
+        """
+        parser=self.get_parser()
+        (options, args) = parser.parse_args(cmdline)
+
+        if len(args) == 0:
+            args = None
+        revision=cmdutil.determine_revision_arch(tree, options.version)
+        return options, revision.get_version(), args
+
+    def do_command(self, cmdargs):
+        """
+        Master function that perfoms the "commit" command.
+        """
+        tree=arch.tree_root()
+        options, version, files = self.parse_commandline(cmdargs, tree)
+        if options.__dict__.has_key("base") and options.base:
+            base = cmdutil.determine_revision_tree(tree, options.base)
+        else:
+            base = cmdutil.submit_revision(tree)
+        
+        writeversion=version
+        archive=version.archive
+        source=cmdutil.get_mirror_source(archive)
+        allow_old=False
+        writethrough="implicit"
+
+        if source!=None:
+            if writethrough=="explicit" and \
+                cmdutil.prompt("Writethrough"):
+                writeversion=arch.Version(str(source)+"/"+str(version.get_nonarch()))
+            elif writethrough=="none":
+                raise CommitToMirror(archive)
+
+        elif archive.is_mirror:
+            raise CommitToMirror(archive)
+
+        try:
+            last_revision=tree.iter_logs(version, True).next().revision
+        except StopIteration, e:
+            if cmdutil.prompt("Import from commit"):
+                return do_import(version)
+            else:
+                raise NoVersionLogs(version)
+        if last_revision!=version.iter_revisions(True).next():
+            if not cmdutil.prompt("Out of date"):
+                raise OutOfDate
+            else:
+                allow_old=True
+
+        try:
+            if not cmdutil.has_changed(version):
+                if not cmdutil.prompt("Empty commit"):
+                    raise EmptyCommit
+        except arch.util.ExecProblem, e:
+            if e.proc.error and e.proc.error.startswith(
+                "missing explicit id for file"):
+                raise MissingID(e)
+            else:
+                raise
+        log = tree.log_message(create=False)
+        if log is None:
+            try:
+                if cmdutil.prompt("Create log"):
+                    edit_log(tree)
+
+            except cmdutil.NoEditorSpecified, e:
+                raise CommandFailed(e)
+            log = tree.log_message(create=False)
+        if log is None: 
+            raise NoLogMessage
+        if log["Summary"] is None or len(log["Summary"].strip()) == 0:
+            if not cmdutil.prompt("Omit log summary"):
+                raise errors.NoLogSummary
+        try:
+            for line in tree.iter_commit(version, seal=options.seal_version,
+                base=base, out_of_date_ok=allow_old, file_list=files):
+                cmdutil.colorize(line, options.suppress_chatter)
+
+        except arch.util.ExecProblem, e:
+            if e.proc.error and e.proc.error.startswith(
+                "These files violate naming conventions:"):
+                raise LintFailure(e.proc.error)
+            else:
+                raise
+
+    def get_parser(self):
+        """
+        Returns the options parser to use for the "commit" command.
+
+        :rtype: cmdutil.CmdOptionParser
+        """
+
+        parser=cmdutil.CmdOptionParser("fai commit [options] [file1]"
+                                       " [file2...]")
+        parser.add_option("--seal", action="store_true", 
+                          dest="seal_version", default=False, 
+                          help="seal this version")
+        parser.add_option("-v", "--version", dest="version", 
+                          help="Use the specified version", 
+                          metavar="VERSION")
+        parser.add_option("-s", "--silent", action="store_true", 
+                          dest="suppress_chatter", default=False, 
+                          help="Suppress chatter messages")
+        if cmdutil.supports_switch("commit", "--base"):
+            parser.add_option("--base", dest="base", help="", 
+                              metavar="REVISION")
+        return parser
+
+    def help(self, parser=None):
+        """
+        Prints a help message.
+
+        :param parser: If supplied, the parser to use for generating help.  If \
+        not supplied, it is retrieved.
+        :type parser: cmdutil.CmdOptionParser
+        """
+        if parser is None:
+            parser=self.get_parser()
+        parser.print_help()
+        print """
+Updates a working tree to the current archive revision
+
+If a version is specified, that is used instead 
+        """
+#        help_tree_spec() 
+        return
+
+
+
+class CatLog(BaseCommand):
+    """
+    Print the log of a given file (from current tree)
+    """
+    def __init__(self):
+        self.description="Prints the patch log for a revision"
+
+    def get_completer(self, arg, index):
+        if index > 0:
+            return None
+        try:
+            tree = arch.tree_root()
+        except:
+            tree = None
+        return cmdutil.iter_revision_completions(arg, tree)
+
+    def do_command(self, cmdargs):
+        """
+        Master function that perfoms the "cat-log" command.
+        """
+        parser=self.get_parser()
+        (options, args) = parser.parse_args(cmdargs)
+        try:
+            tree = arch.tree_root()
+        except arch.errors.TreeRootError, e:
+            tree = None
+        spec=None
+        if len(args) > 0:
+            spec=args[0]
+        if len(args) > 1:
+            raise cmdutil.GetHelp()
+        try:
+            if tree:
+                revision = cmdutil.determine_revision_tree(tree, spec)
+            else:
+                revision = cmdutil.determine_revision_arch(tree, spec)
+        except cmdutil.CantDetermineRevision, e:
+            raise CommandFailedWrapper(e)
+        log = None
+        
+        use_tree = (options.source == "tree" or \
+            (options.source == "any" and tree))
+        use_arch = (options.source == "archive" or options.source == "any")
+        
+        log = None
+        if use_tree:
+            for log in tree.iter_logs(revision.get_version()):
+                if log.revision == revision:
+                    break
+                else:
+                    log = None
+        if log is None and use_arch:
+            cmdutil.ensure_revision_exists(revision)
+            log = arch.Patchlog(revision)
+        if log is not None:
+            for item in log.items():
+                print "%s: %s" % item
+            print log.description
+
+    def get_parser(self):
+        """
+        Returns the options parser to use for the "cat-log" command.
+
+        :rtype: cmdutil.CmdOptionParser
+        """
+        parser=cmdutil.CmdOptionParser("fai cat-log [revision]")
+        parser.add_option("--archive", action="store_const", dest="source",
+                          const="archive", default="any",
+                          help="Always get the log from the archive")
+        parser.add_option("--tree", action="store_const", dest="source",
+                          const="tree", help="Always get the log from the tree")
+        return parser 
+
+    def help(self, parser=None):
+        """
+        Prints a help message.
+
+        :param parser: If supplied, the parser to use for generating help.  If \
+        not supplied, it is retrieved.
+        :type parser: cmdutil.CmdOptionParser
+        """
+        if parser==None:
+            parser=self.get_parser()
+        parser.print_help()
+        print """
+Prints the log for the specified revision
+        """
+        help_tree_spec()
+        return
+
+class Revert(BaseCommand):
+    """ Reverts a tree (or aspects of it) to a revision
+    """
+    def __init__(self):
+        self.description="Reverts a tree (or aspects of it) to a revision "
+
+    def get_completer(self, arg, index):
+        if index > 0:
+            return None
+        try:
+            tree = arch.tree_root()
+        except:
+            tree = None
+        return iter_modified_file_completions(tree, arg)
+
+    def do_command(self, cmdargs):
+        """
+        Master function that perfoms the "revert" command.
+        """
+        parser=self.get_parser()
+        (options, args) = parser.parse_args(cmdargs)
+        try:
+            tree = arch.tree_root()
+        except arch.errors.TreeRootError, e:
+            raise CommandFailed(e)
+        spec=None
+        if options.revision is not None:
+            spec=options.revision
+        try:
+            if spec is not None:
+                revision = cmdutil.determine_revision_tree(tree, spec)
+            else:
+                revision = cmdutil.comp_revision(tree)
+        except cmdutil.CantDetermineRevision, e:
+            raise CommandFailedWrapper(e)
+        munger = None
+
+        if options.file_contents or options.file_perms or options.deletions\
+            or options.additions or options.renames or options.hunk_prompt:
+            munger = cmdutil.MungeOpts()
+            munger.hunk_prompt = options.hunk_prompt
+
+        if len(args) > 0 or options.logs or options.pattern_files or \
+            options.control:
+            if munger is None:
+                munger = cmdutil.MungeOpts(True)
+                munger.all_types(True)
+        if len(args) > 0:
+            t_cwd = cmdutil.tree_cwd(tree)
+            for name in args:
+                if len(t_cwd) > 0:
+                    t_cwd += "/"
+                name = "./" + t_cwd + name
+                munger.add_keep_file(name);
+
+        if options.file_perms:
+            munger.file_perms = True
+        if options.file_contents:
+            munger.file_contents = True
+        if options.deletions:
+            munger.deletions = True
+        if options.additions:
+            munger.additions = True
+        if options.renames:
+            munger.renames = True
+        if options.logs:
+            munger.add_keep_pattern('^\./\{arch\}/[^=].*')
+        if options.control:
+            munger.add_keep_pattern("/\.arch-ids|^\./\{arch\}|"\
+                                    "/\.arch-inventory$")
+        if options.pattern_files:
+            munger.add_keep_pattern(options.pattern_files)
+                
+        for line in cmdutil.revert(tree, revision, munger, 
+                                   not options.no_output):
+            cmdutil.colorize(line)
+
+
+    def get_parser(self):
+        """
+        Returns the options parser to use for the "cat-log" command.
+
+        :rtype: cmdutil.CmdOptionParser
+        """
+        parser=cmdutil.CmdOptionParser("fai revert [options] [FILE...]")
+        parser.add_option("", "--contents", action="store_true", 
+                          dest="file_contents", 
+                          help="Revert file content changes")
+        parser.add_option("", "--permissions", action="store_true", 
+                          dest="file_perms", 
+                          help="Revert file permissions changes")
+        parser.add_option("", "--deletions", action="store_true", 
+                          dest="deletions", 
+                          help="Restore deleted files")
+        parser.add_option("", "--additions", action="store_true", 
+                          dest="additions", 
+                          help="Remove added files")
+        parser.add_option("", "--renames", action="store_true", 
+                          dest="renames", 
+                          help="Revert file names")
+        parser.add_option("--hunks", action="store_true", 
+                          dest="hunk_prompt", default=False,
+                          help="Prompt which hunks to revert")
+        parser.add_option("--pattern-files", dest="pattern_files", 
+                          help="Revert files that match this pattern", 
+                          metavar="REGEX")
+        parser.add_option("--logs", action="store_true", 
+                          dest="logs", default=False,
+                          help="Revert only logs")
+        parser.add_option("--control-files", action="store_true", 
+                          dest="control", default=False,
+                          help="Revert logs and other control files")
+        parser.add_option("-n", "--no-output", action="store_true", 
+                          dest="no_output", 
+                          help="Don't keep an undo changeset")
+        parser.add_option("--revision", dest="revision", 
+                          help="Revert to the specified revision", 
+                          metavar="REVISION")
+        return parser 
+
+    def help(self, parser=None):
+        """
+        Prints a help message.
+
+        :param parser: If supplied, the parser to use for generating help.  If \
+        not supplied, it is retrieved.
+        :type parser: cmdutil.CmdOptionParser
+        """
+        if parser==None:
+            parser=self.get_parser()
+        parser.print_help()
+        print """
+Reverts changes in the current working tree.  If no flags are specified, all
+types of changes are reverted.  Otherwise, only selected types of changes are
+reverted.  
+
+If a revision is specified on the commandline, differences between the current
+tree and that revision are reverted.  If a version is specified, the current
+tree is used to determine the revision.
+
+If files are specified, only those files listed will have any changes applied.
+To specify a renamed file, you can use either the old or new name. (or both!)
+
+Unless "-n" is specified, reversions can be undone with "redo".
+        """
+        return
+
+class Revision(BaseCommand):
+    """
+    Print a revision name based on a revision specifier
+    """
+    def __init__(self):
+        self.description="Prints the name of a revision"
+
+    def get_completer(self, arg, index):
+        if index > 0:
+            return None
+        try:
+            tree = arch.tree_root()
+        except:
+            tree = None
+        return cmdutil.iter_revision_completions(arg, tree)
+
+    def do_command(self, cmdargs):
+        """
+        Master function that perfoms the "revision" command.
+        """
+        parser=self.get_parser()
+        (options, args) = parser.parse_args(cmdargs)
+
+        try:
+            tree = arch.tree_root()
+        except arch.errors.TreeRootError:
+            tree = None
+
+        spec=None
+        if len(args) > 0:
+            spec=args[0]
+        if len(args) > 1:
+            raise cmdutil.GetHelp
+        try:
+            if tree:
+                revision = cmdutil.determine_revision_tree(tree, spec)
+            else:
+                revision = cmdutil.determine_revision_arch(tree, spec)
+        except cmdutil.CantDetermineRevision, e:
+            print str(e)
+            return
+        print options.display(revision)
+
+    def get_parser(self):
+        """
+        Returns the options parser to use for the "revision" command.
+
+        :rtype: cmdutil.CmdOptionParser
+        """
+        parser=cmdutil.CmdOptionParser("fai revision [revision]")
+        parser.add_option("", "--location", action="store_const", 
+                         const=paths.determine_path, dest="display", 
+                         help="Show location instead of name", default=str)
+        parser.add_option("--import", action="store_const", 
+                         const=paths.determine_import_path, dest="display",  
+                         help="Show location of import file")
+        parser.add_option("--log", action="store_const", 
+                         const=paths.determine_log_path, dest="display", 
+                         help="Show location of log file")
+        parser.add_option("--patch", action="store_const", 
+                         dest="display", const=paths.determine_patch_path,
+                         help="Show location of patchfile")
+        parser.add_option("--continuation", action="store_const", 
+                         const=paths.determine_continuation_path, 
+                         dest="display",
+                         help="Show location of continuation file")
+        parser.add_option("--cacherev", action="store_const", 
+                         const=paths.determine_cacherev_path, dest="display",
+                         help="Show location of cacherev file")
+        return parser 
+
+    def help(self, parser=None):
+        """
+        Prints a help message.
+
+        :param parser: If supplied, the parser to use for generating help.  If \
+        not supplied, it is retrieved.
+        :type parser: cmdutil.CmdOptionParser
+        """
+        if parser==None:
+            parser=self.get_parser()
+        parser.print_help()
+        print """
+Expands aliases and prints the name of the specified revision.  Instead of
+the name, several options can be used to print locations.  If more than one is
+specified, the last one is used.
+        """
+        help_tree_spec()
+        return
+
+def require_version_exists(version, spec):
+    if not version.exists():
+        raise cmdutil.CantDetermineVersion(spec, 
+                                           "The version %s does not exist." \
+                                           % version)
+
+class Revisions(BaseCommand):
+    """
+    Print a revision name based on a revision specifier
+    """
+    def __init__(self):
+        self.description="Lists revisions"
+    
+    def do_command(self, cmdargs):
+        """
+        Master function that perfoms the "revision" command.
+        """
+        (options, args) = self.get_parser().parse_args(cmdargs)
+        if len(args) > 1:
+            raise cmdutil.GetHelp
+        try:
+            self.tree = arch.tree_root()
+        except arch.errors.TreeRootError:
+            self.tree = None
+        try:
+            iter = self.get_iterator(options.type, args, options.reverse, 
+                                     options.modified)
+        except cmdutil.CantDetermineRevision, e:
+            raise CommandFailedWrapper(e)
+
+        if options.skip is not None:
+            iter = cmdutil.iter_skip(iter, int(options.skip))
+
+        for revision in iter:
+            log = None
+            if isinstance(revision, arch.Patchlog):
+                log = revision
+                revision=revision.revision
+            print options.display(revision)
+            if log is None and (options.summary or options.creator or 
+                                options.date or options.merges):
+                log = revision.patchlog
+            if options.creator:
+                print "    %s" % log.creator
+            if options.date:
+                print "    %s" % time.strftime('%Y-%m-%d %H:%M:%S %Z', log.date)
+            if options.summary:
+                print "    %s" % log.summary
+            if options.merges:
+                showed_title = False
+                for revision in log.merged_patches:
+                    if not showed_title:
+                        print "    Merged:"
+                        showed_title = True
+                    print "    %s" % revision
+
+    def get_iterator(self, type, args, reverse, modified):
+        if len(args) > 0:
+            spec = args[0]
+        else:
+            spec = None
+        if modified is not None:
+            iter = cmdutil.modified_iter(modified, self.tree)
+            if reverse:
+                return iter
+            else:
+                return cmdutil.iter_reverse(iter)
+        elif type == "archive":
+            if spec is None:
+                if self.tree is None:
+                    raise cmdutil.CantDetermineRevision("", 
+                                                        "Not in a project tree")
+                version = cmdutil.determine_version_tree(spec, self.tree)
+            else:
+                version = cmdutil.determine_version_arch(spec, self.tree)
+                cmdutil.ensure_archive_registered(version.archive)
+                require_version_exists(version, spec)
+            return version.iter_revisions(reverse)
+        elif type == "cacherevs":
+            if spec is None:
+                if self.tree is None:
+                    raise cmdutil.CantDetermineRevision("", 
+                                                        "Not in a project tree")
+                version = cmdutil.determine_version_tree(spec, self.tree)
+            else:
+                version = cmdutil.determine_version_arch(spec, self.tree)
+                cmdutil.ensure_archive_registered(version.archive)
+                require_version_exists(version, spec)
+            return cmdutil.iter_cacherevs(version, reverse)
+        elif type == "library":
+            if spec is None:
+                if self.tree is None:
+                    raise cmdutil.CantDetermineRevision("", 
+                                                        "Not in a project tree")
+                version = cmdutil.determine_version_tree(spec, self.tree)
+            else:
+                version = cmdutil.determine_version_arch(spec, self.tree)
+            return version.iter_library_revisions(reverse)
+        elif type == "logs":
+            if self.tree is None:
+                raise cmdutil.CantDetermineRevision("", "Not in a project tree")
+            return self.tree.iter_logs(cmdutil.determine_version_tree(spec, \
+                                  self.tree), reverse)
+        elif type == "missing" or type == "skip-present":
+            if self.tree is None:
+                raise cmdutil.CantDetermineRevision("", "Not in a project tree")
+            skip = (type == "skip-present")
+            version = cmdutil.determine_version_tree(spec, self.tree)
+            cmdutil.ensure_archive_registered(version.archive)
+            require_version_exists(version, spec)
+            return cmdutil.iter_missing(self.tree, version, reverse,
+                                        skip_present=skip)
+
+        elif type == "present":
+            if self.tree is None:
+                raise cmdutil.CantDetermineRevision("", "Not in a project tree")
+            version = cmdutil.determine_version_tree(spec, self.tree)
+            cmdutil.ensure_archive_registered(version.archive)
+            require_version_exists(version, spec)
+            return cmdutil.iter_present(self.tree, version, reverse)
+
+        elif type == "new-merges" or type == "direct-merges":
+            if self.tree is None:
+                raise cmdutil.CantDetermineRevision("", "Not in a project tree")
+            version = cmdutil.determine_version_tree(spec, self.tree)
+            cmdutil.ensure_archive_registered(version.archive)
+            require_version_exists(version, spec)
+            iter = cmdutil.iter_new_merges(self.tree, version, reverse)
+            if type == "new-merges":
+                return iter
+            elif type == "direct-merges":
+                return cmdutil.direct_merges(iter)
+
+        elif type == "missing-from":
+            if self.tree is None:
+                raise cmdutil.CantDetermineRevision("", "Not in a project tree")
+            revision = cmdutil.determine_revision_tree(self.tree, spec)
+            libtree = cmdutil.find_or_make_local_revision(revision)
+            return cmdutil.iter_missing(libtree, self.tree.tree_version,
+                                        reverse)
+
+        elif type == "partner-missing":
+            return cmdutil.iter_partner_missing(self.tree, reverse)
+
+        elif type == "ancestry":
+            revision = cmdutil.determine_revision_tree(self.tree, spec)
+            iter = cmdutil._iter_ancestry(self.tree, revision)
+            if reverse:
+                return iter
+            else:
+                return cmdutil.iter_reverse(iter)
+
+        elif type == "dependencies" or type == "non-dependencies":
+            nondeps = (type == "non-dependencies")
+            revision = cmdutil.determine_revision_tree(self.tree, spec)
+            anc_iter = cmdutil._iter_ancestry(self.tree, revision)
+            iter_depends = cmdutil.iter_depends(anc_iter, nondeps)
+            if reverse:
+                return iter_depends
+            else:
+                return cmdutil.iter_reverse(iter_depends)
+        elif type == "micro":
+            return cmdutil.iter_micro(self.tree)
+
+    
+    def get_parser(self):
+        """
+        Returns the options parser to use for the "revision" command.
+
+        :rtype: cmdutil.CmdOptionParser
+        """
+        parser=cmdutil.CmdOptionParser("fai revisions [revision]")
+        select = cmdutil.OptionGroup(parser, "Selection options",
+                          "Control which revisions are listed.  These options"
+                          " are mutually exclusive.  If more than one is"
+                          " specified, the last is used.")
+        select.add_option("", "--archive", action="store_const", 
+                          const="archive", dest="type", default="archive",
+                          help="List all revisions in the archive")
+        select.add_option("", "--cacherevs", action="store_const", 
+                          const="cacherevs", dest="type",
+                          help="List all revisions stored in the archive as "
+                          "complete copies")
+        select.add_option("", "--logs", action="store_const", 
+                          const="logs", dest="type",
+                          help="List revisions that have a patchlog in the "
+                          "tree")
+        select.add_option("", "--missing", action="store_const", 
+                          const="missing", dest="type",
+                          help="List revisions from the specified version that"
+                          " have no patchlog in the tree")
+        select.add_option("", "--skip-present", action="store_const", 
+                          const="skip-present", dest="type",
+                          help="List revisions from the specified version that"
+                          " have no patchlogs at all in the tree")
+        select.add_option("", "--present", action="store_const", 
+                          const="present", dest="type",
+                          help="List revisions from the specified version that"
+                          " have no patchlog in the tree, but can't be merged")
+        select.add_option("", "--missing-from", action="store_const", 
+                          const="missing-from", dest="type",
+                          help="List revisions from the specified revision "
+                          "that have no patchlog for the tree version")
+        select.add_option("", "--partner-missing", action="store_const", 
+                          const="partner-missing", dest="type",
+                          help="List revisions in partner versions that are"
+                          " missing")
+        select.add_option("", "--new-merges", action="store_const", 
+                          const="new-merges", dest="type",
+                          help="List revisions that have had patchlogs added"
+                          " to the tree since the last commit")
+        select.add_option("", "--direct-merges", action="store_const", 
+                          const="direct-merges", dest="type",
+                          help="List revisions that have been directly added"
+                          " to tree since the last commit ")
+        select.add_option("", "--library", action="store_const", 
+                          const="library", dest="type",
+                          help="List revisions in the revision library")
+        select.add_option("", "--ancestry", action="store_const", 
+                          const="ancestry", dest="type",
+                          help="List revisions that are ancestors of the "
+                          "current tree version")
+
+        select.add_option("", "--dependencies", action="store_const", 
+                          const="dependencies", dest="type",
+                          help="List revisions that the given revision "
+                          "depends on")
+
+        select.add_option("", "--non-dependencies", action="store_const", 
+                          const="non-dependencies", dest="type",
+                          help="List revisions that the given revision "
+                          "does not depend on")
+
+        select.add_option("--micro", action="store_const", 
+                          const="micro", dest="type",
+                          help="List partner revisions aimed for this "
+                          "micro-branch")
+
+        select.add_option("", "--modified", dest="modified", 
+                          help="List tree ancestor revisions that modified a "
+                          "given file", metavar="FILE[:LINE]")
+
+        parser.add_option("", "--skip", dest="skip", 
+                          help="Skip revisions.  Positive numbers skip from "
+                          "beginning, negative skip from end.",
+                          metavar="NUMBER")
+
+        parser.add_option_group(select)
+
+        format = cmdutil.OptionGroup(parser, "Revision format options",
+                          "These control the appearance of listed revisions")
+        format.add_option("", "--location", action="store_const", 
+                         const=paths.determine_path, dest="display", 
+                         help="Show location instead of name", default=str)
+        format.add_option("--import", action="store_const", 
+                         const=paths.determine_import_path, dest="display",  
+                         help="Show location of import file")
+        format.add_option("--log", action="store_const", 
+                         const=paths.determine_log_path, dest="display", 
+                         help="Show location of log file")
+        format.add_option("--patch", action="store_const", 
+                         dest="display", const=paths.determine_patch_path,
+                         help="Show location of patchfile")
+        format.add_option("--continuation", action="store_const", 
+                         const=paths.determine_continuation_path, 
+                         dest="display",
+                         help="Show location of continuation file")
+        format.add_option("--cacherev", action="store_const", 
+                         const=paths.determine_cacherev_path, dest="display",
+                         help="Show location of cacherev file")
+        parser.add_option_group(format)
+        display = cmdutil.OptionGroup(parser, "Display format options",
+                          "These control the display of data")
+        display.add_option("-r", "--reverse", action="store_true", 
+                          dest="reverse", help="Sort from newest to oldest")
+        display.add_option("-s", "--summary", action="store_true", 
+                          dest="summary", help="Show patchlog summary")
+        display.add_option("-D", "--date", action="store_true", 
+                          dest="date", help="Show patchlog date")
+        display.add_option("-c", "--creator", action="store_true", 
+                          dest="creator", help="Show the id that committed the"
+                          " revision")
+        display.add_option("-m", "--merges", action="store_true", 
+                          dest="merges", help="Show the revisions that were"
+                          " merged")
+        parser.add_option_group(display)
+        return parser 
+    def help(self, parser=None):
+        """Attempt to explain the revisions command
+        
+        :param parser: If supplied, used to determine options
+        """
+        if parser==None:
+            parser=self.get_parser()
+        parser.print_help()
+        print """List revisions.
+        """
+        help_tree_spec()
+
+
+class Get(BaseCommand):
+    """
+    Retrieve a revision from the archive
+    """
+    def __init__(self):
+        self.description="Retrieve a revision from the archive"
+        self.parser=self.get_parser()
+
+
+    def get_completer(self, arg, index):
+        if index > 0:
+            return None
+        try:
+            tree = arch.tree_root()
+        except:
+            tree = None
+        return cmdutil.iter_revision_completions(arg, tree)
+
+
+    def do_command(self, cmdargs):
+        """
+        Master function that perfoms the "get" command.
+        """
+        (options, args) = self.parser.parse_args(cmdargs)
+        if len(args) < 1:
+            return self.help()            
+        try:
+            tree = arch.tree_root()
+        except arch.errors.TreeRootError:
+            tree = None
+        
+        arch_loc = None
+        try:
+            revision, arch_loc = paths.full_path_decode(args[0])
+        except Exception, e:
+            revision = cmdutil.determine_revision_arch(tree, args[0], 
+                check_existence=False, allow_package=True)
+        if len(args) > 1:
+            directory = args[1]
+        else:
+            directory = str(revision.nonarch)
+        if os.path.exists(directory):
+            raise DirectoryExists(directory)
+        cmdutil.ensure_archive_registered(revision.archive, arch_loc)
+        try:
+            cmdutil.ensure_revision_exists(revision)
+        except cmdutil.NoSuchRevision, e:
+            raise CommandFailedWrapper(e)
+
+        link = cmdutil.prompt ("get link")
+        for line in cmdutil.iter_get(revision, directory, link,
+                                     options.no_pristine,
+                                     options.no_greedy_add):
+            cmdutil.colorize(line)
+
+    def get_parser(self):
+        """
+        Returns the options parser to use for the "get" command.
+
+        :rtype: cmdutil.CmdOptionParser
+        """
+        parser=cmdutil.CmdOptionParser("fai get revision [dir]")
+        parser.add_option("--no-pristine", action="store_true", 
+                         dest="no_pristine", 
+                         help="Do not make pristine copy for reference")
+        parser.add_option("--no-greedy-add", action="store_true", 
+                         dest="no_greedy_add", 
+                         help="Never add to greedy libraries")
+
+        return parser 
+
+    def help(self, parser=None):
+        """
+        Prints a help message.
+
+        :param parser: If supplied, the parser to use for generating help.  If \
+        not supplied, it is retrieved.
+        :type parser: cmdutil.CmdOptionParser
+        """
+        if parser==None:
+            parser=self.get_parser()
+        parser.print_help()
+        print """
+Expands aliases and constructs a project tree for a revision.  If the optional
+"dir" argument is provided, the project tree will be stored in this directory.
+        """
+        help_tree_spec()
+        return
+
+class PromptCmd(cmd.Cmd):
+    def __init__(self):
+        cmd.Cmd.__init__(self)
+        self.prompt = "Fai> "
+        try:
+            self.tree = arch.tree_root()
+        except:
+            self.tree = None
+        self.set_title()
+        self.set_prompt()
+        self.fake_aba = abacmds.AbaCmds()
+        self.identchars += '-'
+        self.history_file = os.path.expanduser("~/.fai-history")
+        readline.set_completer_delims(string.whitespace)
+        if os.access(self.history_file, os.R_OK) and \
+            os.path.isfile(self.history_file):
+            readline.read_history_file(self.history_file)
+
+    def write_history(self):
+        readline.write_history_file(self.history_file)
+
+    def do_quit(self, args):
+        self.write_history()
+        sys.exit(0)
+
+    def do_exit(self, args):
+        self.do_quit(args)
+
+    def do_EOF(self, args):
+        print
+        self.do_quit(args)
+
+    def postcmd(self, line, bar):
+        self.set_title()
+        self.set_prompt()
+
+    def set_prompt(self):
+        if self.tree is not None:
+            try:
+                version = " "+self.tree.tree_version.nonarch
+            except:
+                version = ""
+        else:
+            version = ""
+        self.prompt = "Fai%s> " % version
+
+    def set_title(self, command=None):
+        try:
+            version = self.tree.tree_version.nonarch
+        except:
+            version = "[no version]"
+        if command is None:
+            command = ""
+        sys.stdout.write(terminal.term_title("Fai %s %s" % (command, version)))
+
+    def do_cd(self, line):
+        if line == "":
+            line = "~"
+        try:
+            os.chdir(os.path.expanduser(line))
+        except Exception, e:
+            print e
+        try:
+            self.tree = arch.tree_root()
+        except:
+            self.tree = None
+
+    def do_help(self, line):
+        Help()(line)
+
+    def default(self, line):
+        args = line.split()
+        if find_command(args[0]):
+            try:
+                find_command(args[0]).do_command(args[1:])
+            except cmdutil.BadCommandOption, e:
+                print e
+            except cmdutil.GetHelp, e:
+                find_command(args[0]).help()
+            except CommandFailed, e:
+                print e
+            except arch.errors.ArchiveNotRegistered, e:
+                print e
+            except KeyboardInterrupt, e:
+                print "Interrupted"
+            except arch.util.ExecProblem, e:
+                print e.proc.error.rstrip('\n')
+            except cmdutil.CantDetermineVersion, e:
+                print e
+            except cmdutil.CantDetermineRevision, e:
+                print e
+            except Exception, e:
+                print "Unhandled error:\n%s" % cmdutil.exception_str(e)
+
+        elif suggestions.has_key(args[0]):
+            print suggestions[args[0]]
+
+        elif self.fake_aba.is_command(args[0]):
+            tree = None
+            try:
+                tree = arch.tree_root()
+            except arch.errors.TreeRootError:
+                pass
+            cmd = self.fake_aba.is_command(args[0])
+            try:
+                cmd.run(cmdutil.expand_prefix_alias(args[1:], tree))
+            except KeyboardInterrupt, e:
+                print "Interrupted"
+
+        elif options.tla_fallthrough and args[0] != "rm" and \
+            cmdutil.is_tla_command(args[0]):
+            try:
+                tree = None
+                try:
+                    tree = arch.tree_root()
+                except arch.errors.TreeRootError:
+                    pass
+                args = cmdutil.expand_prefix_alias(args, tree)
+                arch.util.exec_safe('tla', args, stderr=sys.stderr,
+                expected=(0, 1))
+            except arch.util.ExecProblem, e:
+                pass
+            except KeyboardInterrupt, e:
+                print "Interrupted"
+        else:
+            try:
+                try:
+                    tree = arch.tree_root()
+                except arch.errors.TreeRootError:
+                    tree = None
+                args=line.split()
+                os.system(" ".join(cmdutil.expand_prefix_alias(args, tree)))
+            except KeyboardInterrupt, e:
+                print "Interrupted"
+
+    def completenames(self, text, line, begidx, endidx):
+        completions = []
+        iter = iter_command_names(self.fake_aba)
+        try:
+            if len(line) > 0:
+                arg = line.split()[-1]
+            else:
+                arg = ""
+            iter = iter_munged_completions(iter, arg, text)
+        except Exception, e:
+            print e
+        return list(iter)
+
+    def completedefault(self, text, line, begidx, endidx):
+        """Perform completion for native commands.
+        
+        :param text: The text to complete
+        :type text: str
+        :param line: The entire line to complete
+        :type line: str
+        :param begidx: The start of the text in the line
+        :type begidx: int
+        :param endidx: The end of the text in the line
+        :type endidx: int
+        """
+        try:
+            (cmd, args, foo) = self.parseline(line)
+            command_obj=find_command(cmd)
+            if command_obj is not None:
+                return command_obj.complete(args.split(), text)
+            elif not self.fake_aba.is_command(cmd) and \
+                cmdutil.is_tla_command(cmd):
+                iter = cmdutil.iter_supported_switches(cmd)
+                if len(args) > 0:
+                    arg = args.split()[-1]
+                else:
+                    arg = ""
+                if arg.startswith("-"):
+                    return list(iter_munged_completions(iter, arg, text))
+                else:
+                    return list(iter_munged_completions(
+                        iter_file_completions(arg), arg, text))
+
+
+            elif cmd == "cd":
+                if len(args) > 0:
+                    arg = args.split()[-1]
+                else:
+                    arg = ""
+                iter = iter_dir_completions(arg)
+                iter = iter_munged_completions(iter, arg, text)
+                return list(iter)
+            elif len(args)>0:
+                arg = args.split()[-1]
+                return list(iter_munged_completions(iter_file_completions(arg),
+                                                    arg, text))
+            else:
+                return self.completenames(text, line, begidx, endidx)
+        except Exception, e:
+            print e
+
+
+def iter_command_names(fake_aba):
+    for entry in cmdutil.iter_combine([commands.iterkeys(), 
+                                     fake_aba.get_commands(), 
+                                     cmdutil.iter_tla_commands(False)]):
+        if not suggestions.has_key(str(entry)):
+            yield entry
+
+
+def iter_file_completions(arg, only_dirs = False):
+    """Generate an iterator that iterates through filename completions.
+
+    :param arg: The filename fragment to match
+    :type arg: str
+    :param only_dirs: If true, match only directories
+    :type only_dirs: bool
+    """
+    cwd = os.getcwd()
+    if cwd != "/":
+        extras = [".", ".."]
+    else:
+        extras = []
+    (dir, file) = os.path.split(arg)
+    if dir != "":
+        listingdir = os.path.expanduser(dir)
+    else:
+        listingdir = cwd
+    for file in cmdutil.iter_combine([os.listdir(listingdir), extras]):
+        if dir != "":
+            userfile = dir+'/'+file
+        else:
+            userfile = file
+        if userfile.startswith(arg):
+            if os.path.isdir(listingdir+'/'+file):
+                userfile+='/'
+                yield userfile
+            elif not only_dirs:
+                yield userfile
+
+def iter_munged_completions(iter, arg, text):
+    for completion in iter:
+        completion = str(completion)
+        if completion.startswith(arg):
+            yield completion[len(arg)-len(text):]
+
+def iter_source_file_completions(tree, arg):
+    treepath = cmdutil.tree_cwd(tree)
+    if len(treepath) > 0:
+        dirs = [treepath]
+    else:
+        dirs = None
+    for file in tree.iter_inventory(dirs, source=True, both=True):
+        file = file_completion_match(file, treepath, arg)
+        if file is not None:
+            yield file
+
+
+def iter_untagged(tree, dirs):
+    for file in arch_core.iter_inventory_filter(tree, dirs, tagged=False, 
+                                                categories=arch_core.non_root,
+                                                control_files=True):
+        yield file.name 
+
+
+def iter_untagged_completions(tree, arg):
+    """Generate an iterator for all visible untagged files that match arg.
+
+    :param tree: The tree to look for untagged files in
+    :type tree: `arch.WorkingTree`
+    :param arg: The argument to match
+    :type arg: str
+    :return: An iterator of all matching untagged files
+    :rtype: iterator of str
+    """
+    treepath = cmdutil.tree_cwd(tree)
+    if len(treepath) > 0:
+        dirs = [treepath]
+    else:
+        dirs = None
+
+    for file in iter_untagged(tree, dirs):
+        file = file_completion_match(file, treepath, arg)
+        if file is not None:
+            yield file
+
+
+def file_completion_match(file, treepath, arg):
+    """Determines whether a file within an arch tree matches the argument.
+
+    :param file: The rooted filename
+    :type file: str
+    :param treepath: The path to the cwd within the tree
+    :type treepath: str
+    :param arg: The prefix to match
+    :return: The completion name, or None if not a match
+    :rtype: str
+    """
+    if not file.startswith(treepath):
+        return None
+    if treepath != "":
+        file = file[len(treepath)+1:]
+
+    if not file.startswith(arg):
+        return None 
+    if os.path.isdir(file):
+        file += '/'
+    return file
+
+def iter_modified_file_completions(tree, arg):
+    """Returns a list of modified files that match the specified prefix.
+
+    :param tree: The current tree
+    :type tree: `arch.WorkingTree`
+    :param arg: The prefix to match
+    :type arg: str
+    """
+    treepath = cmdutil.tree_cwd(tree)
+    tmpdir = cmdutil.tmpdir()
+    changeset = tmpdir+"/changeset"
+    completions = []
+    revision = cmdutil.determine_revision_tree(tree)
+    for line in arch.iter_delta(revision, tree, changeset):
+        if isinstance(line, arch.FileModification):
+            file = file_completion_match(line.name[1:], treepath, arg)
+            if file is not None:
+                completions.append(file)
+    shutil.rmtree(tmpdir)
+    return completions
+
+def iter_dir_completions(arg):
+    """Generate an iterator that iterates through directory name completions.
+
+    :param arg: The directory name fragment to match
+    :type arg: str
+    """
+    return iter_file_completions(arg, True)
+
+class Shell(BaseCommand):
+    def __init__(self):
+        self.description = "Runs Fai as a shell"
+
+    def do_command(self, cmdargs):
+        if len(cmdargs)!=0:
+            raise cmdutil.GetHelp
+        prompt = PromptCmd()
+        try:
+            prompt.cmdloop()
+        finally:
+            prompt.write_history()
+
+class AddID(BaseCommand):
+    """
+    Adds an inventory id for the given file
+    """
+    def __init__(self):
+        self.description="Add an inventory id for a given file"
+
+    def get_completer(self, arg, index):
+        tree = arch.tree_root()
+        return iter_untagged_completions(tree, arg)
+
+    def do_command(self, cmdargs):
+        """
+        Master function that perfoms the "revision" command.
+        """
+        parser=self.get_parser()
+        (options, args) = parser.parse_args(cmdargs)
+
+        tree = arch.tree_root()
+
+        if (len(args) == 0) == (options.untagged == False):
+            raise cmdutil.GetHelp
+
+       #if options.id and len(args) != 1:
+       #    print "If --id is specified, only one file can be named."
+       #    return
+        
+        method = tree.tagging_method
+        
+        if options.id_type == "tagline":
+            if method != "tagline":
+                if not cmdutil.prompt("Tagline in other tree"):
+                    if method == "explicit":
+                        options.id_type == explicit
+                    else:
+                        print "add-id not supported for \"%s\" tagging method"\
+                            % method 
+                        return
+        
+        elif options.id_type == "explicit":
+            if method != "tagline" and method != explicit:
+                if not prompt("Explicit in other tree"):
+                    print "add-id not supported for \"%s\" tagging method" % \
+                        method
+                    return
+        
+        if options.id_type == "auto":
+            if method != "tagline" and method != "explicit":
+                print "add-id not supported for \"%s\" tagging method" % method
+                return
+            else:
+                options.id_type = method
+        if options.untagged:
+            args = None
+        self.add_ids(tree, options.id_type, args)
+
+    def add_ids(self, tree, id_type, files=()):
+        """Add inventory ids to files.
+        
+        :param tree: the tree the files are in
+        :type tree: `arch.WorkingTree`
+        :param id_type: the type of id to add: "explicit" or "tagline"
+        :type id_type: str
+        :param files: The list of files to add.  If None do all untagged.
+        :type files: tuple of str
+        """
+
+        untagged = (files is None)
+        if untagged:
+            files = list(iter_untagged(tree, None))
+        previous_files = []
+        while len(files) > 0:
+            previous_files.extend(files)
+            if id_type == "explicit":
+                cmdutil.add_id(files)
+            elif id_type == "tagline":
+                for file in files:
+                    try:
+                        cmdutil.add_tagline_or_explicit_id(file)
+                    except cmdutil.AlreadyTagged:
+                        print "\"%s\" already has a tagline." % file
+                    except cmdutil.NoCommentSyntax:
+                        pass
+            #do inventory after tagging until no untagged files are encountered
+            if untagged:
+                files = []
+                for file in iter_untagged(tree, None):
+                    if not file in previous_files:
+                        files.append(file)
+
+            else:
+                break
+
+    def get_parser(self):
+        """
+        Returns the options parser to use for the "revision" command.
+
+        :rtype: cmdutil.CmdOptionParser
+        """
+        parser=cmdutil.CmdOptionParser("fai add-id file1 [file2] [file3]...")
+# ddaa suggests removing this to promote GUIDs.  Let's see who squalks.
+#        parser.add_option("-i", "--id", dest="id", 
+#                         help="Specify id for a single file", default=None)
+        parser.add_option("--tltl", action="store_true", 
+                         dest="lord_style",  help="Use Tom Lord's style of id.")
+        parser.add_option("--explicit", action="store_const", 
+                         const="explicit", dest="id_type", 
+                         help="Use an explicit id", default="auto")
+        parser.add_option("--tagline", action="store_const", 
+                         const="tagline", dest="id_type", 
+                         help="Use a tagline id")
+        parser.add_option("--untagged", action="store_true", 
+                         dest="untagged", default=False, 
+                         help="tag all untagged files")
+        return parser 
+
+    def help(self, parser=None):
+        """
+        Prints a help message.
+
+        :param parser: If supplied, the parser to use for generating help.  If \
+        not supplied, it is retrieved.
+        :type parser: cmdutil.CmdOptionParser
+        """
+        if parser==None:
+            parser=self.get_parser()
+        parser.print_help()
+        print """
+Adds an inventory to the specified file(s) and directories.  If --untagged is
+specified, adds inventory to all untagged files and directories.
+        """
+        return
+
+
+class Merge(BaseCommand):
+    """
+    Merges changes from other versions into the current tree
+    """
+    def __init__(self):
+        self.description="Merges changes from other versions"
+        try:
+            self.tree = arch.tree_root()
+        except:
+            self.tree = None
+
+
+    def get_completer(self, arg, index):
+        if self.tree is None:
+            raise arch.errors.TreeRootError
+        completions = list(ancillary.iter_partners(self.tree, 
+                                                   self.tree.tree_version))
+        if len(completions) == 0:
+            completions = list(self.tree.iter_log_versions())
+
+        aliases = []
+        try:
+            for completion in completions:
+                alias = ancillary.compact_alias(str(completion), self.tree)
+                if alias:
+                    aliases.extend(alias)
+
+            for completion in completions:
+                if completion.archive == self.tree.tree_version.archive:
+                    aliases.append(completion.nonarch)
+
+        except Exception, e:
+            print e
+            
+        completions.extend(aliases)
+        return completions
+
+    def do_command(self, cmdargs):
+        """
+        Master function that perfoms the "merge" command.
+        """
+        parser=self.get_parser()
+        (options, args) = parser.parse_args(cmdargs)
+        if options.diff3:
+            action="star-merge"
+        else:
+            action = options.action
+        
+        if self.tree is None:
+            raise arch.errors.TreeRootError(os.getcwd())
+        if cmdutil.has_changed(self.tree.tree_version):
+            raise UncommittedChanges(self.tree)
+
+        if len(args) > 0:
+            revisions = []
+            for arg in args:
+                revisions.append(cmdutil.determine_revision_arch(self.tree, 
+                                                                 arg))
+            source = "from commandline"
+        else:
+            revisions = ancillary.iter_partner_revisions(self.tree, 
+                                                         self.tree.tree_version)
+            source = "from partner version"
+        revisions = misc.rewind_iterator(revisions)
+        try:
+            revisions.next()
+            revisions.rewind()
+        except StopIteration, e:
+            revision = cmdutil.tag_cur(self.tree)
+            if revision is None:
+                raise CantDetermineRevision("", "No version specified, no "
+                                            "partner-versions, and no tag"
+                                            " source")
+            revisions = [revision]
+            source = "from tag source"
+        for revision in revisions:
+            cmdutil.ensure_archive_registered(revision.archive)
+            cmdutil.colorize(arch.Chatter("* Merging %s [%s]" % 
+                             (revision, source)))
+            if action=="native-merge" or action=="update":
+                if self.native_merge(revision, action) == 0:
+                    continue
+            elif action=="star-merge":
+                try: 
+                    self.star_merge(revision, options.diff3)
+                except errors.MergeProblem, e:
+                    break
+            if cmdutil.has_changed(self.tree.tree_version):
+                break
+
+    def star_merge(self, revision, diff3):
+        """Perform a star-merge on the current tree.
+        
+        :param revision: The revision to use for the merge
+        :type revision: `arch.Revision`
+        :param diff3: If true, do a diff3 merge
+        :type diff3: bool
+        """
+        try:
+            for line in self.tree.iter_star_merge(revision, diff3=diff3):
+                cmdutil.colorize(line)
+        except arch.util.ExecProblem, e:
+            if e.proc.status is not None and e.proc.status == 1:
+                if e.proc.error:
+                    print e.proc.error
+                raise MergeProblem
+            else:
+                raise
+
+    def native_merge(self, other_revision, action):
+        """Perform a native-merge on the current tree.
+        
+        :param other_revision: The revision to use for the merge
+        :type other_revision: `arch.Revision`
+        :return: 0 if the merge was skipped, 1 if it was applied
+        """
+        other_tree = cmdutil.find_or_make_local_revision(other_revision)
+        try:
+            if action == "native-merge":
+                ancestor = cmdutil.merge_ancestor2(self.tree, other_tree, 
+                                                   other_revision)
+            elif action == "update":
+                ancestor = cmdutil.tree_latest(self.tree, 
+                                               other_revision.version)
+        except CantDetermineRevision, e:
+            raise CommandFailedWrapper(e)
+        cmdutil.colorize(arch.Chatter("* Found common ancestor %s" % ancestor))
+        if (ancestor == other_revision):
+            cmdutil.colorize(arch.Chatter("* Skipping redundant merge" 
+                                          % ancestor))
+            return 0
+        delta = cmdutil.apply_delta(ancestor, other_tree, self.tree)    
+        for line in cmdutil.iter_apply_delta_filter(delta):
+            cmdutil.colorize(line)
+        return 1
+
+
+
+    def get_parser(self):
+        """
+        Returns the options parser to use for the "merge" command.
+
+        :rtype: cmdutil.CmdOptionParser
+        """
+        parser=cmdutil.CmdOptionParser("fai merge [VERSION]")
+        parser.add_option("-s", "--star-merge", action="store_const",
+                          dest="action", help="Use star-merge",
+                          const="star-merge", default="native-merge")
+        parser.add_option("--update", action="store_const",
+                          dest="action", help="Use update picker",
+                          const="update")
+        parser.add_option("--diff3", action="store_true", 
+                         dest="diff3",  
+                         help="Use diff3 for merge (implies star-merge)")
+        return parser 
+
+    def help(self, parser=None):
+        """
+        Prints a help message.
+
+        :param parser: If supplied, the parser to use for generating help.  If \
+        not supplied, it is retrieved.
+        :type parser: cmdutil.CmdOptionParser
+        """
+        if parser==None:
+            parser=self.get_parser()
+        parser.print_help()
+        print """
+Performs a merge operation using the specified version.
+        """
+        return
+
+class ELog(BaseCommand):
+    """
+    Produces a raw patchlog and invokes the user's editor
+    """
+    def __init__(self):
+        self.description="Edit a patchlog to commit"
+        try:
+            self.tree = arch.tree_root()
+        except:
+            self.tree = None
+
+
+    def do_command(self, cmdargs):
+        """
+        Master function that perfoms the "elog" command.
+        """
+        parser=self.get_parser()
+        (options, args) = parser.parse_args(cmdargs)
+        if self.tree is None:
+            raise arch.errors.TreeRootError
+
+        edit_log(self.tree)
+
+    def get_parser(self):
+        """
+        Returns the options parser to use for the "merge" command.
+
+        :rtype: cmdutil.CmdOptionParser
+        """
+        parser=cmdutil.CmdOptionParser("fai elog")
+        return parser 
+
+
+    def help(self, parser=None):
+        """
+        Invokes $EDITOR to produce a log for committing.
+
+        :param parser: If supplied, the parser to use for generating help.  If \
+        not supplied, it is retrieved.
+        :type parser: cmdutil.CmdOptionParser
+        """
+        if parser==None:
+            parser=self.get_parser()
+        parser.print_help()
+        print """
+Invokes $EDITOR to produce a log for committing.
+        """
+        return
+
+def edit_log(tree):
+    """Makes and edits the log for a tree.  Does all kinds of fancy things
+    like log templates and merge summaries and log-for-merge
+    
+    :param tree: The tree to edit the log for
+    :type tree: `arch.WorkingTree`
+    """
+    #ensure we have an editor before preparing the log
+    cmdutil.find_editor()
+    log = tree.log_message(create=False)
+    log_is_new = False
+    if log is None or cmdutil.prompt("Overwrite log"):
+        if log is not None:
+           os.remove(log.name)
+        log = tree.log_message(create=True)
+        log_is_new = True
+        tmplog = log.name
+        template = tree+"/{arch}/=log-template"
+        if not os.path.exists(template):
+            template = os.path.expanduser("~/.arch-params/=log-template")
+            if not os.path.exists(template):
+                template = None
+        if template:
+            shutil.copyfile(template, tmplog)
+        
+        new_merges = list(cmdutil.iter_new_merges(tree, 
+                                                  tree.tree_version))
+        log["Summary"] = merge_summary(new_merges, tree.tree_version)
+        if len(new_merges) > 0:   
+            if cmdutil.prompt("Log for merge"):
+                mergestuff = cmdutil.log_for_merge(tree)
+                log.description += mergestuff
+        log.save()
+    try:
+        cmdutil.invoke_editor(log.name)
+    except:
+        if log_is_new:
+            os.remove(log.name)
+        raise
+
+def merge_summary(new_merges, tree_version):
+    if len(new_merges) == 0:
+        return ""
+    if len(new_merges) == 1:
+        summary = new_merges[0].summary
+    else:
+        summary = "Merge"
+
+    credits = []
+    for merge in new_merges:
+        if arch.my_id() != merge.creator:
+            name = re.sub("<.*>", "", merge.creator).rstrip(" ");
+            if not name in credits:
+                credits.append(name)
+        else:
+            version = merge.revision.version
+            if version.archive == tree_version.archive:
+                if not version.nonarch in credits:
+                    credits.append(version.nonarch)
+            elif not str(version) in credits:
+                credits.append(str(version))
+
+    return ("%s (%s)") % (summary, ", ".join(credits))
+
+class MirrorArchive(BaseCommand):
+    """
+    Updates a mirror from an archive
+    """
+    def __init__(self):
+        self.description="Update a mirror from an archive"
+
+    def do_command(self, cmdargs):
+        """
+        Master function that perfoms the "revision" command.
+        """
+
+        parser=self.get_parser()
+        (options, args) = parser.parse_args(cmdargs)
+        if len(args) > 1:
+            raise GetHelp
+        try:
+            tree = arch.tree_root()
+        except:
+            tree = None
+
+        if len(args) == 0:
+            if tree is not None:
+                name = tree.tree_version()
+        else:
+            name = cmdutil.expand_alias(args[0], tree)
+            name = arch.NameParser(name)
+
+        to_arch = name.get_archive()
+        from_arch = cmdutil.get_mirror_source(arch.Archive(to_arch))
+        limit = name.get_nonarch()
+
+        iter = arch_core.mirror_archive(from_arch,to_arch, limit)
+        for line in arch.chatter_classifier(iter):
+            cmdutil.colorize(line)
+
+    def get_parser(self):
+        """
+        Returns the options parser to use for the "revision" command.
+
+        :rtype: cmdutil.CmdOptionParser
+        """
+        parser=cmdutil.CmdOptionParser("fai mirror-archive ARCHIVE")
+        return parser 
+
+    def help(self, parser=None):
+        """
+        Prints a help message.
+
+        :param parser: If supplied, the parser to use for generating help.  If \
+        not supplied, it is retrieved.
+        :type parser: cmdutil.CmdOptionParser
+        """
+        if parser==None:
+            parser=self.get_parser()
+        parser.print_help()
+        print """
+Updates a mirror from an archive.  If a branch, package, or version is
+supplied, only changes under it are mirrored.
+        """
+        return
+
+def help_tree_spec():
+    print """Specifying revisions (default: tree)
+Revisions may be specified by alias, revision, version or patchlevel.
+Revisions or versions may be fully qualified.  Unqualified revisions, versions, 
+or patchlevels use the archive of the current project tree.  Versions will
+use the latest patchlevel in the tree.  Patchlevels will use the current tree-
+version.
+
+Use "alias" to list available (user and automatic) aliases."""
+
+def help_aliases(tree):
+    print """Auto-generated aliases
+ acur : The latest revision in the archive of the tree-version.  You can specfy
+        a different version like so: acur:foo--bar--0 (aliases can be used)
+ tcur : (tree current) The latest revision in the tree of the tree-version.
+        You can specify a different version like so: tcur:foo--bar--0 (aliases
+        can be used).
+tprev : (tree previous) The previous revision in the tree of the tree-version.
+        To specify an older revision, use a number, e.g. "tprev:4"
+ tanc : (tree ancestor) The ancestor revision of the tree
+        To specify an older revision, use a number, e.g. "tanc:4"
+tdate : (tree date) The latest revision from a given date (e.g. "tdate:July 6")
+ tmod : (tree modified) The latest revision to modify a given file 
+        (e.g. "tmod:engine.cpp" or "tmod:engine.cpp:16")
+ ttag : (tree tag) The revision that was tagged into the current tree revision,
+        according to the tree.
+tagcur: (tag current) The latest revision of the version that the current tree
+        was tagged from.
+mergeanc : The common ancestor of the current tree and the specified revision.
+        Defaults to the first partner-version's latest revision or to tagcur.
+   """
+    print "User aliases"
+    for parts in ancillary.iter_all_alias(tree):
+        print parts[0].rjust(10)+" : "+parts[1]
+
+
+class Inventory(BaseCommand):
+    """List the status of files in the tree"""
+    def __init__(self):
+        self.description=self.__doc__
+
+    def do_command(self, cmdargs):
+        """
+        Master function that perfoms the "revision" command.
+        """
+
+        parser=self.get_parser()
+        (options, args) = parser.parse_args(cmdargs)
+        tree = arch.tree_root()
+        categories = []
+
+        if (options.source):
+            categories.append(arch_core.SourceFile)
+        if (options.precious):
+            categories.append(arch_core.PreciousFile)
+        if (options.backup):
+            categories.append(arch_core.BackupFile)
+        if (options.junk):
+            categories.append(arch_core.JunkFile)
+
+        if len(categories) == 1:
+            show_leading = False
+        else:
+            show_leading = True
+
+        if len(categories) == 0:
+            categories = None
+
+        if options.untagged:
+            categories = arch_core.non_root
+            show_leading = False
+            tagged = False
+        else:
+            tagged = None
+        
+        for file in arch_core.iter_inventory_filter(tree, None, 
+            control_files=options.control_files, 
+            categories = categories, tagged=tagged):
+            print arch_core.file_line(file, 
+                                      category = show_leading, 
+                                      untagged = show_leading,
+                                      id = options.ids)
+
+    def get_parser(self):
+        """
+        Returns the options parser to use for the "revision" command.
+
+        :rtype: cmdutil.CmdOptionParser
+        """
+        parser=cmdutil.CmdOptionParser("fai inventory [options]")
+        parser.add_option("--ids", action="store_true", dest="ids", 
+                          help="Show file ids")
+        parser.add_option("--control", action="store_true", 
+                          dest="control_files", help="include control files")
+        parser.add_option("--source", action="store_true", dest="source",
+                          help="List source files")
+        parser.add_option("--backup", action="store_true", dest="backup",
+                          help="List backup files")
+        parser.add_option("--precious", action="store_true", dest="precious",
+                          help="List precious files")
+        parser.add_option("--junk", action="store_true", dest="junk",
+                          help="List junk files")
+        parser.add_option("--unrecognized", action="store_true", 
+                          dest="unrecognized", help="List unrecognized files")
+        parser.add_option("--untagged", action="store_true", 
+                          dest="untagged", help="List only untagged files")
+        return parser 
+
+    def help(self, parser=None):
+        """
+        Prints a help message.
+
+        :param parser: If supplied, the parser to use for generating help.  If \
+        not supplied, it is retrieved.
+        :type parser: cmdutil.CmdOptionParser
+        """
+        if parser==None:
+            parser=self.get_parser()
+        parser.print_help()
+        print """
+Lists the status of files in the archive:
+S source
+P precious
+B backup
+J junk
+U unrecognized
+T tree root
+? untagged-source
+Leading letter are not displayed if only one kind of file is shown
+        """
+        return
+
+
+class Alias(BaseCommand):
+    """List or adjust aliases"""
+    def __init__(self):
+        self.description=self.__doc__
+
+    def get_completer(self, arg, index):
+        if index > 2:
+            return ()
+        try:
+            self.tree = arch.tree_root()
+        except:
+            self.tree = None
+
+        if index == 0:
+            return [part[0]+" " for part in ancillary.iter_all_alias(self.tree)]
+        elif index == 1:
+            return cmdutil.iter_revision_completions(arg, self.tree)
+
+
+    def do_command(self, cmdargs):
+        """
+        Master function that perfoms the "revision" command.
+        """
+
+        parser=self.get_parser()
+        (options, args) = parser.parse_args(cmdargs)
+        try:
+            self.tree =  arch.tree_root()
+        except:
+            self.tree = None
+
+
+        try:
+            options.action(args, options)
+        except cmdutil.ForbiddenAliasSyntax, e:
+            raise CommandFailedWrapper(e)
+
+    def arg_dispatch(self, args, options):
+        """Add, modify, or list aliases, depending on number of arguments
+
+        :param args: The list of commandline arguments
+        :type args: list of str
+        :param options: The commandline options
+        """
+        if len(args) == 0:
+            help_aliases(self.tree)
+            return
+        elif len(args) == 1:
+            self.print_alias(args[0])
+        elif (len(args)) == 2:
+            self.add(args[0], args[1], options)
+        else:
+            raise cmdutil.GetHelp
+
+    def print_alias(self, alias):
+        answer = None
+        for pair in ancillary.iter_all_alias(self.tree):
+            if pair[0] == alias:
+                answer = pair[1]
+        if answer is not None:
+            print answer
+        else:
+            print "The alias %s is not assigned." % alias
+
+    def add(self, alias, expansion, options):
+        """Add or modify aliases
+
+        :param alias: The alias name to create/modify
+        :type alias: str
+        :param expansion: The expansion to assign to the alias name
+        :type expansion: str
+        :param options: The commandline options
+        """
+        newlist = ""
+        written = False
+        new_line = "%s=%s\n" % (alias, cmdutil.expand_alias(expansion, 
+            self.tree))
+        ancillary.check_alias(new_line.rstrip("\n"), [alias, expansion])
+
+        for pair in self.get_iterator(options):
+            if pair[0] != alias:
+                newlist+="%s=%s\n" % (pair[0], pair[1])
+            elif not written:
+                newlist+=new_line
+                written = True
+        if not written:
+            newlist+=new_line
+        self.write_aliases(newlist, options)
+            
+    def delete(self, args, options):
+        """Delete the specified alias
+
+        :param args: The list of arguments
+        :type args: list of str
+        :param options: The commandline options
+        """
+        deleted = False
+        if len(args) != 1:
+            raise cmdutil.GetHelp
+        newlist = ""
+        for pair in self.get_iterator(options):
+            if pair[0] != args[0]:
+                newlist+="%s=%s\n" % (pair[0], pair[1])
+            else:
+                deleted = True
+        if not deleted:
+            raise errors.NoSuchAlias(args[0])
+        self.write_aliases(newlist, options)
+
+    def get_alias_file(self, options):
+        """Return the name of the alias file to use
+
+        :param options: The commandline options
+        """
+        if options.tree:
+            if self.tree is None:
+                self.tree == arch.tree_root()
+            return str(self.tree)+"/{arch}/+aliases"
+        else:
+            return "~/.aba/aliases"
+
+    def get_iterator(self, options):
+        """Return the alias iterator to use
+
+        :param options: The commandline options
+        """
+        return ancillary.iter_alias(self.get_alias_file(options))
+
+    def write_aliases(self, newlist, options):
+        """Safely rewrite the alias file
+        :param newlist: The new list of aliases
+        :type newlist: str
+        :param options: The commandline options
+        """
+        filename = os.path.expanduser(self.get_alias_file(options))
+        file = cmdutil.NewFileVersion(filename)
+        file.write(newlist)
+        file.commit()
+
+
+    def get_parser(self):
+        """
+        Returns the options parser to use for the "alias" command.
+
+        :rtype: cmdutil.CmdOptionParser
+        """
+        parser=cmdutil.CmdOptionParser("fai alias [ALIAS] [NAME]")
+        parser.add_option("-d", "--delete", action="store_const", dest="action",
+                          const=self.delete, default=self.arg_dispatch, 
+                          help="Delete an alias")
+        parser.add_option("--tree", action="store_true", dest="tree", 
+                          help="Create a per-tree alias", default=False)
+        return parser 
+
+    def help(self, parser=None):
+        """
+        Prints a help message.
+
+        :param parser: If supplied, the parser to use for generating help.  If \
+        not supplied, it is retrieved.
+        :type parser: cmdutil.CmdOptionParser
+        """
+        if parser==None:
+            parser=self.get_parser()
+        parser.print_help()
+        print """
+Lists current aliases or modifies the list of aliases.
+
+If no arguments are supplied, aliases will be listed.  If two arguments are
+supplied, the specified alias will be created or modified.  If -d or --delete
+is supplied, the specified alias will be deleted.
+
+You can create aliases that refer to any fully-qualified part of the
+Arch namespace, e.g. 
+archive, 
+archive/category, 
+archive/category--branch, 
+archive/category--branch--version (my favourite)
+archive/category--branch--version--patchlevel
+
+Aliases can be used automatically by native commands.  To use them
+with external or tla commands, prefix them with ^ (you can do this
+with native commands, too).
+"""
+
+
+class RequestMerge(BaseCommand):
+    """Submit a merge request to Bug Goo"""
+    def __init__(self):
+        self.description=self.__doc__
+
+    def do_command(self, cmdargs):
+        """Submit a merge request
+
+        :param cmdargs: The commandline arguments
+        :type cmdargs: list of str
+        """
+        cmdutil.find_editor()
+        parser = self.get_parser()
+        (options, args) = parser.parse_args(cmdargs)
+        try:
+            self.tree=arch.tree_root()
+        except:
+            self.tree=None
+        base, revisions = self.revision_specs(args)
+        message = self.make_headers(base, revisions)
+        message += self.make_summary(revisions)
+        path = self.edit_message(message)
+        message = self.tidy_message(path)
+        if cmdutil.prompt("Send merge"):
+            self.send_message(message)
+            print "Merge request sent"
+
+    def make_headers(self, base, revisions):
+        """Produce email and Bug Goo header strings
+
+        :param base: The base revision to apply merges to
+        :type base: `arch.Revision`
+        :param revisions: The revisions to replay into the base
+        :type revisions: list of `arch.Patchlog`
+        :return: The headers
+        :rtype: str
+        """
+        headers = "To: gnu-arch-users@gnu.org\n"
+        headers += "From: %s\n" % options.fromaddr
+        if len(revisions) == 1:
+            headers += "Subject: [MERGE REQUEST] %s\n" % revisions[0].summary
+        else:
+            headers += "Subject: [MERGE REQUEST]\n"
+        headers += "\n"
+        headers += "Base-Revision: %s\n" % base
+        for revision in revisions:
+            headers += "Revision: %s\n" % revision.revision
+        headers += "Bug: \n\n"
+        return headers
+
+    def make_summary(self, logs):
+        """Generate a summary of merges
+
+        :param logs: the patchlogs that were directly added by the merges
+        :type logs: list of `arch.Patchlog`
+        :return: the summary
+        :rtype: str
+        """ 
+        summary = ""
+        for log in logs:
+            summary+=str(log.revision)+"\n"
+            summary+=log.summary+"\n"
+            if log.description.strip():
+                summary+=log.description.strip('\n')+"\n\n"
+        return summary
+
+    def revision_specs(self, args):
+        """Determine the base and merge revisions from tree and arguments.
+
+        :param args: The parsed arguments
+        :type args: list of str
+        :return: The base revision and merge revisions 
+        :rtype: `arch.Revision`, list of `arch.Patchlog`
+        """
+        if len(args) > 0:
+            target_revision = cmdutil.determine_revision_arch(self.tree, 
+                                                              args[0])
+        else:
+            target_revision = cmdutil.tree_latest(self.tree)
+        if len(args) > 1:
+            merges = [ arch.Patchlog(cmdutil.determine_revision_arch(
+                       self.tree, f)) for f in args[1:] ]
+        else:
+            if self.tree is None:
+                raise CantDetermineRevision("", "Not in a project tree")
+            merge_iter = cmdutil.iter_new_merges(self.tree, 
+                                                 target_revision.version, 
+                                                 False)
+            merges = [f for f in cmdutil.direct_merges(merge_iter)]
+        return (target_revision, merges)
+
+    def edit_message(self, message):
+        """Edit an email message in the user's standard editor
+
+        :param message: The message to edit
+        :type message: str
+        :return: the path of the edited message
+        :rtype: str
+        """
+        if self.tree is None:
+            path = os.get_cwd()
+        else:
+            path = self.tree
+        path += "/,merge-request"
+        file = open(path, 'w')
+        file.write(message)
+        file.flush()
+        cmdutil.invoke_editor(path)
+        return path
+
+    def tidy_message(self, path):
+        """Validate and clean up message.
+
+        :param path: The path to the message to clean up
+        :type path: str
+        :return: The parsed message
+        :rtype: `email.Message`
+        """
+        mail = email.message_from_file(open(path))
+        if mail["Subject"].strip() == "[MERGE REQUEST]":
+            raise BlandSubject
+        
+        request = email.message_from_string(mail.get_payload())
+        if request.has_key("Bug"):
+            if request["Bug"].strip()=="":
+                del request["Bug"]
+        mail.set_payload(request.as_string())
+        return mail
+
+    def send_message(self, message):
+        """Send a message, using its headers to address it.
+
+        :param message: The message to send
+        :type message: `email.Message`"""
+        server = smtplib.SMTP()
+        server.sendmail(message['From'], message['To'], message.as_string())
+        server.quit()
+
+    def help(self, parser=None):
+        """Print a usage message
+
+        :param parser: The options parser to use
+        :type parser: `cmdutil.CmdOptionParser`
+        """
+        if parser is None:
+            parser = self.get_parser()
+        parser.print_help()
+        print """
+Sends a merge request formatted for Bug Goo.  Intended use: get the tree
+you'd like to merge into.  Apply the merges you want.  Invoke request-merge.
+The merge request will open in your $EDITOR.
+
+When no TARGET is specified, it uses the current tree revision.  When
+no MERGE is specified, it uses the direct merges (as in "revisions
+--direct-merges").  But you can specify just the TARGET, or all the MERGE
+revisions.
+"""
+
+    def get_parser(self):
+        """Produce a commandline parser for this command.
+
+        :rtype: `cmdutil.CmdOptionParser`
+        """
+        parser=cmdutil.CmdOptionParser("request-merge [TARGET] [MERGE1...]")
+        return parser
+
+commands = { 
+'changes' : Changes,
+'help' : Help,
+'update': Update,
+'apply-changes':ApplyChanges,
+'cat-log': CatLog,
+'commit': Commit,
+'revision': Revision,
+'revisions': Revisions,
+'get': Get,
+'revert': Revert,
+'shell': Shell,
+'add-id': AddID,
+'merge': Merge,
+'elog': ELog,
+'mirror-archive': MirrorArchive,
+'ninventory': Inventory,
+'alias' : Alias,
+'request-merge': RequestMerge,
+}
+suggestions = {
+'apply-delta' : "Try \"apply-changes\".",
+'delta' : "To compare two revisions, use \"changes\".",
+'diff-rev' : "To compare two revisions, use \"changes\".",
+'undo' : "To undo local changes, use \"revert\".",
+'undelete' : "To undo only deletions, use \"revert --deletions\"",
+'missing-from' : "Try \"revisions --missing-from\".",
+'missing' : "Try \"revisions --missing\".",
+'missing-merge' : "Try \"revisions --partner-missing\".",
+'new-merges' : "Try \"revisions --new-merges\".",
+'cachedrevs' : "Try \"revisions --cacherevs\". (no 'd')",
+'logs' : "Try \"revisions --logs\"",
+'tree-source' : "Use the \"^ttag\" alias (\"revision ^ttag\")",
+'latest-revision' : "Use the \"^acur\" alias (\"revision ^acur\")",
+'change-version' : "Try \"update REVISION\"",
+'tree-revision' : "Use the \"^tcur\" alias (\"revision ^tcur\")",
+'rev-depends' : "Use revisions --dependencies",
+'auto-get' : "Plain get will do archive lookups",
+'tagline' : "Use add-id.  It uses taglines in tagline trees",
+'emlog' : "Use elog.  It automatically adds log-for-merge text, if any",
+'library-revisions' : "Use revisions --library",
+'file-revert' : "Use revert FILE"
+}
+# arch-tag: 19d5739d-3708-486c-93ba-deecc3027fc7

*** modified file 'bzrlib/branch.py'
--- bzrlib/branch.py 
+++ bzrlib/branch.py 
@@ -31,6 +31,8 @@
 from revision import Revision
 from errors import bailout, BzrError
 from textui import show_status
+import patches
+from bzrlib import progress
 
 BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
 ## TODO: Maybe include checks for common corruption of newlines, etc?
@@ -864,3 +866,36 @@
 
     s = hexlify(rand_bytes(8))
     return '-'.join((name, compact_date(time.time()), s))
+
+
+def iter_anno_data(branch, file_id):
+    later_revision = branch.revno()
+    q = range(branch.revno())
+    q.reverse()
+    later_text_id = branch.basis_tree().inventory[file_id].text_id
+    i = 0
+    for revno in q:
+        i += 1
+        cur_tree = branch.revision_tree(branch.lookup_revision(revno))
+        if file_id not in cur_tree.inventory:
+            text_id = None
+        else:
+            text_id = cur_tree.inventory[file_id].text_id
+        if text_id != later_text_id:
+            patch = get_patch(branch, revno, later_revision, file_id)
+            yield revno, patch.iter_inserted(), patch
+            later_revision = revno
+            later_text_id = text_id
+        yield progress.Progress("revisions", i)
+
+def get_patch(branch, old_revno, new_revno, file_id):
+    old_tree = branch.revision_tree(branch.lookup_revision(old_revno))
+    new_tree = branch.revision_tree(branch.lookup_revision(new_revno))
+    if file_id in old_tree.inventory:
+        old_file = old_tree.get_file(file_id).readlines()
+    else:
+        old_file = []
+    ud = difflib.unified_diff(old_file, new_tree.get_file(file_id).readlines())
+    return patches.parse_patch(ud)
+
+

*** modified file 'bzrlib/commands.py'
--- bzrlib/commands.py 
+++ bzrlib/commands.py 
@@ -27,6 +27,9 @@
 from bzrlib import Branch, Inventory, InventoryEntry, ScratchBranch, BZRDIR, \
      format_date
 from bzrlib import merge
+from bzrlib.branch import iter_anno_data
+from bzrlib import patches
+from bzrlib import progress
 
 
 def _squish_command_name(cmd):
@@ -882,7 +885,15 @@
                 print '%3d FAILED!' % mf
             else:
                 print
-
+        result = bzrlib.patches.test()
+        resultFailed = len(result.errors) + len(result.failures)
+        print '%-40s %3d tests' % ('bzrlib.patches', result.testsRun),
+        if resultFailed:
+            print '%3d FAILED!' % resultFailed
+        else:
+            print
+        tests += result.testsRun
+        failures += resultFailed
         print '%-40s %3d tests' % ('total', tests),
         if failures:
             print '%3d FAILED!' % failures
@@ -897,6 +908,34 @@
     """Show version of bzr"""
     def run(self):
         show_version()
+
+class cmd_annotate(Command):
+    """Show which revision added each line in a file"""
+
+    takes_args = ['filename']
+    def run(self, filename):
+        if not os.path.exists(filename):
+            raise BzrCommandError("The file %s does not exist." % filename)
+        branch = (Branch(filename))
+        file_id = branch.working_tree().path2id(filename)
+        if file_id is None:
+            raise BzrCommandError("The file %s is not versioned." % filename)
+        lines = branch.basis_tree().get_file(file_id)
+        total = branch.revno()
+        anno_d_iter = iter_anno_data(branch, file_id)
+        progress_bar = progress.ProgressBar()
+        try:
+            for result in patches.iter_annotate_file(lines, anno_d_iter):
+                if isinstance(result, progress.Progress):
+                    result.total = total
+                    progress_bar(result)
+                else:
+                    anno_lines = result
+        finally:
+            progress.clear_progress_bar()
+        for line in anno_lines:
+            sys.stdout.write("%4s:%s" % (str(line.log), line.text))
+
 
 def show_version():
     print "bzr (bazaar-ng) %s" % bzrlib.__version__

