#!/usr/bin/python2.3 -ut

# Copyright 2003-2005 Iustin Pop
#
# This file is part of cfvers.
#
# cfvers 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.
#
# cfvers 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 cfvers; if not, write to the Free Software Foundation,
# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

# $Id: cfv 219 2005-10-30 10:13:57Z iusty $

import tempfile
import sets
import re
import fnmatch
from mx import DateTime

import cfvers
import cfvers.cmd
from cfvers.main import Result, Item, Entry, Revision

from optparse import OptionParser, OptionGroup
import os, os.path
import sys
import errno
import types
import time
import getpass
try:
    import readline
except ImportError:
    pass

# Return codes for store, add commands
RET_INVALID = 1 << 0
RET_STORED  = 1 << 1
RET_NOTCHANGED = 1 << 2
RET_ERRORS = 1 << 3
RET_DELETED = 1 << 4

class CLI(object):
    def __init__(self):
        commands = {
            'find':     (self.cmd_findentries, "", 'Finds entries by their properties'),
            'log':      (self.cmd_log, "[files...]", 'Show changelog of items'),
            'store':    (self.cmd_store, "[files...]", 'Store new versions of items'),
            'retrieve': (self.cmd_retrieve, "[files...]", 'Retrieves items from the repository'),
            'cat':      (self.cmd_cat, "<filename>", 'Shows the contents of an item'),
            'diff':     (self.cmd_diff, "[files...]", 'Shows differences between versions'),
            'stat':     (self.cmd_stat, "[files...]", 'Show metadata of an item'),
            'export':   (self.cmd_export, "", 'Exports the repository to an external format'),
            'add':      (self.cmd_add, "<file>...", 'Adds new items to the repository'),
            'addnew':   (self.cmd_addnew, "", 'Adds new items in the tracke directories'),
            'list':     (self.cmd_list, "", 'List the items registered in the repository'),
            'register': (self.cmd_register, "<name> <command>", "Register a virtual item in the repository")
            }
        
        self.ts_begin = time.time()
        self.curr_cmd = "<command>"
        self.usage="usage: %prog [global options] command [command options and arguments]\nwhere command is one of\n"
        l = commands.keys()
        l.sort()
        for i in l:
            self.usage += "    %-8s %-16s %s\n" % (i, commands[i][1], commands[i][2])
        self.usage += "\nGlobal options:"
        self.commands = commands
        return

    def get_scp(self, earg, find=False):
        """Returns a sub-command parser"""
        op = OptionParser(version=cfvers.cmd.CLIScript.get_version(),
                          usage="%%prog [global options] %s [options] " \
                          "%s\nFor global options, see %%prog --help" \
                          % (self.curr_cmd, earg))
        if find:
            ogg = OptionGroup(op, "Global filters", description="global selection options")
            # Global toggles
            ogg.add_option("--all-areas", dest="allareas",
                          help="search on all areas",
                          default=False, action="store_true",
                          )
            ogg.add_option("--all-entries", dest="allentries",
                          help="return all matching entries for a file, not only latest",
                          default=False, action="store_true",
                          )

            ogr = OptionGroup(op, "Revision filters", description="revision selection options")
            # Revision options
            ogr.add_option("--logmsg", dest="find_logmsg",
                          help="revision log message matches regexp PATTERN",
                          default=None, metavar="PATTERN")
            ogr.add_option("--revno", dest="find_revno", action="append",
                          help="revision number is OP N", metavar="OP N",
                          default=None, nargs=2)
            oge = OptionGroup(op, "Entry filters", description="entry selection options")
            # Entry options
            oge.add_option("--empty", dest="find_empty",
                          help="file is empty and is either a regular file or a directory",
                          default=False, action="store_true")
            oge.add_option("--gid", dest="find_gid", action="append",
                          help="file's numeric group ID is OP n", nargs=2,
                          default=None, type="string", metavar="OP N")
            oge.add_option("--group", dest="find_group", action="append",
                          help="file's group name is OP gname", nargs=2,
                          default=None, type="string", metavar="OP gname")
            oge.add_option("--uid", dest="find_uid", action="append",
                          help="file's numeric user ID is OP n", nargs=2,
                          default=None, type="string", metavar="OP N")
            oge.add_option("--user", dest="find_user", action="append",
                          help="file's user name is OP uname", nargs=2,
                          default=None, type="string", metavar="OP uname")
            oge.add_option("--regex", dest="find_regex", action="append",
                          help="file name matches regular expression PATTERN; this is a match on the whole path, not a search",
                          default=None, type="string", metavar="PATTERN")
            oge.add_option("--iregex", dest="find_iregex", action="append",
                          help="like -regex, but the match is case insensitive",
                          default=None, type="string", metavar="PATTERN")
            oge.add_option("--size", dest="find_size", action="append",
                          help="file's size is OP n", nargs=2,
                          default=None, type="string", metavar="OP N")
            oge.add_option("--type", dest="find_type", action="append",
                          help="file's type is/is not n", nargs=2,
                          default=None, type="string", metavar="OP N")
            oge.add_option("--nouser", dest="find_nouser",
                          help="no user corresponded to file's numeric user ID at store time",
                          default=False, action="store_true")
            oge.add_option("--nogroup", dest="find_nogroup",
                          help="no group corresponded to file's numeric group ID at store time",
                          default=False, action="store_true")
            oge.add_option("--name", dest="find_name", action="append",
                          help="base of file name (the path with the leading directories removed) matches shell pattern GLOB",
                          default=None, type="string", metavar="GLOB")
            oge.add_option("--iname", dest="find_iname", action="append",
                          help="like -name, but the match is case insensitive",
                          default=None, type="string", metavar="GLOB")
            oge.add_option("--path", dest="find_path", action="append",
                          help="file name matches shell pattern GLOB",
                          default=None, type="string", metavar="GLOB")
            oge.add_option("--ipath", dest="find_ipath", action="append",
                          help="like -path, but the match is case insensitive",
                          default=None, type="string", metavar="GLOB")
            oge.add_option("--lname", dest="find_lname", action="append",
                          help="file is a symbolic link whose contents match shell pattern GLOB",
                          default=None, type="string", metavar="GLOB")
            oge.add_option("--ilname", dest="find_ilname", action="append",
                          help="like -lname, but the match is case insensitive",
                          default=None, type="string", metavar="GLOB")
            oge.add_option("--links", dest="find_links", action="append",
                          help="file has OP n links", nargs=2,
                          default=None, type="string", metavar="OP n")
            oge.add_option("--inum", dest="find_inum", action="append",
                          help="file has inode number OP n", nargs=2,
                          default=None, type="string", metavar="OP n")
            oge.add_option("--perm", dest="find_perm", action="append",
                          help="file's permission bits check", nargs=2,
                          default=None, type="string", metavar="OP n")
            op.add_option_group(ogg)
            op.add_option_group(ogr)
            op.add_option_group(oge)
        return op

    def prompt_func(self, key):
        prompt = "Please enter the %s: " % key
        if "password" in key:
            value = getpass.getpass(prompt)
        else:
            value = raw_input(prompt)
        return value
    
    def main(self, argv):
        op = OptionParser(version=cfvers.cmd.CLIScript.get_version(),
                          usage=self.usage)
        op.disable_interspersed_args()

        op.add_option("-a", "--area", dest="area",
                      help="the area to work on",
                      type="string", default=None,
                      metavar="AREA")

        op.add_option("--local", dest="server_type",
                      action="store_const", const="local",
                      help="connect locally (not through the server)")
        op.add_option("--remote", dest="server_type",
                      action="store_const", const="remote",
                      help="connect through the server")
        ogl = OptionGroup(op, "Local options")
        ogl.add_option("--rtype", dest="repo_meth",
                       type="choice", choices=('postgresql', 'sqlite'),
                       help="repository type")
        ogl.add_option("--rdata", dest="repo_data",
                       type="string",
                       help="repository connect information")
        op.add_option_group(ogl)

        ogr = OptionGroup(op, "Remote options")
        ogr.add_option("-s", "--server", dest="host",
                       help="the host to connect to",
                       type="string", default=None,
                       metavar="HOSTNAME")

        ogr.add_option("-p", "--port", dest="port",
                       help="the port on the server",
                       type="int", default=None,
                       metavar="PORT")

        ogr.add_option("-u", "--username", dest="username",
                       help="the username for authentication to the server",
                       type="string", default=None,
                       metavar="USERNAME")
        op.add_option_group(ogr)

        (options, args) = op.parse_args(argv)
        if len(args) == 0:
            op.print_help()
            return
        command = args.pop(0)
        if not command in self.commands:
            cfound = []
            for fcom in self.commands:
                if fcom.startswith(command):
                    cfound.append(fcom)
            if len(cfound) > 1:
                print >> sys.stderr, "Ambigous command '%s' (%s)" % (command, ",".join(cfound))
                op.print_help()
                return
            elif len(cfound) == 0:
                print >>sys.stderr, "Unknown command %s" % command
                op.print_help()
                return
            command = cfound[0]
        try:
            self.cmdi = cfvers.cmd.Commands(options, self.prompt_func)
        except cfvers.ConfigException, e:
            print >>sys.stderr, "Error: your configuration is invalid.\nError message: %s." % e
            return
        except cfvers.RepositoryException, e:
            print >>sys.stderr, "Error: repository opening failed.\nError message: %s." % e
            return
        except cfvers.CommException, e:
            print >>sys.stderr, "Error: communication error.\nError message: %s" % (" ".join(e.args),)
            return
        except KeyboardInterrupt:
            print "Abort."
            return
        except Exception, e:
            print >>sys.stderr,"Error while initializing: %s" % e
            raise
            return
        self.curr_cmd = command
        try:
            func = self.commands[command][0]
            retcode = func(options, args)
        finally:
            self.cmdi.close()
        return retcode
        
    def cmd_list(self, options, args):
        """Lists the items in the repository"""
        op = self.get_scp("")
        op.add_option("--name", dest="name",
                      help="name matches shell-glob NAME",
                      metavar="NAME", type="string",
                      default=None)
        op.add_option("--regexp", dest="regexp",
                      help="name matches regular expression REGEXP (not anchored)",
                      metavar="REGEXP", type="string",
                      default=None)
        dformat = "%(id)s %(flag)c %(name)s   %(command)s"
        op.add_option("-F", "--format", dest="format",
                      help='the listing format, please see the manual; ' \
                      'default is "%s"' % dformat,
                      default=dformat,
                      metavar="FORMAT")

        (cmdopts, cmdargs) = op.parse_args(args)
        if cmdopts.regexp is not None:
            cmdopts.regexp = re.compile(cmdopts.regexp)
        for i in self.cmdi.portal.getItems(self.cmdi.area.name):
            if cmdopts.regexp is not None and \
                   not cmdopts.regexp.search(i.name):
                continue
            if cmdopts.name is not None and \
                   not fnmatch.fnmatch(i.name, cmdopts.name):
                continue
            mydict = {
                'name': "%s" % i.name,
                'id': "%6d" % i.id,
                'ctime': i.ctime,
                'command': "",
                }
            if i.command is not None:
                mydict['command'] = '=`%s`' % i.command
            if i.flags & Item.STORE_VIRTUAL:
                mydict['flag'] = 'V'
            elif i.flags & Item.STORE_CONTENTS:
                mydict['flag'] = 'F'
            elif i.flags & Item.STORE_CHECKSUM:
                mydict['flag'] = 'C'
            elif i.flags & Item.STORE_METADATA:
                mydict['flag'] = 'M'
            elif i.flags > 0:
                mydict['flag'] = '?'
            else:
                mydict['flag'] = 'N'
            try:
                print cmdopts.format % mydict
            except (KeyError, TypeError, ValueError), e:
                print >>sys.stderr, "Invalid format. Error details: %s" % e
                break
        return

    def cmd_findentries(self, options, args):
        """Searches in the repository"""
        op = self.get_scp("[files...]", find=True)
        op.add_option("-d", "--detail", dest="detail",
                      help="shows detailed information",
                      default=None, action="store_true",
                      )
        op.add_option("-l", "--long", dest="long",
                      help="shows 'ls -ld' like details",
                      default=None, action="store_true",
                      )
        (cmdopts, cmdargs) = op.parse_args(args)
        cmdopts.do_payload = False
        cmdopts.match_and = True
        cmdopts.area = self.cmdi.area.name
        try:
            entries = self.cmdi.portal.getEntries(cmdopts)
        except cfvers.main.ParsingException, e:
            print "Invalid search options: error `%s'" % str(e)
            return
        
        for e in entries:
            if cmdopts.detail:
                #print "-" * 25
                print "File %s" % e.filename
                i = self.cmdi.portal.getItemByID(e.item)
                print "Registered at: %s" % (i.ctime.localtime().strftime("%F %T"))
                revs = self.cmdi.portal.getRevNumbers(i.id)
                rlist = Revision.format_crevnos(Revision.collapse_revnos(revs))
                print "Available revisions: %s" % rlist
                print
            elif cmdopts.long:
                if e.mtime is not None:
                    m = DateTime.TimestampFromTicks(e.mtime).strftime("%Y-%m-%d %T")
                else:
                    m = 'N/A'
                print "%s %5d %-8s %-8s %8d %s %s" % \
                      (e.mode2str(), e.revno,
                       e.uname or e.uid, e.gname or e.gid,
                       e.size,
                       m, e.filename)
            else:
                print e.filename
        return

    def cmd_add(self, options, args):
        """Register some items in the repository"""
        op = self.get_scp("<file>...")
        op.add_option("-c", "--commiter", dest="commiter",
                      help="commiter information",
                      type="string", default=None)
        op.add_option("-m", "--logmsg", dest="logmsg",
                      help="log message",
                      type="string", default=None,
                      metavar="MESSAGE")
        op.add_option("-N", "--no-recurse", dest="norecurse",
                      help="disable recursion on directories",
                      action="store_true", default=False)
        op.add_option("-q", "--quiet", dest="quiet",
                      help="only display errors",
                      action="store_true", default=False)
        op.add_option("-v", "--verbose", dest="verbose",
                      help="display additional information",
                      action="store_true", default=False)
        op.add_option("-i", "--inputfile", dest="inputfile",
                      help="File which contains additional items to store",
                      default=None)
        op.add_option("--store", dest="storage",
                      choices=('none', 'metadata', 'checksum', 'full'),
                      help="Level of information stored: none (to " \
                      "prevent an item from being stored at all), " \
                      "metadata (only metadata), checksum (metadata "\
                      "and checksum), full (including contents)",
                      default="full")
        (cmdoptions, cmdargs) = op.parse_args(args)
        if cmdoptions.storage == "none":
            cmdoptions.flags = 0
        elif cmdoptions.storage == "metadata":
            cmdoptions.flags = Item.STORE_METADATA
        elif cmdoptions.storage == "checksum":
            cmdoptions.flags = Item.STORE_METADATA | Item.STORE_CHECKSUM
        elif cmdoptions.storage == "full":
            cmdoptions.flags = Item.STORE_METADATA | \
                               Item.STORE_CHECKSUM | \
                               Item.STORE_CONTENTS
        else:
            raise ValueError("Invalid value for storage type")
        
        if cmdoptions.logmsg is None:
            if cmdoptions.inputfile == "-":
                print >> sys.stderr, "Log message not specified and input list was specified on stdin!"
                print >> sys.stderr, "Please specify log message using -m"
                print >> sys.stderr, "Commit aborted."
                return RET_INVALID
            elif sys.stdin.isatty() and sys.stdout.isatty():
                nt = self.get_text("")
                if len(nt) == 0:
                    print >> sys.stderr, "Commit aborted."
                    return RET_INVALID
                cmdoptions.logmsg = nt
            else:
                print >> sys.stderr, "Log message not specified and stdin/stout are not ttys."
                print >> sys.stderr, "Commit aborted."
                return RET_INVALID
        if cmdoptions.inputfile is not None:
            try:
                if cmdoptions.inputfile == "-":
                    f = sys.stdin
                else:
                    f = file(cmdoptions.inputfile)
                cmdargs += map(lambda line: line.rstrip('\n'), f.readlines())
                f.close()
            except EnvironmentError, e:
                print >> sys.stderr, "Can't read input file: %s" % e
                return RET_INVALID
        if len(cmdargs) == 0:
            print >> sys.stderr, "No items given."
            print >> sys.stderr, "Commit aborted."
            return RET_INVALID
        errors, store_done, newrev = self.cmdi.add(files=cmdargs, options=cmdoptions)
        self.ts_end = time.time()

        totals = {}
        totals[Result.ADDED_OK] = 0
        totals[Result.ADDED_EXISTING] = 0
        totals[Result.STORED_IOERROR] = 0
        totals[Result.ADDED_INVALIDNAME] = 0
        for res in errors:
            totals[res.code] += 1
        if not cmdoptions.quiet:
            if store_done:
                print "Status: Added, revision %d" % newrev
            else:
                print "Status: Commit not done"
            print "Time begin: %s" % time.strftime("%Y-%m-%d %T %Z", time.localtime(self.ts_begin))
            print "Time end:   %s" % time.strftime("%Y-%m-%d %T %Z", time.localtime(self.ts_end))
            l = totals.keys(); l.sort()
            for code in l:
                print "Total %s: %d" % (Result._codeinfo[code].lower(), totals[code])
        for res in errors:
            if res.critical:
                print >> sys.stderr, str(res)
            else:
                if cmdoptions.verbose:
                    print >> sys.stdout, str(res)
        retcode = 0
        if totals[Result.ADDED_INVALIDNAME] > 0:
            retcode |= RET_INVALID
        if totals[Result.ADDED_EXISTING] > 0:
            retcode |= RET_NOTCHANGED
        if totals[Result.ADDED_OK] > 0:
            retcode |= RET_STORED
        return retcode

    def cmd_addnew(self, options, args):
        """Register new items in the directories existing in the repository"""
        op = self.get_scp("")
        op.add_option("-c", "--commiter", dest="commiter",
                      help="commiter information",
                      type="string", default=None)
        op.add_option("-m", "--logmsg", dest="logmsg",
                      help="log message",
                      type="string", default=None,
                      metavar="MESSAGE")
        op.add_option("-N", "--no-recurse", dest="norecurse",
                      help="disable recursion on directories",
                      action="store_true", default=False)
        op.add_option("-q", "--quiet", dest="quiet",
                      help="only display errors",
                      action="store_true", default=False)
        op.add_option("-v", "--verbose", dest="verbose",
                      help="display additional information",
                      action="store_true", default=False)
        (cmdoptions, cmdargs) = op.parse_args(args)
        
        if cmdoptions.logmsg is None:
            if cmdoptions.inputfile == "-":
                print >> sys.stderr, "Log message not specified and input list was specified on stdin!"
                print >> sys.stderr, "Please specify log message using -m"
                print >> sys.stderr, "Commit aborted."
                return RET_INVALID
            elif sys.stdin.isatty() and sys.stdout.isatty():
                nt = self.get_text("")
                if len(nt) == 0:
                    print >> sys.stderr, "Commit aborted."
                    return RET_INVALID
                cmdoptions.logmsg = nt
            else:
                print >> sys.stderr, "Log message not specified and stdin/stout are not ttys."
                print >> sys.stderr, "Commit aborted."
                return RET_INVALID

        errors, store_done, newrev = self.cmdi.addfromdirs(options=cmdoptions)
        self.ts_end = time.time()

        totals = {}
        totals[Result.ADDED_OK] = 0
        totals[Result.STORED_IOERROR] = 0
        totals[Result.ADDED_EACCES] = 0
        for res in errors:
            totals[res.code] += 1
        if not cmdoptions.quiet:
            if store_done:
                print "Status: Added, revision %d" % newrev
            else:
                print "Status: Commit not done"
            print "Time begin: %s" % time.strftime("%Y-%m-%d %T %Z", time.localtime(self.ts_begin))
            print "Time end:   %s" % time.strftime("%Y-%m-%d %T %Z", time.localtime(self.ts_end))
            l = totals.keys(); l.sort()
            for code in l:
                print "Total %s: %d" % (Result._codeinfo[code].lower(), totals[code])
        for res in errors:
            if res.critical:
                print >> sys.stderr, str(res)
            else:
                if cmdoptions.verbose:
                    print >> sys.stdout, str(res)
        retcode = 0
        if totals[Result.ADDED_OK] > 0:
            retcode |= RET_STORED
        return retcode

    def cmd_register(self, options, args):
        """Register a virtual item in the repository"""
        op = self.get_scp("<name> <command line>...")
        op.add_option("-c", "--commiter", dest="commiter",
                      help="commiter information",
                      type="string", default=None)
        op.add_option("-m", "--logmsg", dest="logmsg",
                      help="log message",
                      type="string", default=None,
                      metavar="MESSAGE")
        op.add_option("-q", "--quiet", dest="quiet",
                      help="only display errors",
                      action="store_true", default=False)
        (cmdoptions, cmdargs) = op.parse_args(args)
        
        if cmdoptions.logmsg is None:
            if sys.stdin.isatty() and sys.stdout.isatty():
                nt = self.get_text("")
                if len(nt) == 0:
                    print >> sys.stderr, "Commit aborted."
                    return RET_INVALID
                cmdoptions.logmsg = nt
            else:
                print >> sys.stderr, "Log message not specified and stdin/stout are not ttys."
                print >> sys.stderr, "Commit aborted."
                return RET_INVALID

        if len(cmdargs) < 2:
            print >> sys.stderr, "No name and command line given."
            print >> sys.stderr, "Commit aborted."
            return RET_INVALID
        name = cmdargs.pop(0)
        if not name.startswith("/"):
            print >> sys.stderr, "Error: name must be an absolute path (i.e. it must begin with /)"
            return RET_INVALID
        result, store_done, newrev = self.cmdi.register(name, cmdargs, cmdoptions)

        if result.code == Result.ADDED_OK:
            msg = "Item registered"
            retcode = RET_STORED
        elif result.code == Result.ADDED_EXISTING:
            msg = "Item skipped (already registered)"
            retcode = RET_NOTCHANGED
        else:
            msg = "Unhandled result code: %s" % \
                  Result._codeinfo[result.code].lower()
            retcode = RET_INVALID
        if not cmdoptions.quiet:
            if store_done:
                print "Status: Added, revision %d" % newrev
            else:
                print "Status: Commit not done"
            print msg
        return retcode

    def cmd_store(self, options, args):
        """Store some items in the repository"""
        op = self.get_scp("[files...]")
        op.add_option("-c", "--commiter", dest="commiter",
                      help="commiter information",
                      type="string", default=None)
        op.add_option("-m", "--logmsg", dest="logmsg",
                      help="log message",
                      type="string", default=None,
                      metavar="MESSAGE")
        op.add_option("-N", "--no-recurse", dest="norecurse",
                      help="disable recursion on directories",
                      action="store_true", default=False)
        op.add_option("-q", "--quiet", dest="quiet",
                      help="only display errors",
                      action="store_true", default=False)
        op.add_option("-v", "--verbose", dest="verbose",
                      help="display additional information",
                      action="store_true", default=False)
        op.add_option("-i", "--inputfile", dest="inputfile",
                      help="File which contains additional items to store",
                      default=None)
        (storeoptions, storeargs) = op.parse_args(args)
        if storeoptions.logmsg is None:
            if storeoptions.inputfile == "-":
                print >> sys.stderr, "Log message not specified and input list was specified on stdin!"
                print >> sys.stderr, "Please specify log message using -m"
                print >> sys.stderr, "Commit aborted."
                return RET_INVALID
            elif sys.stdin.isatty() and sys.stdout.isatty():
                nt = self.get_text("")
                if len(nt) == 0:
                    print >> sys.stderr, "Commit aborted."
                    return RET_INVALID
                storeoptions.logmsg = nt
            else:
                print >> sys.stderr, "Log message not specified and stdin/stout are not ttys."
                print >> sys.stderr, "Commit aborted."
                return RET_INVALID
        if storeoptions.inputfile is not None:
            try:
                if storeoptions.inputfile == "-":
                    f = sys.stdin
                else:
                    f = file(storeoptions.inputfile)
                storeargs += map(lambda line: line.rstrip('\n'), f.readlines())
                f.close()
            except EnvironmentError, e:
                print >> sys.stderr, "Can't read input file: %s" % e
                return RET_INVALID
        errors, store_done, newrev = self.cmdi.store(files=storeargs, options=storeoptions)
        self.ts_end = time.time()

        totals = {}
        totals[Result.STORED_DELETED] = 0
        totals[Result.STORED_IOERROR] = 0
        totals[Result.STORED_NOTCHANGED] = 0
        totals[Result.STORED_NOTREG] = 0
        totals[Result.STORED_OK] = 0
        totals[Result.STORED_TOSKIP] = 0
        for res in errors:
            totals[res.code] += 1
        if not storeoptions.quiet:
            if store_done:
                print "Status: Stored revision %d" % newrev
            else:
                print "Status: Commit not done"
            print "Time begin: %s" % time.strftime("%Y-%m-%d %T %Z", time.localtime(self.ts_begin))
            print "Time end:   %s" % time.strftime("%Y-%m-%d %T %Z", time.localtime(self.ts_end))
            l = totals.keys(); l.sort()
            for code in l:
                print "Total %s: %d" % (Result._codeinfo[code].lower(), totals[code])
        for res in errors:
            if res.critical:
                print >> sys.stderr, str(res)
            else:
                if storeoptions.verbose:
                    print >> sys.stdout, str(res)
        retcode = 0
        if totals[Result.STORED_IOERROR] > 0:
            retcode |= RET_ERRORS
        if totals[Result.STORED_NOTCHANGED] > 0:
            retcode |= RET_NOTCHANGED
        if totals[Result.STORED_OK] > 0:
            retcode |= RET_STORED
        if totals[Result.STORED_DELETED] > 0:
            retcode |= RET_DELETED
        return retcode

    def cmd_retrieve(self, options, args):
        """Retrieve some items from the repository"""
        op = self.get_scp("[files...]")
        op.add_option("-d", "--destdir", dest="destdir",
                      help="destination directory",
                      type="string", default=None,
                      metavar="PATH")
        op.add_option("-N", "--no-recurse", dest="norecurse",
                      help="disable recursion on directories restored",
                      action="store_true", default=False)
        op.add_option("-q", "--quiet", dest="quiet",
                      help="only display errors",
                      action="store_true", default=False)
        op.add_option("-r", "--revno", dest="revno",
                      help="revision number to be retrieved",
                      metavar="REVNO", type="int")
        op.add_option("-s", "--skip-dirs", dest="use_dirs",
                      help="together with -d, extract files directly under PATH, ignoring their stored paths",
                      default=1, action="store_false")
        op.add_option("-v", "--verbose", dest="verbose",
                      help="display additional information",
                      action="store_true", default=False)

        (retroptions, retrargs) = op.parse_args(args)
        results = self.cmdi.retrieve(files=retrargs, options=retroptions)
        totals = {}
        if not retroptions.quiet:
            for res in results:
                totals[res.code] = totals.setdefault(res.code, 0) + 1
            l = totals.keys()
            l.sort()
            for code in l:
                print "Total %s: %d" % (Result._codeinfo[code].lower(), totals[code])
        for res in results:
            if res.critical:
                print >> sys.stderr, str(res)
            else:
                if retroptions.verbose:
                    print str(res)
        return

    def cmd_diff(self, options, args):
        """Show the difference between versions for selected items"""
        op = self.get_scp("filename...")
        op.add_option("-r", "--revno", dest="rev",
                      help="revision number(s) to be compared",
                      metavar="REVNO[:REVNO]",
                      default=None
                      )
        op.add_option("-l", "--list", dest="list",
                      help="just list filenames which are different",
                      action="store_true", default=False,
                      )
        op.add_option("-c", "--check", dest="checks",
                      help="check this attribute (multiple options allowed); overrides --no-check",
                      action="append", default=None,
                      type="choice", choices=cfvers.Entry.DIFFABLE_ATTRS,
                      metavar="ATTR",
                      )
        op.add_option("-n", "--no-check", dest="nochecks",
                      help="don't check this attribute (multiple options allowed)",
                      action="append", default=None,
                      type="choice", choices=cfvers.Entry.DIFFABLE_ATTRS,
                      metavar="ATTR",
                      )

        
        (diffoptions, diffargs) = op.parse_args(args)
        if diffoptions.checks is None:
            diffoptions.checks = list(cfvers.Entry.DIFFABLE_ATTRS)
            if diffoptions.nochecks is not None:
                for i in diffoptions.nochecks:
                    if i in diffoptions.checks:
                        diffoptions.checks.remove(i)
        
        results = self.cmdi.diff(options=diffoptions, files=diffargs)
        if diffoptions.list:
            for name, status, err, data, e1, e2 in results:
                if not status or len(data) == 0:
                    print name
        else:
            for areao, nameo, arean, namen, status, err, data, e1, e2 in results:
                if status and len(data) == 0:
                    continue
                if (nameo == namen or namen is None) and (areao == arean or arean is None):
                    print "===== %s" % nameo,
                else:
                    if areao == arean:
                        print "===== %s - %s" % (nameo, namen),
                    else:
                        print "===== %s:%s - %s:%s" % (areao, nameo, arean, namen),
                if e1 is not None and e2 is not None:
                    if e1.revno == e2.revno:
                        print "(rev %s)" % e1.revno,
                    else:
                        print "(rev %s -> %s)" % (e1.revno, e2.revno or 'current'),
                if not status:
                    print ": can't compute diff, error: %s" % err
                    continue
                print
                if e1 is not None and e2 is not None and e1.status != e2.status:
                    print "File has gone from status `%s' to `%s'" % \
                          (Entry.STATUS_MAP[e1.status],
                           Entry.STATUS_MAP[e2.status])
                    continue
                for row in data:
                    if len(row) == 1:
                        print row[0]
                    elif len(row) == 2:
                        print "%s:\n%s\n" % (row[0], row[1])
                    elif len(row) == 3:
                        kind, old, new = row
                        if type(old) == list and type(new) == list:
                            print "%s:" % kind
                            for name in old:
                                print "- %s" % name
                            for name in new:
                                print "+ %s" % name
                            print
                        else:
                            print "%s:\n- %s\n+ %s\n" % (kind, old, new)
        return

    def cmd_log(self, options, args):
        """Show the editing history for selected items"""
        op = self.get_scp("[files...]")
        op.add_option("-r", "--revno", dest="rev",
                      help="revision number(s) to be listed",
                      metavar="REVNO[:REVNO]",
                      default=None
                      )
        op.add_option("-l", "--list", dest="list",
                      help="only list number and date",
                      default=False, action="store_true",
                      )
        (cmdopts, cmdargs) = op.parse_args(args)
        arearevs = self.cmdi.log()
        if len(cmdargs) > 0:
            itemrevs = sets.Set()
            for itname in cmdargs:
                item = self.cmdi.portal.getItemByName(self.cmdi.area.name, itname)
                if item is None:
                    raise ValueError, "Item %s not found in repository!" % itname
                itemrevs |= sets.Set(self.cmdi.portal.getRevNumbers(item.id))
        else:
            itemrevs = sets.Set([x.revno for x in arearevs])
        if cmdopts.rev is not None:
            r1, r2 = cfvers.cmd.CLIScript.parserev(cmdopts.rev)
            if r1 is not None and r2 is not None:
                givenrevs = sets.Set(range(r1, r2+1))
            elif r1 is None and r2 is not None:
                givenrevs = sets.Set([r2])
            elif r1 is not None and r2 is None:
                givenrevs = sets.Set([r1])
            selrevs = itemrevs & givenrevs
        else:
            selrevs = itemrevs
        if cmdopts.list:
            for ar in arearevs:
                if ar.revno in selrevs:
                    print "%4d %12s %12s %s %s" % \
                          (ar.revno, ar.server, ar.commiter,
                           ar.ctime.localtime().strftime("%Y-%m-%d %T"), ar.ctime.tz)
        else:
            for ar in arearevs:
                if ar.revno in selrevs:
                    print "-" * 80
                    print "Revision number:  %d" % ar.revno
                    print "Source server:    %s" % ar.server
                    print "Date entered:     %s %s" % (ar.ctime.localtime().strftime("%F %T"), ar.ctime.tz)
                    print "Commiter info:    %s" % ar.commiter
                    print "Commiter uid/gid: %d/%d" % (ar.uid, ar.gid)
                    print "Modified items:"
                    for itemid, itemname, entrystatus in \
                            self.cmdi.portal.getRevisionItems(ar.area, ar.revno):
                        if entrystatus == cfvers.Entry.STATUS_ADDED:
                            char = "A"
                        elif entrystatus == cfvers.Entry.STATUS_DELETED:
                            char = "D"
                        elif entrystatus == cfvers.Entry.STATUS_MODIFIED:
                            char = "M"
                        else:
                            char = "?"
                        print "   %c %s" % (char, itemname)
                    print "Log message:"
                    print ar.logmsg
            if len(selrevs) > 0:
                print "-" * 80
        return

    def cmd_cat(self, options, args):
        """Displays the contents of selected items"""
        op = self.get_scp("filename")
        op.add_option("-r", "--revno", dest="rev",
                      help="revision number to be shown",
                      metavar="REVNO")
        (catoptions, catargs) = op.parse_args(args)
        if len(catargs) == 0:
            print >>sys.stderr, "Missing filename!"
            sys.exit(1)
        if len(catargs) > 1:
            print >>sys.stderr, "Only one filename allowed!"
            sys.exit(1)
        try:
            data = self.cmdi.show(catargs[0], rev=catoptions.rev)
            sys.stdout.write(data)
        except cfvers.OperationError, e:
            print >>sys.stderr, "Error: %s" % e
        return

    def cmd_stat(self, options, args):
        op = self.get_scp("[files...]")
        op.add_option("-r", "--revno", dest="rev",
                      help="revision number to be shown",
                      metavar="REVNO")
        (cmdopts, cmdargs) = op.parse_args(args)
        results = self.cmdi.stat(cmdopts, cmdargs)
        for name, err, data in results:
            if err is not None:
                print "cannot stat `%s': %s" % (name, err)
                continue
            sys.stdout.write(data)
        return

    def cmd_export(self, options, args):
        op = self.get_scp("", find=True)
        op.add_option("-F", "--format", dest="format",
                      help="the export format, one of sha1sum or tar [sha1sum]",
                      metavar="FORMAT", default="sha1sum",
                      type="choice", choices=("tar", "sha1sum"))
        op.add_option("-o", "--output", dest="output",
                      help="the name of the destination file [STDOUT]",
                      metavar="FILENAME", default="-")
        (cmdopts, cmdargs) = op.parse_args(args)
        if len(cmdargs) > 0:
            print >>sys.stderr, "The export command takes no arguments, please see the help"
            return
        if cmdopts.format not in ("tar", "sha1sum"):
            print >>sys.stderr, "Unknown export format '%s'!" % cmdopts.format
            sys.exit(1)
            
        if cmdopts.output == "-":
            output = sys.stdout
            close = False
        else:
            output = file(cmdopts.output, "w")
            close = True
        try:
            self.cmdi.export(cmdopts, output)
        finally:
            if close:
                output.close()
        return
    
    def get_text(self, extratext):
        """Method to get a text from the user"""
        fd, fname = tempfile.mkstemp()
        fo = os.fdopen(fd, "w+")
        guardian = "---- This line and the rest below won't be included ----"
        fo.write("\n")
        fo.write(guardian)
        fo.write("\n")
        fo.write(extratext)
        fo.flush()
        editor=os.getenv("EDITOR")
        if editor is None:
            editor="vi"
        try:
            os.system("%s %s" % (editor,fname))
            fo.seek(0, 0)
            ndata = fo.read()
            lin = ndata.split("\n")
            realtext = []
            for i in lin:
                if i != guardian:
                    realtext.append(i)
                else:
                    break
            if len(realtext) > 0 and realtext[-1] == "\n":
                realtext.pop()
            return "\n".join(realtext)
        finally:
            fo.close()
            os.unlink(fname)
        return ""
            

if __name__ == "__main__":
    os.stat_float_times(False)
    cli = CLI()
    try:
        retcode = cli.main(sys.argv[1:])
    except IOError, e:
        if e.errno == errno.EPIPE:
            print >> sys.stderr, "Broken pipe."
            sys.exit(1)
        raise
    except KeyboardInterrupt:
        print >> sys.stderr, "Abort."
        sys.exit(1)
    if retcode is not None:
        sys.exit(retcode)
