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