Break out of loop correctly when the first file difference is found.
[cvs2svn.git] / cvs2svn_lib / project.py
blobcef80a72fb6c1e8fa29112f035e0811cf2f8c77a
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 """This module contains database facilities used by cvs2svn."""
20 import os
21 import cPickle
23 from cvs2svn_lib.context import Ctx
24 from cvs2svn_lib.common import FatalError
25 from cvs2svn_lib.common import IllegalSVNPathError
26 from cvs2svn_lib.common import normalize_svn_path
27 from cvs2svn_lib.common import verify_paths_disjoint
28 from cvs2svn_lib.symbol_transform import CompoundSymbolTransform
31 class FileInAndOutOfAtticException(Exception):
32 def __init__(self, non_attic_path, attic_path):
33 Exception.__init__(
34 self,
35 "A CVS repository cannot contain both %s and %s"
36 % (non_attic_path, attic_path))
38 self.non_attic_path = non_attic_path
39 self.attic_path = attic_path
42 def normalize_ttb_path(opt, path, allow_empty=False):
43 try:
44 return normalize_svn_path(path, allow_empty)
45 except IllegalSVNPathError, e:
46 raise FatalError('Problem with %s: %s' % (opt, e,))
49 class Project(object):
50 """A project within a CVS repository."""
52 def __init__(
53 self, id, project_cvs_repos_path,
54 initial_directories=[],
55 symbol_transforms=None,
57 """Create a new Project record.
59 ID is a unique id for this project. PROJECT_CVS_REPOS_PATH is the
60 main CVS directory for this project (within the filesystem).
62 INITIAL_DIRECTORIES is an iterable of all SVN directories that
63 should be created when the project is first created. Normally,
64 this should include the trunk, branches, and tags directory.
66 SYMBOL_TRANSFORMS is an iterable of SymbolTransform instances
67 which will be used to transform any symbol names within this
68 project."""
70 self.id = id
72 self.project_cvs_repos_path = os.path.normpath(project_cvs_repos_path)
73 if not os.path.isdir(self.project_cvs_repos_path):
74 raise FatalError("The specified CVS repository path '%s' is not an "
75 "existing directory." % self.project_cvs_repos_path)
77 self.cvs_repository_root, self.cvs_module = \
78 self.determine_repository_root(
79 os.path.abspath(self.project_cvs_repos_path))
81 # The SVN directories to add when the project is first created:
82 self._initial_directories = []
84 for path in initial_directories:
85 try:
86 path = normalize_svn_path(path, False)
87 except IllegalSVNPathError, e:
88 raise FatalError(
89 'Initial directory %r is not a legal SVN path: %s'
90 % (path, e,)
92 self._initial_directories.append(path)
94 verify_paths_disjoint(*self._initial_directories)
96 # A list of transformation rules (regexp, replacement) applied to
97 # symbol names in this project.
98 if symbol_transforms is None:
99 symbol_transforms = []
101 self.symbol_transform = CompoundSymbolTransform(symbol_transforms)
103 # The ID of the Trunk instance for this Project. This member is
104 # filled in during CollectRevsPass.
105 self.trunk_id = None
107 # The ID of the CVSDirectory representing the root directory of
108 # this project. This member is filled in during CollectRevsPass.
109 self.root_cvs_directory_id = None
111 def __eq__(self, other):
112 return self.id == other.id
114 def __cmp__(self, other):
115 return cmp(self.cvs_module, other.cvs_module) \
116 or cmp(self.id, other.id)
118 def __hash__(self):
119 return self.id
121 @staticmethod
122 def determine_repository_root(path):
123 """Ascend above the specified PATH if necessary to find the
124 cvs_repository_root (a directory containing a CVSROOT directory)
125 and the cvs_module (the path of the conversion root within the cvs
126 repository). Return the root path and the module path of this
127 project relative to the root.
129 NB: cvs_module must be seperated by '/', *not* by os.sep."""
131 def is_cvs_repository_root(path):
132 return os.path.isdir(os.path.join(path, 'CVSROOT'))
134 original_path = path
135 cvs_module = ''
136 while not is_cvs_repository_root(path):
137 # Step up one directory:
138 prev_path = path
139 path, module_component = os.path.split(path)
140 if path == prev_path:
141 # Hit the root (of the drive, on Windows) without finding a
142 # CVSROOT dir.
143 raise FatalError(
144 "the path '%s' is not a CVS repository, nor a path "
145 "within a CVS repository. A CVS repository contains "
146 "a CVSROOT directory within its root directory."
147 % (original_path,))
149 cvs_module = module_component + "/" + cvs_module
151 return path, cvs_module
153 def transform_symbol(self, cvs_file, symbol_name, revision):
154 """Transform the symbol SYMBOL_NAME.
156 SYMBOL_NAME refers to revision number REVISION in CVS_FILE.
157 REVISION is the CVS revision number as a string, with zeros
158 removed (e.g., '1.7' or '1.7.2'). Use the renaming rules
159 specified with --symbol-transform to possibly rename the symbol.
160 Return the transformed symbol name, the original name if it should
161 not be transformed, or None if the symbol should be omitted from
162 the conversion."""
164 return self.symbol_transform.transform(cvs_file, symbol_name, revision)
166 def get_trunk(self):
167 """Return the Trunk instance for this project.
169 This method can only be called after self.trunk_id has been
170 initialized in CollectRevsPass."""
172 return Ctx()._symbol_db.get_symbol(self.trunk_id)
174 def get_root_cvs_directory(self):
175 """Return the root CVSDirectory instance for this project.
177 This method can only be called after self.root_cvs_directory_id
178 has been initialized in CollectRevsPass."""
180 return Ctx()._cvs_path_db.get_path(self.root_cvs_directory_id)
182 def get_initial_directories(self):
183 """Generate the project's initial SVN directories.
185 Yield as strings the SVN paths of directories that should be
186 created when the project is first created."""
188 # Yield the path of the Trunk symbol for this project (which might
189 # differ from the one passed to the --trunk option because of
190 # SymbolStrategyRules). The trunk path might be '' during a
191 # trunk-only conversion, but that is OK because DumpstreamDelegate
192 # considers that directory to exist already and will therefore
193 # ignore it:
194 yield self.get_trunk().base_path
196 for path in self._initial_directories:
197 yield path
199 def __str__(self):
200 return self.project_cvs_repos_path
203 def read_projects(filename):
204 retval = {}
205 for project in cPickle.load(open(filename, 'rb')):
206 retval[project.id] = project
207 return retval
210 def write_projects(filename):
211 cPickle.dump(Ctx()._projects.values(), open(filename, 'wb'), -1)