* MANIFEST.in: Remove cvs2svn.1. Add cvs2*-example.options.
[cvs2svn.git] / cvs2svn_lib / project.py
blob0fe92df52ad63d4ed86c6da9c44241329c8ad8f2
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 re
21 import os
22 import cPickle
24 from cvs2svn_lib.context import Ctx
25 from cvs2svn_lib.common import FatalError
26 from cvs2svn_lib.common import IllegalSVNPathError
27 from cvs2svn_lib.common import normalize_svn_path
28 from cvs2svn_lib.common import verify_paths_disjoint
29 from cvs2svn_lib.symbol_transform import CompoundSymbolTransform
32 class FileInAndOutOfAtticException(Exception):
33 def __init__(self, non_attic_path, attic_path):
34 Exception.__init__(
35 self,
36 "A CVS repository cannot contain both %s and %s"
37 % (non_attic_path, attic_path))
39 self.non_attic_path = non_attic_path
40 self.attic_path = attic_path
43 def normalize_ttb_path(opt, path, allow_empty=False):
44 try:
45 return normalize_svn_path(path, allow_empty)
46 except IllegalSVNPathError, e:
47 raise FatalError('Problem with %s: %s' % (opt, e,))
50 class Project(object):
51 """A project within a CVS repository."""
53 def __init__(
54 self, id, project_cvs_repos_path,
55 initial_directories=[],
56 symbol_transforms=None,
58 """Create a new Project record.
60 ID is a unique id for this project. PROJECT_CVS_REPOS_PATH is the
61 main CVS directory for this project (within the filesystem).
63 INITIAL_DIRECTORIES is an iterable of all SVN directories that
64 should be created when the project is first created. Normally,
65 this should include the trunk, branches, and tags directory.
67 SYMBOL_TRANSFORMS is an iterable of SymbolTransform instances
68 which will be used to transform any symbol names within this
69 project."""
71 self.id = id
73 self.project_cvs_repos_path = os.path.normpath(project_cvs_repos_path)
74 if not os.path.isdir(self.project_cvs_repos_path):
75 raise FatalError("The specified CVS repository path '%s' is not an "
76 "existing directory." % self.project_cvs_repos_path)
78 self.cvs_repository_root, self.cvs_module = \
79 self.determine_repository_root(
80 os.path.abspath(self.project_cvs_repos_path))
82 # A regexp matching project_cvs_repos_path plus an optional separator:
83 self.project_prefix_re = re.compile(
84 r'^' + re.escape(self.project_cvs_repos_path)
85 + r'(' + re.escape(os.sep) + r'|$)')
87 # The SVN directories to add when the project is first created:
88 self._initial_directories = []
90 for path in initial_directories:
91 try:
92 path = normalize_svn_path(path, False)
93 except IllegalSVNPathError, e:
94 raise FatalError(
95 'Initial directory %r is not a legal SVN path: %s'
96 % (path, e,)
98 self._initial_directories.append(path)
100 verify_paths_disjoint(*self._initial_directories)
102 # A list of transformation rules (regexp, replacement) applied to
103 # symbol names in this project.
104 if symbol_transforms is None:
105 symbol_transforms = []
107 self.symbol_transform = CompoundSymbolTransform(symbol_transforms)
109 # The ID of the Trunk instance for this Project. This member is
110 # filled in during CollectRevsPass.
111 self.trunk_id = None
113 # The ID of the CVSDirectory representing the root directory of
114 # this project. This member is filled in during CollectRevsPass.
115 self.root_cvs_directory_id = None
117 def __eq__(self, other):
118 return self.id == other.id
120 def __cmp__(self, other):
121 return cmp(self.cvs_module, other.cvs_module) \
122 or cmp(self.id, other.id)
124 def __hash__(self):
125 return self.id
127 @staticmethod
128 def determine_repository_root(path):
129 """Ascend above the specified PATH if necessary to find the
130 cvs_repository_root (a directory containing a CVSROOT directory)
131 and the cvs_module (the path of the conversion root within the cvs
132 repository). Return the root path and the module path of this
133 project relative to the root.
135 NB: cvs_module must be seperated by '/', *not* by os.sep."""
137 def is_cvs_repository_root(path):
138 return os.path.isdir(os.path.join(path, 'CVSROOT'))
140 original_path = path
141 cvs_module = ''
142 while not is_cvs_repository_root(path):
143 # Step up one directory:
144 prev_path = path
145 path, module_component = os.path.split(path)
146 if path == prev_path:
147 # Hit the root (of the drive, on Windows) without finding a
148 # CVSROOT dir.
149 raise FatalError(
150 "the path '%s' is not a CVS repository, nor a path "
151 "within a CVS repository. A CVS repository contains "
152 "a CVSROOT directory within its root directory."
153 % (original_path,))
155 cvs_module = module_component + "/" + cvs_module
157 return path, cvs_module
159 def transform_symbol(self, cvs_file, symbol_name, revision):
160 """Transform the symbol SYMBOL_NAME.
162 SYMBOL_NAME refers to revision number REVISION in CVS_FILE.
163 REVISION is the CVS revision number as a string, with zeros
164 removed (e.g., '1.7' or '1.7.2'). Use the renaming rules
165 specified with --symbol-transform to possibly rename the symbol.
166 Return the transformed symbol name, the original name if it should
167 not be transformed, or None if the symbol should be omitted from
168 the conversion."""
170 return self.symbol_transform.transform(cvs_file, symbol_name, revision)
172 def get_trunk(self):
173 """Return the Trunk instance for this project.
175 This method can only be called after self.trunk_id has been
176 initialized in CollectRevsPass."""
178 return Ctx()._symbol_db.get_symbol(self.trunk_id)
180 def get_root_cvs_directory(self):
181 """Return the root CVSDirectory instance for this project.
183 This method can only be called after self.root_cvs_directory_id
184 has been initialized in CollectRevsPass."""
186 return Ctx()._cvs_file_db.get_file(self.root_cvs_directory_id)
188 def get_initial_directories(self):
189 """Generate the project's initial SVN directories.
191 Yield as strings the SVN paths of directories that should be
192 created when the project is first created."""
194 # Yield the path of the Trunk symbol for this project (which might
195 # differ from the one passed to the --trunk option because of
196 # SymbolStrategyRules). The trunk path might be '' during a
197 # trunk-only conversion, but that is OK because DumpfileDelegate
198 # considers that directory to exist already and will therefore
199 # ignore it:
200 yield self.get_trunk().base_path
202 for path in self._initial_directories:
203 yield path
205 def __str__(self):
206 return self.project_cvs_repos_path
209 def read_projects(filename):
210 retval = {}
211 for project in cPickle.load(open(filename, 'rb')):
212 retval[project.id] = project
213 return retval
216 def write_projects(filename):
217 cPickle.dump(Ctx()._projects.values(), open(filename, 'wb'), -1)