# Part of the A-A-P recipe executive: Generic Version Control

# 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 version control system and put them back.
# Most of the work is directed to one of the modules for a specific version
# control system.
# Uploading/downloading is done by functions in Remote.py.
#

import os
import os.path
import string

from Process import recipe_error
from Cvs import *
from Work import getwork
from Node import Node
from Remote import download_file, upload_file, remote_remove


def separate_scheme(url):
    """Isolate the scheme of the URL."""
    i = string.find(url, "://")
    if i < 0:
	# Catch "file:~user".
	if len(url) > 5 and url[:5] == "file:":
	    return "file", url[5:]
	raise UserError, _('No :// found in "%s"') % url
    scheme = string.lower(url[:i])
    for c in scheme:
	if not c in string.lowercase:
	    raise UserError, _('Illegal character before colon in "%s"') % url
    return scheme, url[i + 3:]


def repl_file_name(attr, name):
    """Replace all "%file%" in "attr" with "name"."""
    while 1:
	i = string.find(attr, "%file%")
	if i >= 0:
	    attr = attr[:i] + name + attr[i + 6:]
	    continue
	i = string.find(attr, "%basename%")
	if i >= 0:
	    attr = attr[:i] + os.path.basename(name) + attr[i + 10:]
	    continue
	break
    return attr


def verscont_command(recdict, dict, nodelist, use_cache, action):
    """Try performing "action" on node "node" from the semi-URL "dict["name"]".
       May Use a cached file when "use_cache" is non-zero.
       Returns list of nodes that failed."""
    scheme, name = separate_scheme(dict["name"])

    # Handle a scheme for which there is a scheme_command() function.
    if recdict.has_key(scheme + "_command"):
	return recdict[scheme + "_command"](name, dict, nodelist, action)

    # Handle the "cvs://" scheme.
    if scheme == "cvs":
	return cvs_command(recdict, name, dict, nodelist, action)

    # Handle uploading.
    if action in ["commit", "checkin", "publish", "add"]:
	return upload_file(recdict, dict, nodelist)

    failed = []
    for node in nodelist:
	# Assume it's a normal URL, try downloading/uploading/removing the
	# file.
	name = dict["name"]
	dict["name"] = repl_file_name(name, node.name)

	if action in ["fetch", "checkout"]:
	    ok = download_file(recdict, dict, node, use_cache)
	elif action == "remove":
	    ok = remote_remove(recdict, dict, node)
	else:
	    ok = 0
	if not ok:
	    failed.append(node)

	# restore the name, %file% might have to be replaced again
	dict["name"] = name

    return failed


def handle_nodelist(rpstack, recdict, nodelist, use_cache, action, attrnames):
    """Common code for fetching, committing, etc.
       "action" is the name  of the command: "fetch", "commit", etc.
       Perform this action on each node in "nodelist", unless "--nobuild" or
       "--touch" is used.
       "attrnames" is a list of attribute names that can be used for this
       action.
       Return list of failed nodes.
       """
    faillist = []

    # Make a copy of the list of nodes.  Items are removed when done.
    todolist = nodelist[:]

    while todolist:
	# Find a bunch of nodes with identical attributes and handle them all
	# at once.  Limit to 20 to avoid very long command lines.
	attr = ''
	thislist = []
	for node in todolist[:]:
	    # Use the first attribute in "attrnames" that exists and isn't
	    # empty.
	    nx_attr = ''
	    for n in attrnames:
		if node.attributes.has_key(n):
		    nx_attr = node.attributes[n]
		    if nx_attr:
			attrname = n
			break
	    if not nx_attr:
		recipe_error(rpstack, _("Missing %s attribute for %s")
					   % (attrnames[0], node.short_name()))
		todolist.remove(node)
		faillist.append(node)
	    elif not attr or nx_attr == attr:
		thislist.append(node)
		todolist.remove(node)
		attr = nx_attr
		if len(thislist) > 20:
		    break

	# Do the list of nodes with identical attributes "attr".  There is at
	# least one.
	from Dictlist import str2dictlist
	list = str2dictlist(rpstack, attr)
	if not list:
	    recipe_error(rpstack, _("%s attribute for %s is empty")
					% (attrname, thislist[0].short_name()))

	# Perform the action, unless started with --nobuild or --touch.
	if skip_commands():
	    if not Global.cmd_args.options.get("touch"):
		msg_info(recdict, _('skip %s for %s') % (action,
				 str(map(lambda x: x.short_name(), thislist))))
	else:
	    # Loop over the list of locations.  Quit as soon as one worked,
	    # except for "publish", all locations are used then.
	    for loc in list:
		# Try handling thislist with this location.  For publish do
		# this for all locations, otherwise stop when all items have
		# been done.
		res = verscont_command(recdict, loc, thislist,
							     use_cache, action)
		if action == "publish":
		    for r in res:
			if not r in faillist:
			    faillist.append(r)
		else:
		    thislist = res
		    if not thislist:
			break
	    if action != "publish" and thislist:
		# this bunch failed.  Still continue doing the remaining ones.
		faillist.extend(thislist)

    return faillist


def fetch_nodelist(rpstack, recdict, nodelist, use_cache):
    """Fetch nodes in "nodelist" according to its "fetch" attribute.
       When there is no "fetch" attribute use "commit".
       Only use cached files when "use_cache" is non-zero.
       Return list of failed nodes."""
    return handle_nodelist(rpstack, recdict, nodelist, use_cache,
					    "fetch", [ "fetch", "commit" ])


def verscont_nodelist(rpstack, recdict, nodelist, action):
    """Checkout nodes in "nodelist" according to their "commit" attribute.
       Return list of failed nodes."""
    return handle_nodelist(rpstack, recdict, nodelist, 0, action, [ "commit" ])


def publish_nodelist(rpstack, recdict, nodelist, msg):
    """Publish nodes in "nodelist" according to their "publish" attribute.
       When there is no "publish" attribute use "commit".
       When "msg" is non-zero give a message for up-to-date nodes.
       Return None for nothing done, list of failed nodes otherwise.
       """
    # Publishing is only done when the signature is outdated.
    # Need to pull a few tricks to be able to call check_need_update().
    from Sign import sign_updated, buildcheck_updated
    from DoBuild import Update, check_need_update

    todolist = []
    buildchecklist = []
    targetlist = []
    failed = []

    for node in nodelist:
	# Skip nodes that don't exist.
	node_name = node.get_name()
	if not os.path.exists(node_name):
	    failed.append(node)
	    msg_error(recdict,
			  _('Published file does not exist: "%s"') % node_name)
	    continue

	if node.attributes.has_key("publish"):
	    s = node.attributes["publish"]
	elif node.attributes.has_key("commit"):
	    s = node.attributes["commit"]
	else:
	    s = ''

	# Handle each publish/commit item separately, so that when adding a new
	# destination we don't upload to the old destinations as well.
	from Dictlist import str2dictlist, dictlist2str

	list = str2dictlist(rpstack, s)
	if not list:
	    recipe_error(rpstack, _('publish attribute for "%s" is empty')
							   % node.short_name())
	for attr in list:
	    # Use a fake virtual target to attach the signature to.
	    # Use the "remember" attribute, only publish when changed.
	    # Use the sign file in the directory related to the published file.
	    attr_name = attr["name"]
	    attr_str = dictlist2str([ attr ])
	    target = Node(":publish:" + node_name + ">" + attr_name)
	    target.attributes["virtual"] = 1
	    target.attributes["remember"] = 1
	    target.attributes["signfile"] = node.get_sign_fname()

	    source = node.copy()
	    source.attributes["_node"] = source
	    source.attributes["name"] = node_name
	    source.attributes["publish"] = attr_str

	    # Need to publish the file when it's signature has changed or the
	    # relevant attributes have changed (use buildcheck for this).
	    update = Update()
	    check_need_update(recdict, update, source.attributes, target,
				      rootname = ":publish:" + node_name + ">")
	    update.set_buildcheck(recdict, attr_str, target, 0)

	    # Find out if there is a reason to publish this source node.
	    reason = update.upd_reason(0, target)
	    if reason or msg:
		disp_name = node.short_name() + "[" + attr_str + "]"
	    if reason:
		if Global.cmd_args.options.get("touch"):
		    msg_info(recdict, _('Touching "%s": %s')
							 % (disp_name, reason))
		else:
		    msg_depend(recdict, _('Publishing "%s": %s')
							 % (disp_name, reason))
		todolist.append(source)
		buildchecklist.append(update.buildcheck)
		targetlist.append(target)
	    else:
		if msg:
		    msg_depend(recdict, _('item is up-to-date: "%s"')
								   % disp_name)
		# For the "--contents" option we skip items for which the
		# "publish" attribute changed but not the contents.  Need to
		# update the signature anyway, otherwise it will still be
		# published next time.
		if (Global.cmd_args.options.get("contents")
			       and not Global.cmd_args.options.get("nobuild")):
		    sign_updated(recdict, source, source.attributes, target)
		    if update.buildcheck:
			buildcheck_updated(recdict, target, update.buildcheck)

    if not todolist:
	if failed:
	    return failed	# all outdated files do not exist
	return None		# all files exist and targets are up-to-date

    failed.extend(handle_nodelist(rpstack, recdict, todolist, 0,
					   "publish", [ "publish", "commit" ]))

    # Update signatures, unless --nobuild was used without --touch.
    if (not Global.cmd_args.options.get("nobuild")
				      or Global.cmd_args.options.get("touch")):
	i = 0
	while i < len(todolist):
	    # Remember the source signature and the buildcheck signature.
	    # But only for nodes that didn't fail.
	    node = todolist[i]
	    if not node in failed:
		target = targetlist[i]
		buildcheck = buildchecklist[i]
		sign_updated(recdict, node, node.attributes, target)
		if buildcheck:
		    buildcheck_updated(recdict, target, buildcheck)
	    i = i + 1

    return failed


def verscont_remove_add(rpstack, recdict, dir, recursive, action):
    """When "action" is "remove: Remove all files in directory "dir" of VCS
       that don't belong there.
       When "action" is "add:" Add all files in the recipe in directory "dir"
       to the VCS that are missing.
       "dir" is a dictionary for the directory and its attributes.
       Enter directories recursively when "recursive" is non-zero.
       """
    if not dir.has_key("commit"):
	recipe_error(rpstack, _("no commit attribute for %s") % dir["name"])

    from Dictlist import str2dictlist
    commit_list = str2dictlist(rpstack, dir["commit"])
    if not commit_list:
	recipe_error(rpstack, _("commit attribute for %s is empty")
								 % dir["name"])

    dirname = os.path.abspath(dir["name"])
    list = None
    for commit_item in commit_list:
	scheme, name = separate_scheme(commit_item["name"])

	# Handle a scheme for which there is a scheme_list() function.
	if recdict.has_key(scheme + "_list"):
	    list = recdict[scheme + "_list"](name,
					       commit_item, dirname, recursive)
	    break

	# Handle the "cvs://" scheme.
	if scheme == "cvs":
	    list = cvs_list(recdict, name, commit_item, dirname, recursive)
	    break

    if list is None:
	recipe_error(rpstack, _("No working item in commit attribute for %s")
								 % dir["name"])

    work = getwork(recdict)
    ok = 1

    if action == "remove":
	# Remove: Loop over all items found in the VCS.
	for item in list:
	    node = work.find_node(item)
	    if not node or not node.attributes.has_key("commit"):
		if not node:
		    node = Node(item)
		if skip_commands():
		    if not Global.cmd_args.options.get("touch"):
			msg_info(recdict, _('Remove "%s"') % node.short_name())
		elif verscont_command(recdict, commit_item, [ node ],
								  0, "remove"):
		    ok = 0
    else:
	# Add: Loop over all nodes and check if they are in the VCS.
	# Only do this for a node that:
	#  - has the "commit" attribute
	#  - has a full name that is longer than the directory name
	#  - working recursive and the node is below the directory
	#  - not working recursive and the node is in the directory
	#  - the node does not appear in the list from the VCS
	dirname_len = len(dirname)
	for node in work.nodes.values():
	    if (node.attributes.has_key("commit")
		    and len(node.absname) > dirname_len
		    and ((recursive
			    and node.absname[:dirname_len] == dirname
			    and node.absname[dirname_len] == '/')
			or (not recursive
			    and os.path.dirname(node.absname) == dirname))
		    and not node.absname in list):
		if skip_commands():
		    if not Global.cmd_args.options.get("touch"):
			msg_info(recdict, _('Add "%s"') % node.short_name())
		elif verscont_command(recdict, commit_item, [ node ], 0, "add"):
		    ok = 0

    return ok


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