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

# Copyright (C) 2002 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_warning(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_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)))

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

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


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()))

    # 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(node.absname):
		dir_for_path = node.absname
	    else:
		dir_for_path = os.path.dirname(node.absname)

	    cvspath = ''
	    fname = os.path.join(dir_for_path, "CVS/Repository")
	    try:
		f = open(fname)
	    except StandardError, e:
		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]

	# Use node.recipe_dir and take off one part for each part in "path".
	dir = dir_for_path
	path = cvspath
	while path:
	    if os.path.basename(dir) != os.path.basename(path):
		msg_warning(recdict,
			_('mismatch between path in cvs:// and tail of recipe directory: "%s" and "%s"') % (cvspath, dir_for_path))
	    ndir = os.path.dirname(dir)
	    if ndir == dir:
		msg_error(recdict,
			_('path in cvs:// is longer than recipe directory: "%s" and "%s"') % (cvspath, dir_for_path))
	    dir = ndir
	    npath = os.path.dirname(path)
	    if npath == path:   # just in case: avoid getting stuck
		break
	    path = npath
    else:
	dir = os.path.dirname(node.absname)

    # Use the specified "logentry" attribute or generate a message.
    # Only used for "commit".
    if url_dict.has_key("logentry"):
	logentry = url_dict["logentry"]
    elif node.attributes.has_key("logentry"):
	logentry = node.attributes["logentry"]
    else:
	logentry = "Done by A-A-P"

    # Changing directory, don't return until going back!
    cwd = os.getcwd()
    if 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

    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:
	ok = exec_cvs_cmd(recdict, serverarg, action, logentry, node_name)

	# 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)
    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, logentry, node_name):
    """Execute the CVS command for "action".  Handle failure.
       Return non-zero when it worked."""

    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 = ("cvs %s commit -m '%s' '%s'"
					    % (serverarg, logentry, node_name))
	    ok, text = redir_system_int(recdict, cmd)

	    if text:
		msg_log(text, msgt_result)

		# 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 one level.
		# Errors are ignored, the commit will fail later.
		if (ok and (string.find(text, "no version here") >= 0
			     or string.find(text, "not open CVS/Entries") >= 0)
			 and not did_add_dir):
		    msg_info(recdict, _("Directory does not appear to exist in repository, adding it"))
		    cwd = os.getcwd()
		    try:
			os.chdir("..")
			dirname = os.path.dirname(node_name)
			if not dirname:
			    dirname = os.path.basename(cwd)
			logged_system(recdict,
				      "cvs %s add '%s'" % (serverarg, dirname))
		    except:
			pass
		    os.chdir(cwd)
		    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:
	    return 1

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


	# TODO: escaping special characters
	if action == "commit":
	    return logged_system(recdict, "cvs %s commit -m '%s' '%s'"
				       % (serverarg, logentry, node_name)) == 0

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


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 sts=4 tw=79 fo+=l:
