# 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(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 == "refresh":
	action = "checkout"    # "refresh" 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 1	    # unlocking is a no-op

    # TODO: Should group nodes in the same directory together and do them all
    # at once.
    res = 1
    for node in nodelist:
	if cvs_command_node(server, url_dict, node, action) == 0:
	    res = 0
    return res


def cvs_command_node(server, url_dict, node, action):
    """Handle CVS command for one node."""

    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(_('Cannot open for obtaining CVSROOT: "CVS/Root"')
								      + str(e))
	else:
	    try:
		server = f.readline()
		f.close()
	    except StandardError, e:
		msg_warning(_('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 = ''


    msg_info(_('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.
    # Use node.recipe_dir and take off one part for each part in "path".
    # Try to obtain the path from the CVS/Repository file.
    if os.path.isdir(node.absname):
	node_dir = node.absname
    else:
	node_dir = os.path.dirname(node.absname)

    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:
	    dir_for_path = node_dir
	    fname = os.path.join(dir_for_path, "CVS/Repository")
	    try:
		f = open(fname)
	    except StandardError, e:
		msg_warning((_('Cannot open for obtaining path in module: "%s"')
								 % fname) + str(e))
		return 0
	    try:
		cvspath = f.readline()
		f.close()
	    except StandardError, e:
		msg_warning((_('Cannot read for obtaining path in module: "%s"')
								 % fname) + str(e))
		return 0
	    if cvspath[-1] == '\n':
		cvspath = cvspath[:-1]

	dir = dir_for_path
	path = cvspath
	while path:
	    if os.path.basename(dir) != os.path.basename(path):
		msg_warning(_('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(_('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 = node_dir

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

    # Use the specified "logentry" attribute or generate a message.
    if url_dict.has_key("logentry"):
	logentry = url_dict["logentry"]
    else:
	logentry = "Done by A-A-P"

    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()
	tmpname = os.path.join("aap", node_name)
	try:
	    os.rename(node_name, tmpname)
	except:
	    tmpname = ''

    # TODO: quoting and escaping special characters
    try:
	res = exec_cvs_cmd(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 not res and action in [ "remove", "add" ]:
	    res = exec_cvs_cmd(serverarg, "commit", logentry, node_name)
    finally:
	if tmpname:
	    try:
		os.rename(tmpname, node_name)
	    except StandardError, e:
		msg_error((_('Could not move file "%s" back to "%s"')
					      % (tmpname, node_name)) + str(e))

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

    if res != 0:
	return 0

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


def exec_cvs_cmd(serverarg, action, logentry, node_name):
    """Execute the CVS command for "action".  Handle failure."""

    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.
	tmpfile = tempfname()
	try:
	    cmd = ("cvs %s commit -m '%s' %s 2>&1 | tee %s"
				   % (serverarg, logentry, node_name, tmpfile))
	    msg_system(cmd)
	    res = os.system(cmd)
	except:
	    res = 1

	# Read the output of the command, also when it failed.
	text = ''
	try:
	    f = open(tmpfile)
	    text = f.read()
	    f.close()
	except StandardError, e:
	    msg_warning(_('Reading output of "cvs commit" failed: ') + str(e))
	    res = 1
	if text:
	    msg_log(text, msgt_result)
	    # 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 not res and (string.find(text, "nothing known about") >= 0
					 or string.find(text, "cvs add") >= 0):
		res = 1

	# always remove the tempfile, even when system() failed.
	try:
	    os.remove(tmpfile)
	except:
	    pass

	if not res:
	    return 0

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


    if action == "commit":
	return logged_system("cvs %s commit -m '%s' %s"
					    % (serverarg, logentry, node_name))
    return logged_system("cvs %s %s %s" % (serverarg, action, node_name))


def cvs_list(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:
	raise UserError, (_('Cannot open "%s": ') % fname) + str(e)
    try:
	lines = f.readlines()
	f.close()
    except StandardError, e:
	raise UserError, (_('Cannot read "%s": ') % fname) + str(e)

    # 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(name, commit_item, item, 1))
	else:
	    res.append(item)

    return res



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