# 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: