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          @rtype: tuple
    189          @return:
    190            - the absolute path of the directory, that contains the metadata directory
    191            - the absolute path of the RCS object
    192            - the relative path of the RCS object based on the directory above
    193          """
    194          if os.path.isdir(os.path.abspath(rcs_obj)):
    195              rcs_obj_dir = os.path.abspath(rcs_obj)
    196          else:
    197              rcs_obj_dir = os.path.dirname(os.path.abspath(rcs_obj))
    198
    199          if os.path.isdir(os.path.join(rcs_obj_dir, self.RCS_METADIR)):
    200              # is there a metadir next to the rcs_obj?
    201              # (for Subversion, CVS, ...)
    202              location_abs = os.path.abspath(rcs_obj)
    203              location_rel = os.path.basename(location_abs)
    204              return (rcs_obj_dir, location_abs, location_rel)
    205          elif self.SCAN_PARENTS:
    206              # scan for the metadir in parent directories
    207              # (for bzr, GIT, Darcs, ...)
    208              return self._find_rcs_in_parent_directories(rcs_obj)
    209          else:
    210              # no RCS metadata found
    211              return None
    212
    213 -    def _find_rcs_in_parent_directories(self, rcs_obj):
    214          """Try to find the metadata directory in all parent directories"""
    215          # first: resolve possible symlinks
    216          current_dir = os.path.dirname(os.path.realpath(rcs_obj))
    217          # prevent infite loops
    218          max_depth = 64
    219          # stop as soon as we find the metadata directory
    220          while not os.path.isdir(os.path.join(current_dir, self.RCS_METADIR)):
    221              if os.path.dirname(current_dir) == current_dir:
    222                  # we reached the root directory - stop
    223                  return None
    224              if max_depth <= 0:
    225                  # some kind of dead loop or a _very_ deep directory structure
    226                  return None
    227              # go to the next higher level
    228              current_dir = os.path.dirname(current_dir)
    229          # the loop was finished successfully
    230          # i.e.: we found the metadata directory
    231          rcs_dir = current_dir
    232          location_abs = os.path.realpath(rcs_obj)
    233          # strip the base directory from the path of the rcs_obj
    234          basedir = rcs_dir + os.path.sep
    235          if location_abs.startswith(basedir):
    236              # remove the base directory (including the trailing slash)
    237              location_rel = location_abs.replace(basedir, "", 1)
    238              # successfully finished
    239              return (rcs_dir, location_abs, location_rel)
    240          else:
    241              # this should never happen
    242              return None
    243
    244 -    def _self_check(self):
    245          """Check if all necessary attributes are defined
    246
    247          Useful to make sure, that a new implementation does not forget
    248          something like "RCS_METADIR"
    249          """
    250          if self.RCS_METADIR is None:
    251              raise IOError("Incomplete RCS interface implementation: " \
    252                      + "self.RCS_METADIR is None")
    253          if self.SCAN_PARENTS is None:
    254              raise IOError("Incomplete RCS interface implementation: " \
    255                      + "self.SCAN_PARENTS is None")
    256          # we do not check for implemented functions - they raise
    257          # NotImplementedError exceptions anyway
    258          return True
    259
    260 -    def getcleanfile(self, revision=None):
    261          """Dummy to be overridden by real implementations"""
    262          raise NotImplementedError("Incomplete RCS interface implementation:" \
    263                  + " 'getcleanfile' is missing")
    264
    265
    266 -    def commit(self, revision=None, author=None):
    267          """Dummy to be overridden by real implementations"""
    268          raise NotImplementedError("Incomplete RCS interface implementation:" \
    269                  + " 'commit' is missing")
    270
    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 -def get_versioned_object(
    302          location,
    303          versioning_systems=None,
    304          follow_symlinks=True):
    305      """return a versioned object for the given file"""
    306      if versioning_systems is None:
    307          versioning_systems = DEFAULT_RCS
    308      # go through all RCS and return a versioned object if possible
    309      for vers_sys in versioning_systems:
    310          try:
    311              vers_sys_class = __get_rcs_class(vers_sys)
    312              if not vers_sys_class is None:
    313                  return vers_sys_class(location)
    314          except IOError:
    315              continue
    316      # if 'location' is a symlink, then we should try the original file
    317      if follow_symlinks and os.path.islink(location):
    318          return get_versioned_object(os.path.realpath(location),
    319                  versioning_systems = versioning_systems,
    320                  follow_symlinks = False)
    321      # if everything fails:
    322      raise IOError("Could not find version control information: %s" % location)
    323
    324 -def get_available_version_control_systems():
    325      """ return the class objects of all locally available version control
    326      systems
    327      """
    328      result = []
    329      for rcs in DEFAULT_RCS:
    330          rcs_class = __get_rcs_class(rcs)
    331          if rcs_class:
    332              result.append(rcs_class)
    333      return result
    334
    335  # stay compatible to the previous version
    336 -def updatefile(filename):
    337      return get_versioned_object(filename).update()
    338
    339 -def getcleanfile(filename, revision=None):
    340      return get_versioned_object(filename).getcleanfile(revision)
    341
    342 -def commitfile(filename, message=None, author=None):
    343      return get_versioned_object(filename).commit(message=message, author=author)
    344
    345 -def commitdirectory(directory, message=None, author=None):
    346      """commit all files below the given directory
    347
    348      files that are just symlinked into the directory are supported, too
    349      """
    350      # for now all files are committed separately
    351      # should we combine them into one commit?
    352      for rcs_obj in get_versioned_objects_recursive(directory):
    353          rcs_obj.commit(message=message, author=author)
    354
    355 -def updatedirectory(directory):
    356      """update all files below the given directory
    357
    358      files that are just symlinked into the directory are supported, too
    359      """
    360      # for now all files are updated separately
    361      # should we combine them into one update?
    362      for rcs_obj in get_versioned_objects_recursive(directory):
    363          rcs_obj.update()
    364
    365 -def hasversioning(item):
    366      try:
    367          # try all available version control systems
    368          get_versioned_object(item)
    369          return True
    370      except IOError:
    371          return False
    372
    373
    374
    375  if __name__ == "__main__":
    376      import sys
    377      filenames = sys.argv[1:]
    378      if filenames:
    379          # try to retrieve the given (local) file from a repository
    380          for filename in filenames:
    381              contents = getcleanfile(filename)
    382              sys.stdout.write("\n\n******** %s ********\n\n" % filename)
    383              sys.stdout.write(contents)
    384      else:
    385          # first: make sure, that the translate toolkit is available
    386          # (useful if "python __init__.py" was called without an appropriate
    387          # PYTHONPATH)
    388          import translate.storage.versioncontrol
    389          # print the names of locally available version control systems
    390          for rcs in get_available_version_control_systems():
    391              print rcs
    392