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