translate.storage.versioncontrol

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