1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
66
67
68
69 try:
70
71 import subprocess
72
73
74
75
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
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
97 return -1, "", err_msg
98
99 except ImportError:
100
101 import popen2
102
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
119
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
132 """Shell-escape any non-alphanumeric characters."""
133 return re.sub(r'(\W)', r'\\\1', path)
134
135
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
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
176 self._self_check()
177
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
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
201
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
207
208 return self._find_rcs_in_parent_directories(rcs_obj)
209 else:
210
211 return None
212
214 """Try to find the metadata directory in all parent directories"""
215
216 current_dir = os.path.dirname(os.path.realpath(rcs_obj))
217
218 max_depth = 64
219
220 while not os.path.isdir(os.path.join(current_dir, self.RCS_METADIR)):
221 if os.path.dirname(current_dir) == current_dir:
222
223 return None
224 if max_depth <= 0:
225
226 return None
227
228 current_dir = os.path.dirname(current_dir)
229
230
231 rcs_dir = current_dir
232 location_abs = os.path.realpath(rcs_obj)
233
234 basedir = rcs_dir + os.path.sep
235 if location_abs.startswith(basedir):
236
237 location_rel = location_abs.replace(basedir, "", 1)
238
239 return (rcs_dir, location_abs, location_rel)
240 else:
241
242 return None
243
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
257
258 return True
259
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
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
305 """return a versioned object for the given file"""
306 if versioning_systems is None:
307 versioning_systems = DEFAULT_RCS
308
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
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
322 raise IOError("Could not find version control information: %s" % location)
323
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
337 return get_versioned_object(filename).update()
338
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
346 """commit all files below the given directory
347
348 files that are just symlinked into the directory are supported, too
349 """
350
351
352 for rcs_obj in get_versioned_objects_recursive(directory):
353 rcs_obj.commit(message=message, author=author)
354
356 """update all files below the given directory
357
358 files that are just symlinked into the directory are supported, too
359 """
360
361
362 for rcs_obj in get_versioned_objects_recursive(directory):
363 rcs_obj.update()
364
366 try:
367
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
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
386
387
388 import translate.storage.versioncontrol
389
390 for rcs in get_available_version_control_systems():
391 print rcs
392