# Part of the A-A-P recipe executive: CVS access

# Copyright (C) 2002-2003 Stichting NLnet Labs
# Permission to copy and use this file is specified in the file COPYING.
# If this file is missing you can find it here: http://www.a-a-p.org/COPYING

#
# Functions to get files out of a CVS repository and put them back.
# See interface.txt for an explanation of what each function does.
#

import string
import os
import os.path

from Error import *
from Message import *
from Util import *

def cvs_command(recdict, server, url_dict, nodelist, action):
    """Handle CVS command "action".
       Return non-zero when it worked."""
    # Since CVS doesn't do locking, quite a few commands can be simplified:
    if action == "fetch":
        action = "checkout"    # "fetch" is exactly the same as "checkout"
    elif action in [ "checkin", "publish" ]:
        action = "commit"   # "checkin" and "publish" are the same as "commit"
    elif action == "unlock":
        return []           # unlocking is a no-op

    # All CVS commands require an argument to specify where the server is.
    if not server:
        # Obtain the previously used CVSROOT from CVS/Root.
        # There are several of these files that contain the same info, just use
        # the one in the current directory.
        try:
            f = open("CVS/Root")
        except StandardError, e:
            msg_extra(recdict,
                   _('Cannot open for obtaining CVSROOT: "CVS/Root"') + str(e))
        else:
            try:
                server = f.readline()
                f.close()
            except StandardError, e:
                msg_warning(recdict,
                   _('Cannot read for obtaining CVSROOT: "CVS/Root"') + str(e))
                server = ''     # in case something was read
            else:
                if server[-1] == '\n':
                    server = server[:-1]
    if server:
        serverarg = "-d" + server
    else:
        serverarg = ''

    #
    # Loop over the list of nodes and handle each separately.  This is required
    # to be able to do something useful with an error message.
    # For a "tag" command all nodes with the same tag are done at once to speed
    # it up.
    #
    failed = []

    if action == "tag":
        # Loop over todolist, taking out nodes with identical tags, until it's
        # empty.
        todolist = nodelist[:]
        while todolist:
            tag = ''
            thislist = []
            for node in todolist[:]:
                # Use the specified "tag" attribute for tagging.
                if not node.attributes.has_key("tag"):
                    msg_error(recdict, _('tag attribute missing for "%s"')
                                                           % node.short_name())
                    failed.extend(todolist)
                    failed.extend(thislist)
                    return failed
                if not tag or node.attributes["tag"] == tag:
                    thislist.append(node)
                    todolist.remove(node)
                    tag = node.attributes["tag"]
            f = cvs_tag(recdict, serverarg, tag, thislist)
            if f:
                failed.extend(f)

    else:
        for node in nodelist:
            if not cvs_command_node(recdict, serverarg, url_dict, node, action):
                failed.append(node)

    return failed


def cvs_prepare(recdict):
    """
    Prepare for using the cvs command.  Returns the program name.
    """
    # Install cvs when needed.
    from DoInstall import assert_pkg
    assert_pkg([], recdict, "cvs")

    # Create ~/.cvspass if it doesn't exist.  An empty file should be
    # sufficient for anonymous logins.
    if os.environ.get("HOME"):
        fn = os.path.expanduser("~/.cvspass")
        if not os.path.exists(fn):
            from Commands import touch_file
            touch_file(fn, 0644)

    n = get_var_val(0, recdict, "_no", "CVSCMD")
    if n:
        return n

    # Use $CVS if defined, otherwise use "cvs".
    return get_progname(recdict, "CVS", "cvs", "")


def cvs_tag(recdict, serverarg, tag, nodelist):
    """Handle CVS tag command for a list of nodes.
       Return list of nodes that failed."""
    msg_info(recdict, _('CVS tag for nodes %s')
                                % str(map(lambda x: x.short_name(), nodelist)))

    # Prepare for using CVS and get the command name.
    cvscmd = cvs_prepare(recdict)

    names = ''
    for node in nodelist:
        names = names + '"' + node.short_name() + '" '

    # TODO: check which of the nodes actually failed
    if logged_system(recdict,
                 '"%s" %s tag "%s" %s' % (cvscmd, serverarg, tag, names)) == 0:
        return []
    return nodelist


def cvs_get_repository(recdict, dir):
    """Get the first line of the CVS/Repository file in directory "dir"."""
    cvspath = ''
    fname = os.path.join(dir, "CVS/Repository")
    try:
        f = open(fname)
    except StandardError, e:
        # Only give this error when the directory exists, when it doesn't it's
        # probably the first time the files are checked out.
        if os.path.exists(os.path.join(dir, "CVS")):
            msg_warning(recdict,
                (_('Cannot open for obtaining path in module: "%s"')
                                                             % fname) + str(e))
    else:
        try:
            cvspath = f.readline()
            f.close()
        except StandardError, e:
            msg_warning(recdict,
                    (_('Cannot read for obtaining path in module: "%s"')
                                                             % fname) + str(e))
        else:
            if cvspath[-1] == '\n':
                cvspath = cvspath[:-1]
    return cvspath


def cvs_command_node(recdict, serverarg, url_dict, node, action):
    """Handle CVS command "action" for one node.
       Return non-zero when it worked."""

    msg_info(recdict, _('CVS %s for node "%s"') % (action, node.short_name()))

    # Count the number of directories in the node name.
    n = node.short_name()
    dirlevels = 0
    while n:
        prev = n
        n = os.path.dirname(n)
        if n == prev:
            break
        dirlevels = dirlevels + 1

    # A "checkout" only works reliably when in the top directory  of the
    # module.
    # "add" must be done in the current directory of the file.
    # Change to the directory where "path" + "node.name" is valid.
    if action == "checkout":
        cvspath = ''
        if url_dict.has_key("path"):
            # Use the specified "path" attribute.
            cvspath = url_dict["path"]
            dir_for_path = node.recipe_dir
        else:
            # Try to obtain the path from the CVS/Repository file.
            if os.path.isdir(os.path.join(node.absname, "CVS")):
                dir_for_path = node.absname
            else:
                dir_for_path = os.path.dirname(node.absname)

            cvspath = cvs_get_repository(recdict, dir_for_path)

        # Use node.recipe_dir and take off one part for each part in "path".
        dir = fname_fold(dir_for_path)
        path = fname_fold(cvspath)
        while path:
            if os.path.basename(dir) != os.path.basename(path):
                # This might happen when a module has an alias.
                msg_note(recdict, _('mismatch between path in cvs:// and tail of recipe directory: "%s" and "%s"') % (cvspath, dir_for_path))
                break
            ndir = os.path.dirname(dir)
            if ndir == dir:
                # This is probably an error somewhere...
                msg_error(recdict, _('path in cvs:// is longer than recipe directory: "%s" and "%s"') % (cvspath, dir_for_path))
                break
            dir = ndir
            npath = os.path.dirname(path)
            if npath == path:   # just in case: avoid getting stuck
                break
            path = npath
            if not path:
                break

            # Check that the CVS repository mentioned here is correct.  Bail
            # out here when it isn't, happens when a full path was used in
            # CVS/Repository (CVS sometimes does that for unknown reasons).
            # Also happens when a module "foo" is an alias for "bar/foo".
            # CVS/Repository then contains "bar/foo" but we must checkout
            # "foo", because the "bar" directory doesn't exist.
            p = cvs_get_repository(recdict, dir)
            if not p:
                break
            # Extra check for wrong assumptions about CVS/Repository contents.
            if fname_fold(os.path.basename(p)) != os.path.basename(path):
                msg_note(recdict, _('mismatch between contents of CVS/Repository at different levels: "%s" and "%s"') % (dir, path))
                break
    else:
        dir = os.path.dirname(node.absname)

    # Use the specified "logentry" attribute for a log message.
    # Only used for "commit" (also for add and remove).
    if url_dict.has_key("logentry"):
        logentry = url_dict["logentry"]
    elif node.attributes.has_key("logentry"):
        logentry = node.attributes["logentry"]
    else:
        logentry = get_var_val_int(recdict, "LOGENTRY")

    # Changing directory, don't return until going back!
    cwd = os.getcwd()
    if fname_equal(cwd, dir):
        cwd = ''        # we're already there, avoid a chdir()
    else:
        try:
            os.chdir(dir)
        except StandardError, e:
            msg_warning(recdict,
                      (_('Could not change to directory "%s"') % dir) + str(e))
            return 0

    msg_log(recdict, 'Cvs command in "%s"' % dir)

    node_name = node.short_name()

    tmpname = ''
    if action == "remove" and os.path.exists(node_name):
        # CVS refuses to remove a file that still exists, temporarily rename
        # it.  Careful: must always move it back when an exception is thrown!
        assert_aap_dir(recdict)
        tmpname = in_aap_dir(node_name)
        try:
            os.rename(node_name, tmpname)
        except:
            tmpname = ''

    try:
        # If the node has a "binary" attribute, give CVS the "-kb" argument for
        # an "add" action (also for commit with auto-add).
        if node.attributes.get("binary"):
            addbinarg = "-kb"
        else:
            addbinarg = ""

        ok = exec_cvs_cmd(recdict, serverarg, action, addbinarg,
                                    logentry, node_name, dirlevels = dirlevels)

        # For a remove we must commit it now, otherwise the local file will be
        # deleted when doing it later.  To be consistent, also do it for "add".
        if ok and action in [ "remove", "add" ]:
            ok = exec_cvs_cmd(recdict, serverarg, "commit", "", logentry,
                                node_name, dirlevels = dirlevels, auto_add = 0)
    finally:
        if tmpname:
            try:
                os.rename(tmpname, node_name)
            except StandardError, e:
                msg_error(recdict, (_('Could not move file "%s" back to "%s"')
                                              % (tmpname, node_name)) + str(e))

        if cwd:
            try:
                os.chdir(cwd)
            except StandardError, e:
                msg_error(recdict, (_('Could not go back to directory "%s"')
                                                               % cwd) + str(e))

    # TODO: how to check if it really worked?
    return ok


def exec_cvs_cmd(recdict, serverarg, action, addbinarg, logentry, node_name,
                                                  dirlevels = 1, auto_add = 1):
    """Execute the CVS command for "action".  Handle failure.
       For "commit" may create directories up to "dirlevels" upwards.
       When "auto_add" is non-zero and committing fails, try to add the file
       first.
       Return non-zero when it worked."""

    # Prepare for using CVS and get the command name.
    cvscmd = cvs_prepare(recdict)

    if logentry:
        logarg = '-m "%s"' % logentry
    else:
        logarg = ''

    if action == "commit":
        # If the file was never added to the repository we need to add it.
        # Since most files will exist in the repository, trying to commit and
        # handling the error is the best method.

        # Repeat this when the directory needs to be added.
        did_add_dir = 0
        while 1:
            # TODO: escaping special characters
            cmd = ('"%s" %s commit %s "%s"'
                                      % (cvscmd, serverarg, logarg, node_name))
            ok, text = redir_system_int(recdict, cmd)

            if text:
                msg_log(recdict, text)

                # If the directory for the file doesn't exist, CVS says "there
                # is no version here".  Need to create the directory first.
                # This is only done for the number of levels that were included
                # in the node name for the original directory.
                # Errors are ignored, the commit will fail later.
                if ((string.find(text, "no version here") >= 0
                             or string.find(text, "not open CVS/Entries") >= 0)
                         and not did_add_dir
                         and auto_add):
                    msg_info(recdict, _("Directory does not appear to exist in repository, adding it"))
                    commit_dir(recdict, node_name, serverarg, dirlevels, cvscmd)
                    did_add_dir = 1
                    continue

                # If the file was never in the repository CVS says "nothing
                # known about".  If it was there before "use `cvs add' to
                # create an entry".
                if ok and (string.find(text, "nothing known about") >= 0
                                         or string.find(text, "cvs add") >= 0):
                    ok = 0
            break

        if ok or not auto_add:
            return ok

        try:
            msg_info(recdict,
                   _("File does not appear to exist in repository, adding it"))
            logged_system(recdict, '"%s" %s add %s %s'
                                   % (cvscmd, serverarg, addbinarg, node_name))
        except StandardError, e:
            msg_warning(recdict, _('Adding file failed: ') + str(e))


        # TODO: escaping special characters
        return logged_system(recdict, '"%s" %s commit %s "%s"'
                      % (cvscmd, serverarg, logarg, node_name)) == 0

    # TODO: escaping special characters
    if action != "add":
        addbinarg = ""
    return logged_system(recdict, '"%s" %s %s %s "%s"'
                      % (cvscmd, serverarg, action, addbinarg, node_name)) == 0


def commit_dir(recdict, node_name, serverarg, dirlevels, cvscmd):
    """Commit to create the current directory.  If its parent is not in CVS
       either go up further."""
    cwd = os.getcwd()
    try:
        os.chdir("..")
        dirname = os.path.dirname(node_name)
        if not dirname:
            dirname = os.path.basename(cwd)

        if dirlevels > 0 and not os.path.isdir("CVS"):
            commit_dir(recdict, dirname, serverarg, dirlevels - 1, cvscmd)

        logged_system(recdict, '"%s" %s add "%s"'
                                                % (cvscmd, serverarg, dirname))
    except:
        pass
    os.chdir(cwd)


def cvs_list(recdict, name, commit_item, dirname, recursive):
    """Obtain a list of items in CVS for directory "dirname".
       Recursively entry directories if "recursive" is non-zero.
       "name" is not used, we don't access the server."""
    # We could use "cvs status" to obtain the actual entries in the repository,
    # but that is slow and the output is verbose and hard to parse.
    # Instead read the "CVS/Entries" file.  A disadvantage is that we might
    # list a file that is actually already removed from the repository if
    # another user removed it.
    fname = os.path.join(dirname, "CVS/Entries")
    try:
        f = open(fname)
    except StandardError, e:
        msg_error(recdict, (_('Cannot open "%s": ') % fname) + str(e))
        return []
    try:
        lines = f.readlines()
        f.close()
    except StandardError, e:
        msg_error(recdict, (_('Cannot read "%s": ') % fname) + str(e))
        return []

    # The format of the lines is:
    #   D/dirname////
    #   /itemname/vers/foo//
    # We only need to extract "dirname" or "itemname".
    res = []
    for line in lines:
        s = string.find(line, "/")
        if s < 0:
            continue
        s = s + 1
        e = string.find(line, "/", s)
        if e < 0:
            continue
        item = os.path.join(dirname, line[s:e])

        if line[0] == 'D' and recursive:
            res.extend(cvs_list(recdict, name, commit_item, item, 1))
        else:
            res.append(item)

    return res



# vim: set sw=4 et sts=4 tw=79 fo+=l:
