# Part of the A-A-P recipe executive: Generic Version Control
# 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 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 VersContCvs 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 nodes "nodelist" 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.
fun = recdict["_no"].get(scheme + "_command")
if fun:
return apply(fun, (recdict, 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[:]
try:
groupcount = int(get_var_val(0, recdict, "_no", "GROUPCOUNT"))
except StandardError, e:
recipe_error(rpstack, _('Invalid $GROUPCOUNT value: %s: %s')
% (str(get_var_val(0, recdict, "_no", "GROUPCOUNT")), str(e)))
while todolist:
# Find a bunch of nodes with identical attributes and handle them all
# at once.
attr = ''
thislist = []
this_use_cache = None
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
# Using cache for this node?
uc = use_cache
if (node.attributes.get("usecache")
or node.attributes.get("constant")):
uc = 1
if node.attributes.get("nocache"):
uc = 0
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)
and (this_use_cache is None or this_use_cache == uc)):
thislist.append(node)
todolist.remove(node)
attr = nx_attr
this_use_cache = uc
# Limit to 20 to avoid very long command lines.
if len(thislist) >= groupcount:
break
# Do the list of nodes with identical attributes "attr". There is at
# least one.
from Dictlist import str2dictlist
alist = str2dictlist(rpstack, attr)
if not alist:
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 alist:
# 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,
this_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
alist = str2dictlist(rpstack, s)
if not alist:
recipe_error(rpstack, _('publish attribute for "%s" is empty')
% node.short_name())
for attr in alist:
# 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 ])
# Change "scp://" and "rscync://" to "scheme://", so that changing
# the method for uploading doesn't cause the signature to be
# outdated.
attr_name = re.sub("^[a-z]*://", "scheme://", attr_name)
buildcheck_str = re.sub("\\b[a-z]*://", "scheme://", attr_str)
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, buildcheck_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 withouth --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"])
alist = 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.
fun = recdict["_no"].get(scheme + "_list")
if fun:
alist = apply(fun, (recdict, name, commit_item, dirname, recursive))
break
# Handle the "cvs://" scheme.
if scheme == "cvs":
alist = cvs_list(recdict, name, commit_item, dirname, recursive)
break
if alist 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 alist:
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 alist):
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 et sts=4 tw=79 fo+=l:
|