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