add "is_available" function to all version control modules
[translate_toolkit.git] / storage / versioncontrol / __init__.py
blob409f80539d212ac5b6fd4d06a4296aa4b143ba71
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.
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.
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.
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
22 """This module manages interaction with version control systems.
24 To implement support for a new version control system, inherit the class
25 GenericRevisionControlSystem.
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 """
33 import re
34 import os
36 DEFAULT_RCS = ["svn", "cvs", "darcs", "git", "bzr", "hg"]
37 """the names of all supported revision control systems
39 modules of the same name containing a class with the same name are expected
40 to be defined below 'translate.storage.versioncontrol'
41 """
43 __CACHED_RCS_CLASSES = {}
44 """The dynamically loaded revision control system implementations (python
45 modules) are cached here for faster access.
46 """
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]
68 # use either 'popen2' or 'subprocess' for command execution
69 try:
70 # available for python >= 2.4
71 import subprocess
73 # The subprocess module allows to use cross-platform command execution
74 # without using the shell (increases security).
76 def run_command(command):
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 # ok - we use "subprocess"
81 proc = subprocess.Popen(args = command,
82 stdout = subprocess.PIPE,
83 stderr = subprocess.PIPE,
84 stdin = subprocess.PIPE)
85 (output, error) = proc.communicate()
86 ret = proc.returncode
87 return ret, output, error
89 except ImportError:
90 # fallback for python < 2.4
91 import popen2
93 def run_command(command):
94 """Runs a command (array of program name and arguments) and returns the
95 exitcode, the output and the error as a tuple.
96 """
97 escaped_command = " ".join([__shellescape(arg) for arg in command])
98 proc = popen2.Popen3(escaped_command, True)
99 (c_stdin, c_stdout, c_stderr) = (proc.tochild, proc.fromchild, proc.childerr)
100 output = c_stdout.read()
101 error = c_stderr.read()
102 ret = proc.wait()
103 c_stdout.close()
104 c_stderr.close()
105 c_stdin.close()
106 return ret, output, error
108 def __shellescape(path):
109 """Shell-escape any non-alphanumeric characters."""
110 return re.sub(r'(\W)', r'\\\1', path)
113 class GenericRevisionControlSystem:
114 """The super class for all version control classes.
116 Always inherit from this class to implement another RC interface.
118 At least the two attributes "RCS_METADIR" and "SCAN_PARENTS" must be
119 overriden by all implementations that derive from this class.
121 By default, all implementations can rely on the following attributes:
122 root_dir: the parent of the metadata directory of the working copy
123 location_abs: the absolute path of the RCS object
124 location_rel: the path of the RCS object relative to 'root_dir'
127 RCS_METADIR = None
128 """The name of the metadata directory of the RCS
130 e.g.: for Subversion -> ".svn"
133 SCAN_PARENTS = None
134 """whether to check the parent directories for the metadata directory of
135 the RCS working copy
137 some revision control systems store their metadata directory only
138 in the base of the working copy (e.g. bzr, GIT and Darcs)
139 use "True" for these RCS
141 other RCS store a metadata directory in every single directory of
142 the working copy (e.g. Subversion and CVS)
143 use "False" for these RCS
146 def __init__(self, location):
147 """find the relevant information about this RCS object
149 The IOError exception indicates that the specified object (file or
150 directory) is not controlled by the given version control system.
152 # check if the implementation looks ok - otherwise raise IOError
153 self._self_check()
154 # search for the repository information
155 result = self._find_rcs_directory(location)
156 if result is None:
157 raise IOError("Could not find revision control information: %s" \
158 % location)
159 else:
160 self.root_dir, self.location_abs, self.location_rel = result
162 def _find_rcs_directory(self, rcs_obj):
163 """Try to find the metadata directory of the RCS
165 returns a tuple:
166 the absolute path of the directory, that contains the metadata directory
167 the absolute path of the RCS object
168 the relative path of the RCS object based on the directory above
170 rcs_obj_dir = os.path.dirname(os.path.abspath(rcs_obj))
171 if os.path.isdir(os.path.join(rcs_obj_dir, self.RCS_METADIR)):
172 # is there a metadir next to the rcs_obj?
173 # (for Subversion, CVS, ...)
174 location_abs = os.path.abspath(rcs_obj)
175 location_rel = os.path.basename(location_abs)
176 return (rcs_obj_dir, location_abs, location_rel)
177 elif self.SCAN_PARENTS:
178 # scan for the metadir in parent directories
179 # (for bzr, GIT, Darcs, ...)
180 return self._find_rcs_in_parent_directories(rcs_obj)
181 else:
182 # no RCS metadata found
183 return None
185 def _find_rcs_in_parent_directories(self, rcs_obj):
186 """Try to find the metadata directory in all parent directories"""
187 # first: resolve possible symlinks
188 current_dir = os.path.dirname(os.path.realpath(rcs_obj))
189 # prevent infite loops
190 max_depth = 64
191 # stop as soon as we find the metadata directory
192 while not os.path.isdir(os.path.join(current_dir, self.RCS_METADIR)):
193 if os.path.dirname(current_dir) == current_dir:
194 # we reached the root directory - stop
195 return None
196 if max_depth <= 0:
197 # some kind of dead loop or a _very_ deep directory structure
198 return None
199 # go to the next higher level
200 current_dir = os.path.dirname(current_dir)
201 # the loop was finished successfully
202 # i.e.: we found the metadata directory
203 rcs_dir = current_dir
204 location_abs = os.path.realpath(rcs_obj)
205 # strip the base directory from the path of the rcs_obj
206 basedir = rcs_dir + os.path.sep
207 if location_abs.startswith(basedir):
208 # remove the base directory (including the trailing slash)
209 location_rel = location_abs.replace(basedir, "", 1)
210 # successfully finished
211 return (rcs_dir, location_abs, location_rel)
212 else:
213 # this should never happen
214 return None
216 def _self_check(self):
217 """Check if all necessary attributes are defined
219 Useful to make sure, that a new implementation does not forget
220 something like "RCS_METADIR"
222 if self.RCS_METADIR is None:
223 raise IOError("Incomplete RCS interface implementation: " \
224 + "self.RCS_METADIR is None")
225 if self.SCAN_PARENTS is None:
226 raise IOError("Incomplete RCS interface implementation: " \
227 + "self.SCAN_PARENTS is None")
228 # we do not check for implemented functions - they raise
229 # NotImplementedError exceptions anyway
230 return True
232 def getcleanfile(self, revision=None):
233 """Dummy to be overridden by real implementations"""
234 raise NotImplementedError("Incomplete RCS interface implementation:" \
235 + " 'getcleanfile' is missing")
238 def commit(self, revision=None):
239 """Dummy to be overridden by real implementations"""
240 raise NotImplementedError("Incomplete RCS interface implementation:" \
241 + " 'commit' is missing")
244 def update(self, revision=None):
245 """Dummy to be overridden by real implementations"""
246 raise NotImplementedError("Incomplete RCS interface implementation:" \
247 + " 'update' is missing")
250 def get_versioned_objects_recursive(
251 location,
252 versioning_systems=None,
253 follow_symlinks=True):
254 """return a list of objects, each pointing to a file below this directory
256 rcs_objs = []
257 if versioning_systems is None:
258 versioning_systems = DEFAULT_RCS
260 def scan_directory(arg, dirname, fnames):
261 for fname in fnames:
262 full_fname = os.path.join(dirname, fname)
263 if os.path.isfile(full_fname):
264 try:
265 rcs_objs.append(get_versioned_object(full_fname,
266 versioning_systems, follow_symlinks))
267 except IOError:
268 pass
270 os.path.walk(location, scan_directory, None)
271 return rcs_objs
273 def get_versioned_object(
274 location,
275 versioning_systems=None,
276 follow_symlinks=True):
277 """return a versioned object for the given file"""
278 if versioning_systems is None:
279 versioning_systems = DEFAULT_RCS
280 # go through all RCS and return a versioned object if possible
281 for vers_sys in versioning_systems:
282 try:
283 vers_sys_class = __get_rcs_class(vers_sys)
284 if not vers_sys_class is None:
285 return vers_sys_class(location)
286 except IOError:
287 continue
288 # if 'location' is a symlink, then we should try the original file
289 if follow_symlinks and os.path.islink(location):
290 return get_versioned_object(os.path.realpath(location),
291 versioning_systems = versioning_systems,
292 follow_symlinks = False)
293 # if everything fails:
294 raise IOError("Could not find version control information: %s" % location)
296 def get_available_version_control_systems():
297 """ return the class objects of all locally available version control
298 systems
300 result = []
301 for rcs in DEFAULT_RCS:
302 rcs_class = __get_rcs_class(rcs)
303 if rcs_class:
304 result.append(rcs_class)
305 return result
307 # stay compatible to the previous version
308 def updatefile(filename):
309 return get_versioned_object(filename).update()
311 def getcleanfile(filename, revision=None):
312 return get_versioned_object(filename).getcleanfile(revision)
314 def commitfile(filename, message=None):
315 return get_versioned_object(filename).commit(message)
317 def commitdirectory(directory, message=None):
318 """commit all files below the given directory
320 files that are just symlinked into the directory are supported, too
322 # for now all files are committed separately
323 # should we combine them into one commit?
324 for rcs_obj in get_versioned_objects_recursive(directory):
325 rcs_obj.commit(message)
327 def updatedirectory(directory):
328 """update all files below the given directory
330 files that are just symlinked into the directory are supported, too
332 # for now all files are updated separately
333 # should we combine them into one update?
334 for rcs_obj in get_versioned_objects_recursive(directory):
335 rcs_obj.update()
337 def hasversioning(item):
338 try:
339 # try all available version control systems
340 get_versioned_object(item)
341 return True
342 except IOError:
343 return False
347 if __name__ == "__main__":
348 import sys
349 filenames = sys.argv[1:]
350 if filenames:
351 # try to retrieve the given (local) file from a repository
352 for filename in filenames:
353 contents = getcleanfile(filename)
354 sys.stdout.write("\n\n******** %s ********\n\n" % filename)
355 sys.stdout.write(contents)
356 else:
357 # first: make sure, that the translate toolkit is available
358 # (useful if "python __init__.py" was called without an appropriate
359 # PYTHONPATH)
360 import translate.storage.versioncontrol
361 # print the names of locally available version control systems
362 for rcs in get_available_version_control_systems():
363 print rcs