Move code for analyzing fixup commits to DVCSOutputOption.
[cvs2svn.git] / cvs2svn_lib / dvcs_common.py
blob84143d3c98c3f0bb2a786efff2cd7cf00777b1e1
1 # (Be in -*- python -*- mode.)
3 # ====================================================================
4 # Copyright (c) 2007-2009 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 """Miscellaneous utility code common to DVCS backends (like
18 Git, Mercurial, or Bazaar).
19 """
21 import sys
23 from cvs2svn_lib import config
24 from cvs2svn_lib.common import FatalError
25 from cvs2svn_lib.common import InternalError
26 from cvs2svn_lib.run_options import RunOptions
27 from cvs2svn_lib.log import Log
28 from cvs2svn_lib.context import Ctx
29 from cvs2svn_lib.artifact_manager import artifact_manager
30 from cvs2svn_lib.project import Project
31 from cvs2svn_lib.svn_revision_range import RevisionScores
32 from cvs2svn_lib.openings_closings import SymbolingsReader
33 from cvs2svn_lib.repository_mirror import RepositoryMirror
34 from cvs2svn_lib.output_option import OutputOption
37 class DVCSRunOptions(RunOptions):
38 """Dumping ground for whatever is common to GitRunOptions and
39 HgRunOptions."""
40 def __init__(self, progname, cmd_args, pass_manager):
41 Ctx().cross_project_commits = False
42 Ctx().cross_branch_commits = False
43 RunOptions.__init__(self, progname, cmd_args, pass_manager)
45 def set_project(
46 self,
47 project_cvs_repos_path,
48 symbol_transforms=None,
49 symbol_strategy_rules=[],
51 """Set the project to be converted.
53 If a project had already been set, overwrite it.
55 Most arguments are passed straight through to the Project
56 constructor. SYMBOL_STRATEGY_RULES is an iterable of
57 SymbolStrategyRules that will be applied to symbols in this
58 project."""
60 symbol_strategy_rules = list(symbol_strategy_rules)
62 project = Project(
64 project_cvs_repos_path,
65 symbol_transforms=symbol_transforms,
68 self.projects = [project]
69 self.project_symbol_strategy_rules = [symbol_strategy_rules]
71 def process_options(self):
72 # Consistency check for options and arguments.
73 if len(self.args) == 0:
74 self.usage()
75 sys.exit(1)
77 if len(self.args) > 1:
78 Log().error(error_prefix + ": must pass only one CVS repository.\n")
79 self.usage()
80 sys.exit(1)
82 cvsroot = self.args[0]
84 self.process_extraction_options()
85 self.process_output_options()
86 self.process_symbol_strategy_options()
87 self.process_property_setter_options()
89 # Create the project:
90 self.set_project(
91 cvsroot,
92 symbol_transforms=self.options.symbol_transforms,
93 symbol_strategy_rules=self.options.symbol_strategy_rules,
97 class DVCSOutputOption(OutputOption):
98 # name of output format (for error messages); must be set by
99 # subclasses
100 name = None
102 def __init__(self):
103 self._mirror = RepositoryMirror()
104 self._symbolings_reader = None
106 def normalize_author_transforms(self, author_transforms):
107 """Return a new dict with the same content as author_transforms, but all
108 strings encoded to UTF-8. Also turns None into the empty dict."""
109 result = {}
110 if author_transforms is not None:
111 for (cvsauthor, (name, email,)) in author_transforms.iteritems():
112 cvsauthor = to_utf8(cvsauthor)
113 name = to_utf8(name)
114 email = to_utf8(email)
115 result[cvsauthor] = (name, email,)
116 return result
118 def register_artifacts(self, which_pass):
119 # These artifacts are needed for SymbolingsReader:
120 artifact_manager.register_temp_file_needed(
121 config.SYMBOL_OPENINGS_CLOSINGS_SORTED, which_pass
123 artifact_manager.register_temp_file_needed(
124 config.SYMBOL_OFFSETS_DB, which_pass
126 self._mirror.register_artifacts(which_pass)
128 def check(self):
129 if Ctx().cross_project_commits:
130 raise FatalError(
131 '%s output is not supported with cross-project commits' % self.name
133 if Ctx().cross_branch_commits:
134 raise FatalError(
135 '%s output is not supported with cross-branch commits' % self.name
137 if Ctx().username is None:
138 raise FatalError(
139 '%s output requires a default commit username' % self.name
142 def setup(self, svn_rev_count):
143 self._symbolings_reader = SymbolingsReader()
144 self._mirror.open()
146 def cleanup(self):
147 self._mirror.close()
148 self._symbolings_reader.close()
149 del self._symbolings_reader
151 def _get_source_groups(self, svn_commit):
152 """Return groups of sources for SVN_COMMIT.
154 SVN_COMMIT is an instance of SVNSymbolCommit. Yield tuples
155 (source_lod, svn_revnum, cvs_symbols) where source_lod is the line
156 of development and svn_revnum is the revision that should serve as
157 a source, and cvs_symbols is a list of CVSSymbolItems that can be
158 copied from that source. The groups are returned in arbitrary
159 order."""
161 # Get a map {CVSSymbol : SVNRevisionRange}:
162 range_map = self._symbolings_reader.get_range_map(svn_commit)
164 # range_map, split up into one map per LOD; i.e., {LOD :
165 # {CVSSymbol : SVNRevisionRange}}:
166 lod_range_maps = {}
168 for (cvs_symbol, range) in range_map.iteritems():
169 lod_range_map = lod_range_maps.get(range.source_lod)
170 if lod_range_map is None:
171 lod_range_map = {}
172 lod_range_maps[range.source_lod] = lod_range_map
173 lod_range_map[cvs_symbol] = range
175 # Sort the sources so that the branch that serves most often as
176 # parent is processed first:
177 lod_ranges = lod_range_maps.items()
178 lod_ranges.sort(
179 lambda (lod1,lod_range_map1),(lod2,lod_range_map2):
180 -cmp(len(lod_range_map1), len(lod_range_map2)) or cmp(lod1, lod2)
183 for (lod, lod_range_map) in lod_ranges:
184 while lod_range_map:
185 revision_scores = RevisionScores(lod_range_map.values())
186 (source_lod, revnum, score) = revision_scores.get_best_revnum()
187 assert source_lod == lod
188 cvs_symbols = []
189 for (cvs_symbol, range) in lod_range_map.items():
190 if revnum in range:
191 cvs_symbols.append(cvs_symbol)
192 del lod_range_map[cvs_symbol]
193 yield (lod, revnum, cvs_symbols)
195 def _is_simple_copy(self, svn_commit, source_groups):
196 """Return True iff SVN_COMMIT can be created as a simple copy.
198 SVN_COMMIT is an SVNTagCommit. Return True iff it can be created
199 as a simple copy from an existing revision (i.e., if the fixup
200 branch can be avoided for this tag creation)."""
202 # The first requirement is that there be exactly one source:
203 if len(source_groups) != 1:
204 return False
206 (source_lod, svn_revnum, cvs_symbols) = source_groups[0]
208 # The second requirement is that the destination LOD not already
209 # exist:
210 try:
211 self._mirror.get_current_lod_directory(svn_commit.symbol)
212 except KeyError:
213 # The LOD doesn't already exist. This is good.
214 pass
215 else:
216 # The LOD already exists. It cannot be created by a copy.
217 return False
219 # The third requirement is that the source LOD contains exactly
220 # the same files as we need to add to the symbol:
221 try:
222 source_node = self._mirror.get_old_lod_directory(source_lod, svn_revnum)
223 except KeyError:
224 raise InternalError('Source %r does not exist' % (source_lod,))
225 return (
226 set([cvs_symbol.cvs_file for cvs_symbol in cvs_symbols])
227 == set(self._get_all_files(source_node))
230 def _get_all_files(self, node):
231 """Generate all of the CVSFiles under NODE."""
233 for cvs_path in node:
234 subnode = node[cvs_path]
235 if subnode is None:
236 yield cvs_path
237 else:
238 for sub_cvs_path in self._get_all_files(subnode):
239 yield sub_cvs_path
242 def to_utf8(s):
243 if isinstance(s, unicode):
244 return s.encode('utf8')
245 else:
246 return s