# 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".
adir = fname_fold(dir_for_path)
path = fname_fold(cvspath)
while path:
if os.path.basename(adir) != 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(adir)
if ndir == adir:
# 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
adir = 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, adir)
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"') % (adir, path))
break
else:
adir = 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, adir):
cwd = '' # we're already there, avoid a chdir()
else:
try:
os.chdir(adir)
except StandardError, e:
msg_warning(recdict,
(_('Could not change to directory "%s"') % adir) + str(e))
return 0
msg_log(recdict, 'Cvs command in "%s"' % adir)
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
# Return if it worked, we are not doing automatic adds or the error
# indicates that the file exists but our copy is not up-to-date.
if ok or not auto_add or string.find(text, "Up-to-date check failed") >= 0:
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:
syntax highlighted by Code2HTML, v. 0.9.1