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 """SymbolStrategy classes determine how to convert symbols."""
21 from cvs2svn_lib
.common
import FatalError
22 from cvs2svn_lib
.common
import path_join
23 from cvs2svn_lib
.common
import normalize_svn_path
24 from cvs2svn_lib
.log
import Log
25 from cvs2svn_lib
.symbol
import Trunk
26 from cvs2svn_lib
.symbol
import TypedSymbol
27 from cvs2svn_lib
.symbol
import Branch
28 from cvs2svn_lib
.symbol
import Tag
29 from cvs2svn_lib
.symbol
import ExcludedSymbol
30 from cvs2svn_lib
.symbol_statistics
import SymbolPlanError
34 """A single rule that might determine how to convert a symbol."""
36 def start(self
, symbol_statistics
):
37 """This method is called once before get_symbol() is ever called.
39 The StrategyRule can override this method to do whatever it wants
40 to prepare itself for work. SYMBOL_STATISTICS is an instance of
41 SymbolStatistics containing the statistics for all symbols in all
46 def get_symbol(self
, symbol
, stats
):
47 """Return an object describing what to do with the symbol in STATS.
49 SYMBOL holds a Trunk or Symbol object as it has been determined so
50 far. Hopefully one of these method calls will turn any naked
51 Symbol instances into TypedSymbols.
53 If this rule applies to the SYMBOL (whose statistics are collected
54 in STATS), then return a new or modified AbstractSymbol object.
55 If this rule doesn't apply, return SYMBOL unchanged."""
57 raise NotImplementedError()
60 """This method is called once after get_symbol() is done being called.
62 The StrategyRule can override this method do whatever it wants to
63 release resources, etc."""
68 class _RegexpStrategyRule(StrategyRule
):
69 """A Strategy rule that bases its decisions on regexp matches.
71 If self.regexp matches a symbol name, return self.action(symbol);
72 otherwise, return the symbol unchanged."""
74 def __init__(self
, pattern
, action
):
75 """Initialize a _RegexpStrategyRule.
77 PATTERN is a string that will be treated as a regexp pattern.
78 PATTERN must match a full symbol name for the rule to apply (i.e.,
79 it is anchored at the beginning and end of the symbol name).
81 ACTION is the class representing how the symbol should be
82 converted. It should be one of the classes Branch, Tag, or
85 If PATTERN matches a symbol name, then get_symbol() returns
86 ACTION(name, id); otherwise it returns SYMBOL unchanged."""
89 self
.regexp
= re
.compile('^' + pattern
+ '$')
91 raise FatalError("%r is not a valid regexp." % (pattern
,))
95 def log(self
, symbol
):
96 raise NotImplementedError()
98 def get_symbol(self
, symbol
, stats
):
99 if isinstance(symbol
, (Trunk
, TypedSymbol
)):
101 elif self
.regexp
.match(symbol
.name
):
103 return self
.action(symbol
)
108 class ForceBranchRegexpStrategyRule(_RegexpStrategyRule
):
109 """Force symbols matching pattern to be branches."""
111 def __init__(self
, pattern
):
112 _RegexpStrategyRule
.__init
__(self
, pattern
, Branch
)
114 def log(self
, symbol
):
116 'Converting symbol %s as a branch because it matches regexp "%s".'
117 % (symbol
, self
.regexp
.pattern
,)
121 class ForceTagRegexpStrategyRule(_RegexpStrategyRule
):
122 """Force symbols matching pattern to be tags."""
124 def __init__(self
, pattern
):
125 _RegexpStrategyRule
.__init
__(self
, pattern
, Tag
)
127 def log(self
, symbol
):
129 'Converting symbol %s as a tag because it matches regexp "%s".'
130 % (symbol
, self
.regexp
.pattern
,)
134 class ExcludeRegexpStrategyRule(_RegexpStrategyRule
):
135 """Exclude symbols matching pattern."""
137 def __init__(self
, pattern
):
138 _RegexpStrategyRule
.__init
__(self
, pattern
, ExcludedSymbol
)
140 def log(self
, symbol
):
142 'Excluding symbol %s because it matches regexp "%s".'
143 % (symbol
, self
.regexp
.pattern
,)
147 class ExcludeTrivialImportBranchRule(StrategyRule
):
148 """If a symbol is a trivial import branch, exclude it.
150 A trivial import branch is defined to be a branch that only had a
151 single import on it (no other kinds of commits) in every file in
152 which it appeared. In most cases these branches are worthless."""
154 def get_symbol(self
, symbol
, stats
):
155 if isinstance(symbol
, (Trunk
, TypedSymbol
)):
157 if stats
.tag_create_count
== 0 \
158 and stats
.branch_create_count
== stats
.trivial_import_count
:
160 'Excluding branch %s because it is a trivial import branch.'
163 return ExcludedSymbol(symbol
)
168 class ExcludeVendorBranchRule(StrategyRule
):
169 """If a symbol is a pure vendor branch, exclude it.
171 A pure vendor branch is defined to be a branch that only had imports
172 on it (no other kinds of commits) in every file in which it
175 def get_symbol(self
, symbol
, stats
):
176 if isinstance(symbol
, (Trunk
, TypedSymbol
)):
178 if stats
.tag_create_count
== 0 \
179 and stats
.branch_create_count
== stats
.pure_ntdb_count
:
181 'Excluding branch %s because it is a pure vendor branch.'
184 return ExcludedSymbol(symbol
)
189 class UnambiguousUsageRule(StrategyRule
):
190 """If a symbol is used unambiguously as a tag/branch, convert it as such."""
192 def get_symbol(self
, symbol
, stats
):
193 if isinstance(symbol
, (Trunk
, TypedSymbol
)):
195 is_tag
= stats
.tag_create_count
> 0
196 is_branch
= stats
.branch_create_count
> 0 or stats
.branch_commit_count
> 0
197 if is_tag
and is_branch
:
202 'Converting symbol %s as a branch because it is always used '
206 return Branch(symbol
)
209 'Converting symbol %s as a tag because it is always used '
215 # The symbol didn't appear at all:
219 class BranchIfCommitsRule(StrategyRule
):
220 """If there was ever a commit on the symbol, convert it as a branch."""
222 def get_symbol(self
, symbol
, stats
):
223 if isinstance(symbol
, (Trunk
, TypedSymbol
)):
225 elif stats
.branch_commit_count
> 0:
227 'Converting symbol %s as a branch because there are commits on it.'
230 return Branch(symbol
)
235 class HeuristicStrategyRule(StrategyRule
):
236 """Convert symbol based on how often it was used as a branch/tag.
238 Whichever happened more often determines how the symbol is
241 def get_symbol(self
, symbol
, stats
):
242 if isinstance(symbol
, (Trunk
, TypedSymbol
)):
244 elif stats
.tag_create_count
>= stats
.branch_create_count
:
246 'Converting symbol %s as a tag because it is more often used '
253 'Converting symbol %s as a branch because it is more often used '
257 return Branch(symbol
)
260 class _CatchAllRule(StrategyRule
):
261 """Base class for catch-all rules.
263 Usually this rule will appear after a list of more careful rules
264 (including a general rule like UnambiguousUsageRule) and will
265 therefore only apply to the symbols not handled earlier."""
267 def __init__(self
, action
):
268 self
._action
= action
270 def log(self
, symbol
):
271 raise NotImplementedError()
273 def get_symbol(self
, symbol
, stats
):
274 if isinstance(symbol
, (Trunk
, TypedSymbol
)):
278 return self
._action
(symbol
)
281 class AllBranchRule(_CatchAllRule
):
282 """Convert all symbols as branches.
284 Usually this rule will appear after a list of more careful rules
285 (including a general rule like UnambiguousUsageRule) and will
286 therefore only apply to the symbols not handled earlier."""
289 _CatchAllRule
.__init
__(self
, Branch
)
291 def log(self
, symbol
):
293 'Converting symbol %s as a branch because no other rules applied.'
298 class AllTagRule(_CatchAllRule
):
299 """Convert all symbols as tags.
301 We don't worry about conflicts here; they will be caught later by
302 SymbolStatistics.check_consistency().
304 Usually this rule will appear after a list of more careful rules
305 (including a general rule like UnambiguousUsageRule) and will
306 therefore only apply to the symbols not handled earlier."""
309 _CatchAllRule
.__init
__(self
, Tag
)
311 def log(self
, symbol
):
313 'Converting symbol %s as a tag because no other rules applied.'
318 class TrunkPathRule(StrategyRule
):
319 """Set the base path for Trunk."""
321 def __init__(self
, trunk_path
):
322 self
.trunk_path
= trunk_path
324 def get_symbol(self
, symbol
, stats
):
325 if isinstance(symbol
, Trunk
) and symbol
.base_path
is None:
326 symbol
.base_path
= self
.trunk_path
331 class SymbolPathRule(StrategyRule
):
332 """Set the base paths for symbol LODs."""
334 def __init__(self
, symbol_type
, base_path
):
335 self
.symbol_type
= symbol_type
336 self
.base_path
= base_path
338 def get_symbol(self
, symbol
, stats
):
339 if isinstance(symbol
, self
.symbol_type
) and symbol
.base_path
is None:
340 symbol
.base_path
= path_join(self
.base_path
, symbol
.name
)
345 class BranchesPathRule(SymbolPathRule
):
346 """Set the base paths for Branch LODs."""
348 def __init__(self
, branch_path
):
349 SymbolPathRule
.__init
__(self
, Branch
, branch_path
)
352 class TagsPathRule(SymbolPathRule
):
353 """Set the base paths for Tag LODs."""
355 def __init__(self
, tag_path
):
356 SymbolPathRule
.__init
__(self
, Tag
, tag_path
)
359 class HeuristicPreferredParentRule(StrategyRule
):
360 """Use a heuristic rule to pick preferred parents.
362 Pick the parent that should be preferred for any TypedSymbols. As
363 parent, use the symbol that appeared most often as a possible parent
364 of the symbol in question. If multiple symbols are tied, choose the
365 one that comes first according to the Symbol class's natural sort
368 def _get_preferred_parent(self
, stats
):
369 """Return the LODs that are most often possible parents in STATS.
371 Return the set of LinesOfDevelopment that appeared most often as
372 possible parents. The return value might contain multiple symbols
373 if multiple LinesOfDevelopment appeared the same number of times."""
377 for (symbol
, count
) in stats
.possible_parents
.items():
378 if count
> best_count
or (count
== best_count
and symbol
< best_symbol
):
382 if best_symbol
is None:
387 def get_symbol(self
, symbol
, stats
):
388 if isinstance(symbol
, TypedSymbol
) and symbol
.preferred_parent_id
is None:
389 preferred_parent
= self
._get
_preferred
_parent
(stats
)
390 if preferred_parent
is None:
391 Log().verbose('%s has no preferred parent' % (symbol
,))
393 symbol
.preferred_parent_id
= preferred_parent
.id
395 'The preferred parent of %s is %s' % (symbol
, preferred_parent
,)
401 class ManualTrunkRule(StrategyRule
):
402 """Change the SVN path of Trunk LODs.
406 project_id -- (int or None) The id of the project whose trunk
407 should be affected by this rule. If project_id is None, then
408 the rule is not project-specific.
410 svn_path -- (str) The SVN path that should be used as the base
411 directory for this trunk. This member must not be None,
412 though it may be the empty string for a single-project,
413 trunk-only conversion.
417 def __init__(self
, project_id
, svn_path
):
418 self
.project_id
= project_id
419 self
.svn_path
= normalize_svn_path(svn_path
, allow_empty
=True)
421 def get_symbol(self
, symbol
, stats
):
422 if (self
.project_id
is not None
423 and self
.project_id
!= stats
.lod
.project
.id):
426 if isinstance(symbol
, Trunk
):
427 symbol
.base_path
= self
.svn_path
432 def convert_as_branch(symbol
):
434 'Converting symbol %s as a branch because of manual setting.'
437 return Branch(symbol
)
440 def convert_as_tag(symbol
):
442 'Converting symbol %s as a tag because of manual setting.'
450 'Excluding symbol %s because of manual setting.'
453 return ExcludedSymbol(symbol
)
456 class ManualSymbolRule(StrategyRule
):
457 """Change how particular symbols are converted.
461 project_id -- (int or None) The id of the project whose trunk
462 should be affected by this rule. If project_id is None, then
463 the rule is not project-specific.
465 symbol_name -- (str) The name of the symbol that should be
466 affected by this rule.
468 conversion -- (callable or None) A callable that converts the
469 symbol to its preferred output type. This should normally be
470 one of (convert_as_branch, convert_as_tag, exclude). If this
471 member is None, then this rule does not affect the symbol's
474 svn_path -- (str) The SVN path that should be used as the base
475 directory for this trunk. This member must not be None,
476 though it may be the empty string for a single-project,
477 trunk-only conversion.
479 parent_lod_name -- (str or None) The name of the line of
480 development that should be preferred as the parent of this
481 symbol. (The preferred parent is the line of development from
482 which the symbol should sprout.) If this member is set to the
483 string '.trunk.', then the symbol will be set to sprout
484 directly from trunk. If this member is set to None, then this
485 rule won't affect the symbol's parent.
490 self
, project_id
, symbol_name
, conversion
, svn_path
, parent_lod_name
492 self
.project_id
= project_id
493 self
.symbol_name
= symbol_name
494 self
.conversion
= conversion
498 self
.svn_path
= normalize_svn_path(svn_path
, allow_empty
=True)
499 self
.parent_lod_name
= parent_lod_name
501 def _get_parent_by_id(self
, parent_lod_name
, stats
):
502 """Return the LOD object for the parent with name PARENT_LOD_NAME.
504 STATS is the _Stats object describing a symbol whose parent needs
505 to be determined from its name. If none of its possible parents
506 has name PARENT_LOD_NAME, raise a SymbolPlanError."""
508 for pp
in stats
.possible_parents
.keys():
509 if isinstance(pp
, Trunk
):
511 elif pp
.name
== parent_lod_name
:
514 parent_counts
= stats
.possible_parents
.items()
515 parent_counts
.sort(lambda a
,b
: - cmp(a
[1], b
[1]))
517 '%s is not a valid parent for %s;'
518 % (parent_lod_name
, stats
.lod
,),
519 ' possible parents (with counts):'
521 for (symbol
, count
) in parent_counts
:
522 if isinstance(symbol
, Trunk
):
523 lines
.append(' .trunk. : %d' % count
)
525 lines
.append(' %s : %d' % (symbol
.name
, count
))
526 raise SymbolPlanError('\n'.join(lines
))
528 def get_symbol(self
, symbol
, stats
):
529 if (self
.project_id
is not None
530 and self
.project_id
!= stats
.lod
.project
.id):
533 elif isinstance(symbol
, Trunk
):
536 elif self
.symbol_name
== stats
.lod
.name
:
537 if self
.conversion
is not None:
538 symbol
= self
.conversion(symbol
)
540 if self
.parent_lod_name
is None:
542 elif self
.parent_lod_name
== '.trunk.':
543 symbol
.preferred_parent_id
= stats
.lod
.project
.trunk_id
545 symbol
.preferred_parent_id
= self
._get
_parent
_by
_id
(
546 self
.parent_lod_name
, stats
549 if self
.svn_path
is not None:
550 symbol
.base_path
= self
.svn_path
555 class SymbolHintsFileRule(StrategyRule
):
556 """Use manual symbol configurations read from a file.
558 The input file is line-oriented with the following format:
560 <project-id> <symbol-name> <conversion> [<svn-path> [<parent-lod-name>]]
562 Where the fields are separated by whitespace and
564 project-id -- the numerical id of the Project to which the
565 symbol belongs (numbered starting with 0). This field can
566 be '.' if the rule is not project-specific.
568 symbol-name -- the name of the symbol being specified, or
569 '.trunk.' if the rule should apply to trunk.
571 conversion -- how the symbol should be treated in the
572 conversion. This is one of the following values: 'branch',
573 'tag', or 'exclude'. This field can be '.' if the rule
574 shouldn't affect how the symbol is treated in the
577 svn-path -- the SVN path that should serve as the root path of
578 this LOD. The path should be expressed as a path relative
579 to the SVN root directory, with or without a leading '/'.
580 This field can be omitted or '.' if the rule shouldn't
581 affect the LOD's SVN path.
583 parent-lod-name -- the name of the LOD that should serve as this
584 symbol's parent. This field can be omitted or '.' if the
585 rule shouldn't affect the symbol's parent, or it can be
586 '.trunk.' to indicate that the symbol should sprout from the
589 comment_re
= re
.compile(r
'^(\#|$)')
592 'branch' : convert_as_branch
,
593 'tag' : convert_as_tag
,
598 def __init__(self
, filename
):
599 self
.filename
= filename
601 def start(self
, symbol_statistics
):
604 f
= open(self
.filename
, 'r')
608 if self
.comment_re
.match(s
):
614 'The following line in "%s" cannot be parsed:\n "%s"'
615 % (self
.filename
, l
,)
618 project_id
= fields
.pop(0)
619 symbol_name
= fields
.pop(0)
620 conversion
= fields
.pop(0)
623 svn_path
= fields
.pop(0)
626 elif svn_path
[0] == '/':
627 svn_path
= svn_path
[1:]
632 parent_lod_name
= fields
.pop(0)
634 parent_lod_name
= '.'
638 'The following line in "%s" cannot be parsed:\n "%s"'
639 % (self
.filename
, l
,)
642 if project_id
== '.':
646 project_id
= int(project_id
)
649 'Illegal project_id in the following line:\n "%s"' % (l
,)
652 if symbol_name
== '.trunk.':
653 if conversion
not in ['.', 'trunk']:
654 raise FatalError('Trunk cannot be converted as a different type')
656 if parent_lod_name
!= '.':
657 raise FatalError('Trunk\'s parent cannot be set')
660 # This rule doesn't do anything:
663 self
._rules
.append(ManualTrunkRule(project_id
, svn_path
))
667 conversion
= self
.conversion_map
[conversion
]
670 'Illegal conversion in the following line:\n "%s"' % (l
,)
673 if parent_lod_name
== '.':
674 parent_lod_name
= None
676 if conversion
is None \
677 and svn_path
is None \
678 and parent_lod_name
is None:
679 # There is nothing to be done:
684 project_id
, symbol_name
,
685 conversion
, svn_path
, parent_lod_name
689 for rule
in self
._rules
:
690 rule
.start(symbol_statistics
)
692 def get_symbol(self
, symbol
, stats
):
693 for rule
in self
._rules
:
694 symbol
= rule
.get_symbol(symbol
, stats
)
699 for rule
in self
._rules
: