Rewrite CVSDirectory.get_path_components() without recursion.
[cvs2svn.git] / cvs2svn_lib / cvs_path.py
blob318eafe6f3db2a26756e5a53bb51527c66f8ed67
1 # (Be in -*- python -*- mode.)
3 # ====================================================================
4 # Copyright (c) 2000-2008 CollabNet. All rights reserved.
6 # This software is licensed as described in the file COPYING, which
7 # you should have received as part of this distribution. The terms
8 # are also available at http://subversion.tigris.org/license-1.html.
9 # If newer versions of this license are posted there, you may use a
10 # newer version instead, at your option.
12 # This software consists of voluntary contributions made by many
13 # individuals. For exact contribution history, see the revision
14 # history and logs, available at http://cvs2svn.tigris.org/.
15 # ====================================================================
17 """Classes that represent files and directories within CVS repositories."""
19 import os
21 from cvs2svn_lib.common import path_join
22 from cvs2svn_lib.context import Ctx
25 class CVSPath(object):
26 """Represent a CVS file or directory.
28 Members:
30 id -- (int) unique ID for this CVSPath. At any moment, there is
31 at most one CVSPath instance with a particular ID. (This
32 means that object identity is the same as object equality, and
33 objects can be used as map keys even though they don't have a
34 __hash__() method).
36 project -- (Project) the project containing this CVSPath.
38 parent_directory -- (CVSDirectory or None) the CVSDirectory
39 containing this CVSPath.
41 rcs_basename -- (string) the base name of the filename path in the
42 CVS repository corresponding to this CVSPath (but with ',v'
43 removed for CVSFiles). The rcs_basename of the root directory
44 of a project is ''.
46 rcs_path -- (string) the filesystem path to this CVSPath in the
47 CVS repository. This is in native format, and already
48 normalised the way os.path.normpath() normalises paths. It
49 starts with the repository path passed to
50 run_options.add_project() in the options.py file.
52 ordinal -- (int) the order that this instance should be sorted
53 relative to other CVSPath instances. This member is set based
54 on the ordering imposed by sort_key() by CVSPathDatabase after
55 all CVSFiles have been processed. Comparisons of CVSPath
56 using __cmp__() simply compare the ordinals.
58 """
60 __slots__ = [
61 'id',
62 'project',
63 'parent_directory',
64 'rcs_basename',
65 'ordinal',
66 'rcs_path',
69 def __init__(self, id, project, parent_directory, rcs_basename):
70 self.id = id
71 self.project = project
72 self.parent_directory = parent_directory
73 self.rcs_basename = rcs_basename
75 # The rcs_path used to be computed on demand, but it turned out to
76 # be a hot path through the code in some cases. It's used by
77 # SubtreeSymbolTransform and similar transforms, so it's called at
78 # least:
80 # (num_files * num_symbols_per_file * num_subtree_symbol_transforms)
82 # times. On a large repository with several subtree symbol
83 # transforms, that can exceed 100,000,000 calls. And
84 # _calculate_rcs_path() is quite complex, so doing that every time
85 # could add about 10 minutes to the cvs2svn runtime.
87 # So now we precalculate this and just return it.
88 self.rcs_path = os.path.normpath(self._calculate_rcs_path())
90 def __getstate__(self):
91 """This method must only be called after ordinal has been set."""
93 return (
94 self.id, self.project.id,
95 self.parent_directory, self.rcs_basename,
96 self.ordinal,
99 def __setstate__(self, state):
101 self.id, project_id,
102 self.parent_directory, self.rcs_basename,
103 self.ordinal,
104 ) = state
105 self.project = Ctx()._projects[project_id]
106 self.rcs_path = os.path.normpath(self._calculate_rcs_path())
108 def get_ancestry(self):
109 """Return a list of the CVSPaths leading from the root path to SELF.
111 Return the CVSPaths in a list, starting with
112 self.project.get_root_cvs_directory() and ending with self."""
114 ancestry = []
115 p = self
116 while p is not None:
117 ancestry.append(p)
118 p = p.parent_directory
120 ancestry.reverse()
121 return ancestry
123 def get_path_components(self, rcs=False):
124 """Return the path components to this CVSPath.
126 Return the components of this CVSPath's path, relative to the
127 project's project_cvs_repos_path, as a list of strings. If rcs is
128 True, return the components of the filesystem path to the RCS file
129 corresponding to this CVSPath (i.e., including any 'Attic'
130 component and trailing ',v'. If rcs is False, return the
131 components of the logical CVS path name (i.e., including 'Attic'
132 only if the file is to be left in an Attic directory in the SVN
133 repository and without trailing ',v')."""
135 raise NotImplementedError()
137 def get_cvs_path(self):
138 """Return the canonical path within the Project.
140 The canonical path:
142 - Uses forward slashes
144 - Doesn't include ',v' for files
146 - This doesn't include the 'Attic' segment of the path unless the
147 file is to be left in an Attic directory in the SVN repository;
148 i.e., if a filename exists in and out of Attic and the
149 --retain-conflicting-attic-files option was specified.
153 return path_join(*[p.rcs_basename for p in self.get_ancestry()[1:]])
155 cvs_path = property(get_cvs_path)
157 def _get_dir_components(self):
158 """Return a list containing the components of the path leading to SELF.
160 The return value contains the base names of all of the parent
161 directories (except for the root directory) and SELF."""
163 return [p.rcs_basename for p in self.get_ancestry()[1:]]
165 def _calculate_rcs_path(self):
166 """Return the filesystem path in the CVS repo corresponding to SELF."""
168 return os.path.join(
169 self.project.project_cvs_repos_path,
170 *self.get_path_components(rcs=True)
173 def __eq__(a, b):
174 """Compare two CVSPath instances for equality.
176 This method is supplied to avoid using __cmp__() for comparing for
177 equality."""
179 return a is b
181 def sort_key(self):
182 """Return the key that should be used for sorting CVSPath instances.
184 This is a relatively expensive computation, so it is only used
185 once, the the results are used to set the ordinal member."""
187 return (
188 # Sort first by project:
189 self.project,
190 # Then by directory components:
191 self._get_dir_components(),
194 def __cmp__(a, b):
195 """This method must only be called after ordinal has been set."""
197 return cmp(a.ordinal, b.ordinal)
200 class CVSDirectory(CVSPath):
201 """Represent a CVS directory.
203 Members:
205 id -- (int or None) unique id for this file. If None, a new id is
206 generated.
208 project -- (Project) the project containing this file.
210 parent_directory -- (CVSDirectory or None) the CVSDirectory
211 containing this CVSDirectory.
213 rcs_basename -- (string) the base name of the filename path in the
214 CVS repository corresponding to this CVSDirectory. The
215 rcs_basename of the root directory of a project is ''.
217 ordinal -- (int) the order that this instance should be sorted
218 relative to other CVSPath instances. See CVSPath.ordinal.
220 empty_subdirectory_ids -- (list of int) a list of the ids of any
221 direct subdirectories that are empty. (An empty directory is
222 defined to be a directory that doesn't contain any RCS files
223 or non-empty subdirectories.
227 __slots__ = ['empty_subdirectory_ids']
229 def __init__(self, id, project, parent_directory, rcs_basename):
230 """Initialize a new CVSDirectory object."""
232 CVSPath.__init__(self, id, project, parent_directory, rcs_basename)
234 # This member is filled in by CollectData.close():
235 self.empty_subdirectory_ids = []
237 def get_path_components(self, rcs=False):
238 components = []
239 p = self
240 while p.parent_directory is not None:
241 components.append(p.rcs_basename)
242 p = p.parent_directory
244 components.reverse()
245 return components
247 def __getstate__(self):
248 return (
249 CVSPath.__getstate__(self),
250 self.empty_subdirectory_ids,
253 def __setstate__(self, state):
255 cvs_path_state,
256 self.empty_subdirectory_ids,
257 ) = state
258 CVSPath.__setstate__(self, cvs_path_state)
260 def __str__(self):
261 """For convenience only. The format is subject to change at any time."""
263 return self.cvs_path + '/'
265 def __repr__(self):
266 return 'CVSDirectory<%x>(%r)' % (self.id, str(self),)
269 class CVSFile(CVSPath):
270 """Represent a CVS file.
272 Members:
274 id -- (int) unique id for this file.
276 project -- (Project) the project containing this file.
278 parent_directory -- (CVSDirectory) the CVSDirectory containing
279 this CVSFile.
281 rcs_basename -- (string) the base name of the RCS file in the CVS
282 repository corresponding to this CVSPath (but with the ',v'
283 removed).
285 ordinal -- (int) the order that this instance should be sorted
286 relative to other CVSPath instances. See CVSPath.ordinal.
288 _in_attic -- (bool) True if RCS file is in an Attic subdirectory
289 that is not considered the parent directory. (If a file is
290 in-and-out-of-attic and one copy is to be left in Attic after
291 the conversion, then the Attic directory is that file's
292 PARENT_DIRECTORY and _IN_ATTIC is False.)
294 executable -- (bool) True iff RCS file has executable bit set.
296 file_size -- (long) size of the RCS file in bytes.
298 mode -- (string or None) 'kv', 'b', etc., as read from the CVS
299 file.
301 description -- (string or None) the file description as read from
302 the RCS file.
304 properties -- (dict) file properties that are preserved across
305 this history of this file. Keys are strings; values are
306 strings (indicating the property value) or None (indicating
307 that the property should be left unset). These properties can
308 be overridden by CVSRevision.properties. Different backends
309 can use these properties for different purposes; for cvs2svn
310 they become SVN versioned properties. Properties whose names
311 start with underscore are reserved for internal cvs2svn
312 purposes.
314 PARENT_DIRECTORY might contain an 'Attic' component if it should be
315 retained in the SVN repository; i.e., if the same filename exists
316 out of Attic and the --retain-conflicting-attic-files option was
317 specified.
321 __slots__ = [
322 '_in_attic',
323 'executable',
324 'file_size',
325 'mode',
326 'description',
327 'properties',
330 def __init__(
331 self, id, project, parent_directory, rcs_basename, in_attic,
332 executable, file_size, mode, description
334 """Initialize a new CVSFile object."""
336 assert parent_directory is not None
338 # This member is needed by _calculate_rcs_path(), which is
339 # called by CVSPath.__init__(). So initialize it before calling
340 # CVSPath.__init__().
341 self._in_attic = in_attic
342 CVSPath.__init__(self, id, project, parent_directory, rcs_basename)
344 self.executable = executable
345 self.file_size = file_size
346 self.mode = mode
347 self.description = description
348 self.properties = None
350 def determine_file_properties(self, file_property_setters):
351 """Determine the properties for this file from FILE_PROPERTY_SETTERS.
353 This must only be called after SELF.mode and SELF.description have
354 been set by CollectData."""
356 self.properties = {}
358 for file_property_setter in file_property_setters:
359 file_property_setter.set_properties(self)
361 def get_path_components(self, rcs=False):
362 components = self.parent_directory.get_path_components(rcs=rcs)
363 if rcs:
364 if self._in_attic:
365 components.append('Attic')
366 components.append(self.rcs_basename + ',v')
367 else:
368 components.append(self.rcs_basename)
369 return components
371 def __getstate__(self):
372 return (
373 CVSPath.__getstate__(self),
374 self._in_attic, self.executable, self.file_size, self.mode,
375 self.description, self.properties,
378 def __setstate__(self, state):
380 cvs_path_state,
381 self._in_attic, self.executable, self.file_size, self.mode,
382 self.description, self.properties,
383 ) = state
384 CVSPath.__setstate__(self, cvs_path_state)
386 def __str__(self):
387 """For convenience only. The format is subject to change at any time."""
389 return self.cvs_path
391 def __repr__(self):
392 return 'CVSFile<%x>(%r)' % (self.id, str(self),)