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