fix git support for v1.5.3 (or higher) by setting "--work-tree"
[translate_toolkit.git] / storage / versioncontrol / __init__.py
blob5bcf302f0bba284f35f01249f8ac8ad4c77be2b5
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 authenticatin 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 rcs_class = getattr(module, name)
54 except [ImportError, AttributeError]:
55 return None
56 __CACHED_RCS_CLASSES[name] = rcs_class
57 return __CACHED_RCS_CLASSES[name]
60 # use either 'popen2' or 'subprocess' for command execution
61 try:
62 # available for python >= 2.4
63 import subprocess
65 # The subprocess module allows to use cross-platform command execution
66 # without using the shell (increases security).
68 def run_command(command):
69 """Runs a command (array of program name and arguments) and returns the
70 exitcode, the output and the error as a tuple.
71 """
72 # ok - we use "subprocess"
73 proc = subprocess.Popen(args = command,
74 stdout = subprocess.PIPE,
75 stderr = subprocess.PIPE,
76 stdin = subprocess.PIPE)
77 (output, error) = proc.communicate()
78 ret = proc.returncode
79 return ret, output, error
81 except ImportError:
82 # fallback for python < 2.4
83 import popen2
85 def run_command(command):
86 """Runs a command (array of program name and arguments) and returns the
87 exitcode, the output and the error as a tuple.
88 """
89 escaped_command = " ".join([__shellescape(arg) for arg in command])
90 proc = popen2.Popen3(escaped_command, True)
91 (c_stdin, c_stdout, c_stderr) = (proc.tochild, proc.fromchild, proc.childerr)
92 output = c_stdout.read()
93 error = c_stderr.read()
94 ret = proc.wait()
95 c_stdout.close()
96 c_stderr.close()
97 c_stdin.close()
98 return ret, output, error
100 def __shellescape(path):
101 """Shell-escape any non-alphanumeric characters."""
102 return re.sub(r'(\W)', r'\\\1', path)
105 class GenericRevisionControlSystem:
106 """The super class for all version control classes.
108 Always inherit from this class to implement another RC interface.
110 At least the two attributes "RCS_METADIR" and "SCAN_PARENTS" must be
111 overriden by all implementations that derive from this class.
113 By default, all implementations can rely on the following attributes:
114 root_dir: the parent of the metadata directory of the working copy
115 location_abs: the absolute path of the RCS object
116 location_rel: the path of the RCS object relative to 'root_dir'
119 RCS_METADIR = None
120 """The name of the metadata directory of the RCS
122 e.g.: for Subversion -> ".svn"
125 SCAN_PARENTS = None
126 """whether to check the parent directories for the metadata directory of
127 the RCS working copy
129 some revision control systems store their metadata directory only
130 in the base of the working copy (e.g. bzr, GIT and Darcs)
131 use "True" for these RCS
133 other RCS store a metadata directory in every single directory of
134 the working copy (e.g. Subversion and CVS)
135 use "False" for these RCS
138 def __init__(self, location):
139 """find the relevant information about this RCS object
141 The IOError exception indicates that the specified object (file or
142 directory) is not controlled by the given version control system.
144 # check if the implementation looks ok - otherwise raise IOError
145 self._self_check()
146 # search for the repository information
147 result = self._find_rcs_directory(location)
148 if result is None:
149 raise IOError("Could not find revision control information: %s" \
150 % location)
151 else:
152 self.root_dir, self.location_abs, self.location_rel = result
154 def _find_rcs_directory(self, rcs_obj):
155 """Try to find the metadata directory of the RCS
157 returns a tuple:
158 the absolute path of the directory, that contains the metadata directory
159 the absolute path of the RCS object
160 the relative path of the RCS object based on the directory above
162 rcs_obj_dir = os.path.dirname(os.path.abspath(rcs_obj))
163 if os.path.isdir(os.path.join(rcs_obj_dir, self.RCS_METADIR)):
164 # is there a metadir next to the rcs_obj?
165 # (for Subversion, CVS, ...)
166 location_abs = os.path.abspath(rcs_obj)
167 location_rel = os.path.basename(location_abs)
168 return (rcs_obj_dir, location_abs, location_rel)
169 elif self.SCAN_PARENTS:
170 # scan for the metadir in parent directories
171 # (for bzr, GIT, Darcs, ...)
172 return self._find_rcs_in_parent_directories(rcs_obj)
173 else:
174 # no RCS metadata found
175 return None
177 def _find_rcs_in_parent_directories(self, rcs_obj):
178 """Try to find the metadata directory in all parent directories"""
179 # first: resolve possible symlinks
180 current_dir = os.path.dirname(os.path.realpath(rcs_obj))
181 # prevent infite loops
182 max_depth = 64
183 # stop as soon as we find the metadata directory
184 while not os.path.isdir(os.path.join(current_dir, self.RCS_METADIR)):
185 if os.path.dirname(current_dir) == current_dir:
186 # we reached the root directory - stop
187 return None
188 if max_depth <= 0:
189 # some kind of dead loop or a _very_ deep directory structure
190 return None
191 # go to the next higher level
192 current_dir = os.path.dirname(current_dir)
193 # the loop was finished successfully
194 # i.e.: we found the metadata directory
195 rcs_dir = current_dir
196 location_abs = os.path.realpath(rcs_obj)
197 # strip the base directory from the path of the rcs_obj
198 basedir = rcs_dir + os.path.sep
199 if location_abs.startswith(basedir):
200 # remove the base directory (including the trailing slash)
201 location_rel = location_abs.replace(basedir, "", 1)
202 # successfully finished
203 return (rcs_dir, location_abs, location_rel)
204 else:
205 # this should never happen
206 return None
208 def _self_check(self):
209 """Check if all necessary attributes are defined
211 Useful to make sure, that a new implementation does not forget
212 something like "RCS_METADIR"
214 if self.RCS_METADIR is None:
215 raise IOError("Incomplete RCS interface implementation: " \
216 + "self.RCS_METADIR is None")
217 if self.SCAN_PARENTS is None:
218 raise IOError("Incomplete RCS interface implementation: " \
219 + "self.SCAN_PARENTS is None")
220 # we do not check for implemented functions - they raise
221 # NotImplementedError exceptions anyway
222 return True
224 def getcleanfile(self, revision=None):
225 """Dummy to be overridden by real implementations"""
226 raise NotImplementedError("Incomplete RCS interface implementation:" \
227 + " 'getcleanfile' is missing")
230 def commit(self, revision=None):
231 """Dummy to be overridden by real implementations"""
232 raise NotImplementedError("Incomplete RCS interface implementation:" \
233 + " 'commit' is missing")
236 def update(self, revision=None):
237 """Dummy to be overridden by real implementations"""
238 raise NotImplementedError("Incomplete RCS interface implementation:" \
239 + " 'update' is missing")
242 def get_versioned_objects_recursive(
243 location,
244 versioning_systems=DEFAULT_RCS,
245 follow_symlinks=True):
246 """return a list of objcts, each pointing to a file below this directory
248 rcs_objs = []
250 def scan_directory(arg, dirname, fnames):
251 for fname in fnames:
252 full_fname = os.path.join(dirname, fname)
253 if os.path.isfile(full_fname):
254 try:
255 rcs_objs.append(get_versioned_object(full_fname,
256 versioning_systems, follow_symlinks))
257 except IOError:
258 pass
260 os.path.walk(location, scan_directory, None)
261 return rcs_objs
263 def get_versioned_object(
264 location,
265 versioning_systems=DEFAULT_RCS,
266 follow_symlinks=True):
267 """return a versioned object for the given file"""
268 # go through all RCS and return a versioned object if possible
269 for vers_sys in versioning_systems:
270 try:
271 vers_sys_class = __get_rcs_class(vers_sys)
272 if not vers_sys_class is None:
273 return vers_sys_class(location)
274 except IOError:
275 continue
276 # if 'location' is a symlink, then we should try the original file
277 if follow_symlinks and os.path.islink(location):
278 return get_versioned_object(os.path.realpath(location),
279 versioning_systems = versioning_systems,
280 follow_symlinks = False)
281 # if everything fails:
282 raise IOError("Could not find version control information: %s" % location)
284 # stay compatible to the previous version
285 def updatefile(filename):
286 return get_versioned_object(filename).update()
288 def getcleanfile(filename, revision=None):
289 return get_versioned_object(filename).getcleanfile(revision)
291 def commitfile(filename, message=None):
292 return get_versioned_object(filename).commit(message)
294 def commitdirectory(directory, message=None):
295 """commit all files below the given directory
297 files that are just symlinked into the directory are supported, too
299 # for now all files are committed separately
300 # should we combine them into one commit?
301 for rcs_obj in get_versioned_objects_recursive(directory):
302 rcs_obj.commit(message)
304 def updatedirectory(directory):
305 """update all files below the given directory
307 files that are just symlinked into the directory are supported, too
309 # for now all files are updated separately
310 # should we combine them into one update?
311 for rcs_obj in get_versioned_objects_recursive(directory):
312 rcs_obj.update()
314 def hasversioning(item):
315 try:
316 # try all available version control systems
317 get_versioned_object(item)
318 return True
319 except IOError:
320 return False
324 if __name__ == "__main__":
325 import sys
326 filenames = sys.argv[1:]
327 for filename in filenames:
328 contents = getcleanfile(filename)
329 sys.stdout.write("\n\n******** %s ********\n\n" % filename)
330 sys.stdout.write(contents)