Package translate :: Package storage :: Package versioncontrol

[hide private]
[frames] | no frames]

# Source Code for Package translate.storage.versioncontrol

      1  #!/usr/bin/env python
      2  # -*- coding: utf-8 -*-
      3  #
      4  # Copyright 2004-2008 Zuza Software Foundation
      5  #
      6  # This file is part of translate.
      7  #
      8  # translate is free software; you can redistribute it and/or modify
      9  # it under the terms of the GNU General Public License as published by
     10  # the Free Software Foundation; either version 2 of the License, or
     11  # (at your option) any later version.
     12  #
     13  # translate is distributed in the hope that it will be useful,
     14  # but WITHOUT ANY WARRANTY; without even the implied warranty of
     15  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     16  # GNU General Public License for more details.
     17  #
     18  # You should have received a copy of the GNU General Public License
     19  # along with translate; if not, write to the Free Software
     20  # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
     21
     22  """This module manages interaction with version control systems.
     23
     24  To implement support for a new version control system, inherit the class
     25  GenericRevisionControlSystem.
     26
     27  TODO:
     28      * add authentication handling
     29      * 'commitdirectory' should do a single commit instead of one for each file
     30      * maybe implement some caching for 'get_versioned_object' - check profiler
     31  """
     32
     33  import re
     34  import os
     35
     36  DEFAULT_RCS = ["svn", "cvs", "darcs", "git", "bzr", "hg"]
     37  """the names of all supported revision control systems
     38
     39  modules of the same name containing a class with the same name are expected
     40  to be defined below 'translate.storage.versioncontrol'
     41  """
     42
     43  __CACHED_RCS_CLASSES = {}
     44  """The dynamically loaded revision control system implementations (python
     45  modules) are cached here for faster access.
     46  """
     47
     48 -def __get_rcs_class(name):
     49      if not name in __CACHED_RCS_CLASSES:
     50          try:
     51              module = __import__("translate.storage.versioncontrol.%s" % name,
     52                      globals(), {}, name)
     53              # the module function "is_available" must return "True"
     54              if (hasattr(module, "is_available") and \
     55                      callable(module.is_available) and \
     56                      module.is_available()):
     57                  # we found an appropriate module
     58                  rcs_class = getattr(module, name)
     59              else:
     60                  # the RCS client does not seem to be installed
     61                  rcs_class = None
     62          except (ImportError, AttributeError):
     63              rcs_class = None
     64          __CACHED_RCS_CLASSES[name] = rcs_class
     65      return __CACHED_RCS_CLASSES[name]
     66
     67
     68  # use either 'popen2' or 'subprocess' for command execution
     69  try:
     70      # available for python >= 2.4
     71      import subprocess
     72
     73      # The subprocess module allows to use cross-platform command execution
     74      # without using the shell (increases security).
     75
     76 -    def run_command(command, cwd=None):
     77          """Runs a command (array of program name and arguments) and returns the
     78          exitcode, the output and the error as a tuple.
     79
     80          @param command: list of arguments to be joined for a program call
     81          @type command: list
     82          @param cwd: optional directory where the command should be executed
     83          @type cwd: str
     84          """
     85          # ok - we use "subprocess"
     86          try:
     87              proc = subprocess.Popen(args = command,
     88                      stdout = subprocess.PIPE,
     89                      stderr = subprocess.PIPE,
     90                      stdin = subprocess.PIPE,
     91                      cwd = cwd)
     92              (output, error) = proc.communicate()
     93              ret = proc.returncode
     94              return ret, output, error
     95          except OSError, err_msg:
     96              # failed to run the program (e.g. the executable was not found)
     97              return -1, "", err_msg
     98
     99  except ImportError:
    100      # fallback for python < 2.4
    101      import popen2
    102
    103 -    def run_command(command, cwd=None):
    104          """Runs a command (array of program name and arguments) and returns the
    105          exitcode, the output and the error as a tuple.
    106
    107          There is no need to check for exceptions (like for subprocess above),
    108          since popen2 opens a shell that will fail with an error code in case
    109          of a missing executable.
    110
    111          @param command: list of arguments to be joined for a program call
    112          @type command: list
    113          @param cwd: optional directory where the command should be executed
    114          @type cwd: str
    115          """
    116          escaped_command = " ".join([__shellescape(arg) for arg in command])
    117          if cwd:
    118              # "Popen3" uses shell execution anyway - so we do it the easy way
    119              # there is no need to chdir back, since the the shell is separated
    120              escaped_command = "cd %s; %s" % (__shellescape(cwd), escaped_command)
    121          proc = popen2.Popen3(escaped_command, True)
    122          (c_stdin, c_stdout, c_stderr) = (proc.tochild, proc.fromchild, proc.childerr)
    123          output = c_stdout.read()
    124          error = c_stderr.read()
    125          ret = proc.wait()
    126          c_stdout.close()
    127          c_stderr.close()
    128          c_stdin.close()
    129          return ret, output, error
    130
    131 -def __shellescape(path):
    132      """Shell-escape any non-alphanumeric characters."""
    133      return re.sub(r'(\W)', r'\\\1', path)
    134
    135
    136 -class GenericRevisionControlSystem:
    137      """The super class for all version control classes.
    138
    139      Always inherit from this class to implement another RC interface.
    140
    141      At least the two attributes "RCS_METADIR" and "SCAN_PARENTS" must be
    142      overriden by all implementations that derive from this class.
    143
    144      By default, all implementations can rely on the following attributes:
    145          root_dir: the parent of the metadata directory of the working copy
    146          location_abs: the absolute path of the RCS object
    147          location_rel: the path of the RCS object relative to 'root_dir'
    148      """
    149
    150      RCS_METADIR = None
    151      """The name of the metadata directory of the RCS
    152
    153      e.g.: for Subversion -> ".svn"
    154      """
    155
    156      SCAN_PARENTS = None
    157      """whether to check the parent directories for the metadata directory of
    158      the RCS working copy
    159
    160      some revision control systems store their metadata directory only
    161      in the base of the working copy (e.g. bzr, GIT and Darcs)
    162      use "True" for these RCS
    163
    164      other RCS store a metadata directory in every single directory of
    165      the working copy (e.g. Subversion and CVS)
    166      use "False" for these RCS
    167      """
    168
    169 -    def __init__(self, location):
    170          """find the relevant information about this RCS object
    171
    172          The IOError exception indicates that the specified object (file or
    173          directory) is not controlled by the given version control system.
    174          """
    175          # check if the implementation looks ok - otherwise raise IOError
    176          self._self_check()
    177          # search for the repository information
    178          result = self._find_rcs_directory(location)
    179          if result is None:
    180              raise IOError("Could not find revision control information: %s" \
    181                      % location)
    182          else:
    183              self.root_dir, self.location_abs, self.location_rel = result
    184
    185 -    def _find_rcs_directory(self, rcs_obj):
    186          """Try to find the metadata directory of the RCS
    187
    188          returns a tuple:
    189              the absolute path of the directory, that contains the metadata directory
    190              the absolute path of the RCS object
    191              the relative path of the RCS object based on the directory above
    192          """
    193          rcs_obj_dir = os.path.dirname(os.path.abspath(rcs_obj))
    194          if os.path.isdir(os.path.join(rcs_obj_dir, self.RCS_METADIR)):
    195              # is there a metadir next to the rcs_obj?
    196              # (for Subversion, CVS, ...)
    197              location_abs = os.path.abspath(rcs_obj)
    198              location_rel = os.path.basename(location_abs)
    199              return (rcs_obj_dir, location_abs, location_rel)
    200          elif self.SCAN_PARENTS:
    201              # scan for the metadir in parent directories
    202              # (for bzr, GIT, Darcs, ...)
    203              return self._find_rcs_in_parent_directories(rcs_obj)
    204          else:
    205              # no RCS metadata found
    206              return None
    207
    208 -    def _find_rcs_in_parent_directories(self, rcs_obj):
    209          """Try to find the metadata directory in all parent directories"""
    210          # first: resolve possible symlinks
    211          current_dir = os.path.dirname(os.path.realpath(rcs_obj))
    212          # prevent infite loops
    213          max_depth = 64
    214          # stop as soon as we find the metadata directory
    215          while not os.path.isdir(os.path.join(current_dir, self.RCS_METADIR)):
    216              if os.path.dirname(current_dir) == current_dir:
    217                  # we reached the root directory - stop
    218                  return None
    219              if max_depth <= 0:
    220                  # some kind of dead loop or a _very_ deep directory structure
    221                  return None
    222              # go to the next higher level
    223              current_dir = os.path.dirname(current_dir)
    224          # the loop was finished successfully
    225          # i.e.: we found the metadata directory
    226          rcs_dir = current_dir
    227          location_abs = os.path.realpath(rcs_obj)
    228          # strip the base directory from the path of the rcs_obj
    229          basedir = rcs_dir + os.path.sep
    230          if location_abs.startswith(basedir):
    231              # remove the base directory (including the trailing slash)
    232              location_rel = location_abs.replace(basedir, "", 1)
    233              # successfully finished
    234              return (rcs_dir, location_abs, location_rel)
    235          else:
    236              # this should never happen
    237              return None
    238
    239 -    def _self_check(self):
    240          """Check if all necessary attributes are defined
    241
    242          Useful to make sure, that a new implementation does not forget
    243          something like "RCS_METADIR"
    244          """
    245          if self.RCS_METADIR is None:
    246              raise IOError("Incomplete RCS interface implementation: " \
    247                      + "self.RCS_METADIR is None")
    248          if self.SCAN_PARENTS is None:
    249              raise IOError("Incomplete RCS interface implementation: " \
    250                      + "self.SCAN_PARENTS is None")
    251          # we do not check for implemented functions - they raise
    252          # NotImplementedError exceptions anyway
    253          return True
    254
    255 -    def getcleanfile(self, revision=None):
    256          """Dummy to be overridden by real implementations"""
    257          raise NotImplementedError("Incomplete RCS interface implementation:" \
    258                  + " 'getcleanfile' is missing")
    259
    260
    261 -    def commit(self, revision=None, author=None):
    262          """Dummy to be overridden by real implementations"""
    263          raise NotImplementedError("Incomplete RCS interface implementation:" \
    264                  + " 'commit' is missing")
    265
    266
    267 -    def update(self, revision=None):
    268          """Dummy to be overridden by real implementations"""
    269          raise NotImplementedError("Incomplete RCS interface implementation:" \
    270                  + " 'update' is missing")
    271
    272
    273 -def get_versioned_objects_recursive(
    274          location,
    275          versioning_systems=None,
    276          follow_symlinks=True):
    277      """return a list of objects, each pointing to a file below this directory
    278      """
    279      rcs_objs = []
    280      if versioning_systems is None:
    281          versioning_systems = DEFAULT_RCS
    282
    283      def scan_directory(arg, dirname, fnames):
    284          for fname in fnames:
    285              full_fname = os.path.join(dirname, fname)
    286              if os.path.isfile(full_fname):
    287                  try:
    288                      rcs_objs.append(get_versioned_object(full_fname,
    289                              versioning_systems, follow_symlinks))
    290                  except IOError:
    291                      pass
    292
    293      os.path.walk(location, scan_directory, None)
    294      return rcs_objs
    295
    296 -def get_versioned_object(
    297          location,
    298          versioning_systems=None,
    299          follow_symlinks=True):
    300      """return a versioned object for the given file"""
    301      if versioning_systems is None:
    302          versioning_systems = DEFAULT_RCS
    303      # go through all RCS and return a versioned object if possible
    304      for vers_sys in versioning_systems:
    305          try:
    306              vers_sys_class = __get_rcs_class(vers_sys)
    307              if not vers_sys_class is None:
    308                  return vers_sys_class(location)
    309          except IOError:
    310              continue
    311      # if 'location' is a symlink, then we should try the original file
    312      if follow_symlinks and os.path.islink(location):
    313          return get_versioned_object(os.path.realpath(location),
    314                  versioning_systems = versioning_systems,
    315                  follow_symlinks = False)
    316      # if everything fails:
    317      raise IOError("Could not find version control information: %s" % location)
    318
    319 -def get_available_version_control_systems():
    320      """ return the class objects of all locally available version control
    321      systems
    322      """
    323      result = []
    324      for rcs in DEFAULT_RCS:
    325          rcs_class = __get_rcs_class(rcs)
    326          if rcs_class:
    327              result.append(rcs_class)
    328      return result
    329
    330  # stay compatible to the previous version
    331 -def updatefile(filename):
    332      return get_versioned_object(filename).update()
    333
    334 -def getcleanfile(filename, revision=None):
    335      return get_versioned_object(filename).getcleanfile(revision)
    336
    337 -def commitfile(filename, message=None, author=None):
    338      return get_versioned_object(filename).commit(message=message, author=author)
    339
    340 -def commitdirectory(directory, message=None, author=None):
    341      """commit all files below the given directory
    342
    343      files that are just symlinked into the directory are supported, too
    344      """
    345      # for now all files are committed separately
    346      # should we combine them into one commit?
    347      for rcs_obj in get_versioned_objects_recursive(directory):
    348          rcs_obj.commit(message=message, author=author)
    349
    350 -def updatedirectory(directory):
    351      """update all files below the given directory
    352
    353      files that are just symlinked into the directory are supported, too
    354      """
    355      # for now all files are updated separately
    356      # should we combine them into one update?
    357      for rcs_obj in get_versioned_objects_recursive(directory):
    358          rcs_obj.update()
    359
    360 -def hasversioning(item):
    361      try:
    362          # try all available version control systems
    363          get_versioned_object(item)
    364          return True
    365      except IOError:
    366          return False
    367
    368
    369
    370  if __name__ == "__main__":
    371      import sys
    372      filenames = sys.argv[1:]
    373      if filenames:
    374          # try to retrieve the given (local) file from a repository
    375          for filename in filenames:
    376              contents = getcleanfile(filename)
    377              sys.stdout.write("\n\n******** %s ********\n\n" % filename)
    378              sys.stdout.write(contents)
    379      else:
    380          # first: make sure, that the translate toolkit is available
    381          # (useful if "python __init__.py" was called without an appropriate
    382          # PYTHONPATH)
    383          import translate.storage.versioncontrol
    384          # print the names of locally available version control systems
    385          for rcs in get_available_version_control_systems():
    386              print rcs
    387