for git v1.5.2 (and below): chdir to the directory of the target file before executin...
[translate_toolkit.git] / storage / versioncontrol / __init__.py
blobdf9d0ba2168de6b6c9a188de00a2b08fce931e5f
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", "git_old", "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 try:
82 proc = subprocess.Popen(args = command,
83 stdout = subprocess.PIPE,
84 stderr = subprocess.PIPE,
85 stdin = subprocess.PIPE)
86 (output, error) = proc.communicate()
87 ret = proc.returncode
88 return ret, output, error
89 except OSError, err_msg:
90 # failed to run the program (e.g. the executable was not found)
91 return -1, "", err_msg
93 except ImportError:
94 # fallback for python < 2.4
95 import popen2
97 def run_command(command):
98 """Runs a command (array of program name and arguments) and returns the
99 exitcode, the output and the error as a tuple.
101 There is no need to check for exceptions (like for subprocess above),
102 since popen2 opens a shell that will fail with an error code in case
103 of a missing executable.
105 escaped_command = " ".join([__shellescape(arg) for arg in command])
106 proc = popen2.Popen3(escaped_command, True)
107 (c_stdin, c_stdout, c_stderr) = (proc.tochild, proc.fromchild, proc.childerr)
108 output = c_stdout.read()
109 error = c_stderr.read()
110 ret = proc.wait()
111 c_stdout.close()
112 c_stderr.close()
113 c_stdin.close()
114 return ret, output, error
116 def __shellescape(path):
117 """Shell-escape any non-alphanumeric characters."""
118 return re.sub(r'(\W)', r'\\\1', path)
121 class GenericRevisionControlSystem:
122 """The super class for all version control classes.
124 Always inherit from this class to implement another RC interface.
126 At least the two attributes "RCS_METADIR" and "SCAN_PARENTS" must be
127 overriden by all implementations that derive from this class.
129 By default, all implementations can rely on the following attributes:
130 root_dir: the parent of the metadata directory of the working copy
131 location_abs: the absolute path of the RCS object
132 location_rel: the path of the RCS object relative to 'root_dir'
135 RCS_METADIR = None
136 """The name of the metadata directory of the RCS
138 e.g.: for Subversion -> ".svn"
141 SCAN_PARENTS = None
142 """whether to check the parent directories for the metadata directory of
143 the RCS working copy
145 some revision control systems store their metadata directory only
146 in the base of the working copy (e.g. bzr, GIT and Darcs)
147 use "True" for these RCS
149 other RCS store a metadata directory in every single directory of
150 the working copy (e.g. Subversion and CVS)
151 use "False" for these RCS
154 def __init__(self, location):
155 """find the relevant information about this RCS object
157 The IOError exception indicates that the specified object (file or
158 directory) is not controlled by the given version control system.
160 # check if the implementation looks ok - otherwise raise IOError
161 self._self_check()
162 # search for the repository information
163 result = self._find_rcs_directory(location)
164 if result is None:
165 raise IOError("Could not find revision control information: %s" \
166 % location)
167 else:
168 self.root_dir, self.location_abs, self.location_rel = result
170 def _find_rcs_directory(self, rcs_obj):
171 """Try to find the metadata directory of the RCS
173 returns a tuple:
174 the absolute path of the directory, that contains the metadata directory
175 the absolute path of the RCS object
176 the relative path of the RCS object based on the directory above
178 rcs_obj_dir = os.path.dirname(os.path.abspath(rcs_obj))
179 if os.path.isdir(os.path.join(rcs_obj_dir, self.RCS_METADIR)):
180 # is there a metadir next to the rcs_obj?
181 # (for Subversion, CVS, ...)
182 location_abs = os.path.abspath(rcs_obj)
183 location_rel = os.path.basename(location_abs)
184 return (rcs_obj_dir, location_abs, location_rel)
185 elif self.SCAN_PARENTS:
186 # scan for the metadir in parent directories
187 # (for bzr, GIT, Darcs, ...)
188 return self._find_rcs_in_parent_directories(rcs_obj)
189 else:
190 # no RCS metadata found
191 return None
193 def _find_rcs_in_parent_directories(self, rcs_obj):
194 """Try to find the metadata directory in all parent directories"""
195 # first: resolve possible symlinks
196 current_dir = os.path.dirname(os.path.realpath(rcs_obj))
197 # prevent infite loops
198 max_depth = 64
199 # stop as soon as we find the metadata directory
200 while not os.path.isdir(os.path.join(current_dir, self.RCS_METADIR)):
201 if os.path.dirname(current_dir) == current_dir:
202 # we reached the root directory - stop
203 return None
204 if max_depth <= 0:
205 # some kind of dead loop or a _very_ deep directory structure
206 return None
207 # go to the next higher level
208 current_dir = os.path.dirname(current_dir)
209 # the loop was finished successfully
210 # i.e.: we found the metadata directory
211 rcs_dir = current_dir
212 location_abs = os.path.realpath(rcs_obj)
213 # strip the base directory from the path of the rcs_obj
214 basedir = rcs_dir + os.path.sep
215 if location_abs.startswith(basedir):
216 # remove the base directory (including the trailing slash)
217 location_rel = location_abs.replace(basedir, "", 1)
218 # successfully finished
219 return (rcs_dir, location_abs, location_rel)
220 else:
221 # this should never happen
222 return None
224 def _self_check(self):
225 """Check if all necessary attributes are defined
227 Useful to make sure, that a new implementation does not forget
228 something like "RCS_METADIR"
230 if self.RCS_METADIR is None:
231 raise IOError("Incomplete RCS interface implementation: " \
232 + "self.RCS_METADIR is None")
233 if self.SCAN_PARENTS is None:
234 raise IOError("Incomplete RCS interface implementation: " \
235 + "self.SCAN_PARENTS is None")
236 # we do not check for implemented functions - they raise
237 # NotImplementedError exceptions anyway
238 return True
240 def getcleanfile(self, revision=None):
241 """Dummy to be overridden by real implementations"""
242 raise NotImplementedError("Incomplete RCS interface implementation:" \
243 + " 'getcleanfile' is missing")
246 def commit(self, revision=None):
247 """Dummy to be overridden by real implementations"""
248 raise NotImplementedError("Incomplete RCS interface implementation:" \
249 + " 'commit' is missing")
252 def update(self, revision=None):
253 """Dummy to be overridden by real implementations"""
254 raise NotImplementedError("Incomplete RCS interface implementation:" \
255 + " 'update' is missing")
258 def get_versioned_objects_recursive(
259 location,
260 versioning_systems=None,
261 follow_symlinks=True):
262 """return a list of objects, each pointing to a file below this directory
264 rcs_objs = []
265 if versioning_systems is None:
266 versioning_systems = DEFAULT_RCS
268 def scan_directory(arg, dirname, fnames):
269 for fname in fnames:
270 full_fname = os.path.join(dirname, fname)
271 if os.path.isfile(full_fname):
272 try:
273 rcs_objs.append(get_versioned_object(full_fname,
274 versioning_systems, follow_symlinks))
275 except IOError:
276 pass
278 os.path.walk(location, scan_directory, None)
279 return rcs_objs
281 def get_versioned_object(
282 location,
283 versioning_systems=None,
284 follow_symlinks=True):
285 """return a versioned object for the given file"""
286 if versioning_systems is None:
287 versioning_systems = DEFAULT_RCS
288 # go through all RCS and return a versioned object if possible
289 for vers_sys in versioning_systems:
290 try:
291 vers_sys_class = __get_rcs_class(vers_sys)
292 if not vers_sys_class is None:
293 return vers_sys_class(location)
294 except IOError:
295 continue
296 # if 'location' is a symlink, then we should try the original file
297 if follow_symlinks and os.path.islink(location):
298 return get_versioned_object(os.path.realpath(location),
299 versioning_systems = versioning_systems,
300 follow_symlinks = False)
301 # if everything fails:
302 raise IOError("Could not find version control information: %s" % location)
304 def get_available_version_control_systems():
305 """ return the class objects of all locally available version control
306 systems
308 result = []
309 for rcs in DEFAULT_RCS:
310 rcs_class = __get_rcs_class(rcs)
311 if rcs_class:
312 result.append(rcs_class)
313 return result
315 # stay compatible to the previous version
316 def updatefile(filename):
317 return get_versioned_object(filename).update()
319 def getcleanfile(filename, revision=None):
320 return get_versioned_object(filename).getcleanfile(revision)
322 def commitfile(filename, message=None):
323 return get_versioned_object(filename).commit(message)
325 def commitdirectory(directory, message=None):
326 """commit all files below the given directory
328 files that are just symlinked into the directory are supported, too
330 # for now all files are committed separately
331 # should we combine them into one commit?
332 for rcs_obj in get_versioned_objects_recursive(directory):
333 rcs_obj.commit(message)
335 def updatedirectory(directory):
336 """update all files below the given directory
338 files that are just symlinked into the directory are supported, too
340 # for now all files are updated separately
341 # should we combine them into one update?
342 for rcs_obj in get_versioned_objects_recursive(directory):
343 rcs_obj.update()
345 def hasversioning(item):
346 try:
347 # try all available version control systems
348 get_versioned_object(item)
349 return True
350 except IOError:
351 return False
355 if __name__ == "__main__":
356 import sys
357 filenames = sys.argv[1:]
358 if filenames:
359 # try to retrieve the given (local) file from a repository
360 for filename in filenames:
361 contents = getcleanfile(filename)
362 sys.stdout.write("\n\n******** %s ********\n\n" % filename)
363 sys.stdout.write(contents)
364 else:
365 # first: make sure, that the translate toolkit is available
366 # (useful if "python __init__.py" was called without an appropriate
367 # PYTHONPATH)
368 import translate.storage.versioncontrol
369 # print the names of locally available version control systems
370 for rcs in get_available_version_control_systems():
371 print rcs