aboutsummaryrefslogtreecommitdiff
blob: 86d1ba44c3c27ee8753d11d3885802a929c20ffb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
# (Be in -*- python -*- mode.)
#
# ====================================================================
# Copyright (c) 2000-2009 CollabNet.  All rights reserved.
#
# This software is licensed as described in the file COPYING, which
# you should have received as part of this distribution.  The terms
# are also available at http://subversion.tigris.org/license-1.html.
# If newer versions of this license are posted there, you may use a
# newer version instead, at your option.
#
# This software consists of voluntary contributions made by many
# individuals.  For exact contribution history, see the revision
# history and logs, available at http://cvs2svn.tigris.org/.
# ====================================================================

"""Classes for outputting the converted repository to SVN."""


import os

from cvs2svn_lib import config
from cvs2svn_lib.common import InternalError
from cvs2svn_lib.common import FatalError
from cvs2svn_lib.common import FatalException
from cvs2svn_lib.common import error_prefix
from cvs2svn_lib.common import format_date
from cvs2svn_lib.common import PathsNotDisjointException
from cvs2svn_lib.common import verify_paths_disjoint
from cvs2svn_lib.log import Log
from cvs2svn_lib.context import Ctx
from cvs2svn_lib.artifact_manager import artifact_manager
from cvs2svn_lib.process import CommandFailedException
from cvs2svn_lib.process import check_command_runs
from cvs2svn_lib.process import call_command
from cvs2svn_lib.cvs_file import CVSDirectory
from cvs2svn_lib.symbol import Trunk
from cvs2svn_lib.symbol import LineOfDevelopment
from cvs2svn_lib.cvs_item import CVSRevisionAdd
from cvs2svn_lib.cvs_item import CVSRevisionChange
from cvs2svn_lib.cvs_item import CVSRevisionDelete
from cvs2svn_lib.cvs_item import CVSRevisionNoop
from cvs2svn_lib.repository_mirror import RepositoryMirror
from cvs2svn_lib.repository_mirror import PathExistsError
from cvs2svn_lib.svn_commit_item import SVNCommitItem
from cvs2svn_lib.openings_closings import SymbolingsReader
from cvs2svn_lib.fill_source import get_source_set
from cvs2svn_lib.stdout_delegate import StdoutDelegate
from cvs2svn_lib.dumpfile_delegate import DumpfileDelegate
from cvs2svn_lib.repository_delegate import RepositoryDelegate
from cvs2svn_lib.output_option import OutputOption


class SVNOutputOption(OutputOption):
  """An OutputOption appropriate for output to Subversion."""

  class ParentMissingError(Exception):
    """The parent of a path is missing.

    Exception raised if an attempt is made to add a path to the
    repository mirror but the parent's path doesn't exist in the
    youngest revision of the repository."""

    pass

  class ExpectedDirectoryError(Exception):
    """A file was found where a directory was expected."""

    pass

  def __init__(self, author_transforms=None):
    self._mirror = RepositoryMirror()

    def to_utf8(s):
      if isinstance(s, unicode):
	return s.encode('utf8')
      else:
	return s

    self.author_transforms = {}
    if author_transforms is not None:
      for (cvsauthor, name) in author_transforms.iteritems():
	cvsauthor = to_utf8(cvsauthor)
	name = to_utf8(name)
	self.author_transforms[cvsauthor] = name

  def register_artifacts(self, which_pass):
    # These artifacts are needed for SymbolingsReader:
    artifact_manager.register_temp_file_needed(
        config.SYMBOL_OPENINGS_CLOSINGS_SORTED, which_pass
        )
    artifact_manager.register_temp_file_needed(
        config.SYMBOL_OFFSETS_DB, which_pass
        )

    self._mirror.register_artifacts(which_pass)
    Ctx().revision_reader.register_artifacts(which_pass)

  def check_symbols(self, symbol_map):
    """Check that the paths of all included LODs are set and disjoint."""

    error_found = False

    # Check that all included LODs have their base paths set, and
    # collect the paths into a list:
    paths = []
    for lod in symbol_map.itervalues():
      if isinstance(lod, LineOfDevelopment):
        if lod.base_path is None:
          Log().error('%s: No path was set for %r\n' % (error_prefix, lod,))
          error_found = True
        else:
          paths.append(lod.base_path)

    # Check that the SVN paths of all LODS are disjoint:
    try:
      verify_paths_disjoint(*paths)
    except PathsNotDisjointException, e:
      Log().error(str(e))
      error_found = True

    if error_found:
      raise FatalException(
          'Please fix the above errors and restart CollateSymbolsPass'
          )

  def setup(self, svn_rev_count):
    self._symbolings_reader = SymbolingsReader()
    self._mirror.open()
    self._delegates = []
    Ctx().revision_reader.start()
    self.add_delegate(StdoutDelegate(svn_rev_count))

  def _get_author(self, svn_commit):
    author = svn_commit.get_author()
    name = self.author_transforms.get(author, author)
    return name

  def _get_revprops(self, svn_commit):
    """Return the Subversion revprops for this SVNCommit."""

    return {
        'svn:author' : self._get_author(svn_commit),
        'svn:log'    : svn_commit.get_log_msg(),
        'svn:date'   : format_date(svn_commit.date),
        }

  def start_commit(self, revnum, revprops):
    """Start a new commit."""

    self._mirror.start_commit(revnum)
    self._invoke_delegates('start_commit', revnum, revprops)

  def end_commit(self):
    """Called at the end of each commit.

    This method copies the newly created nodes to the on-disk nodes
    db."""

    self._mirror.end_commit()
    self._invoke_delegates('end_commit')

  def delete_lod(self, lod):
    """Delete the main path for LOD from the tree.

    The path must currently exist.  Silently refuse to delete trunk
    paths."""

    if isinstance(lod, Trunk):
      # Never delete a Trunk path.
      return

    self._mirror.get_current_lod_directory(lod).delete()
    self._invoke_delegates('delete_lod', lod)

  def delete_path(self, cvs_path, lod, should_prune=False):
    """Delete CVS_PATH from LOD."""

    if cvs_path.parent_directory is None:
      self.delete_lod(lod)
      return

    parent_node = self._mirror.get_current_path(
        cvs_path.parent_directory, lod
        )
    del parent_node[cvs_path]
    self._invoke_delegates('delete_path', lod, cvs_path)

    if should_prune:
      while parent_node is not None and len(parent_node) == 0:
        # A drawback of this code is that we issue a delete for each
        # path and not just a single delete for the topmost directory
        # pruned.
        node = parent_node
        cvs_path = node.cvs_path
        if cvs_path.parent_directory is None:
          parent_node = None
          self.delete_lod(lod)
        else:
          parent_node = node.parent_mirror_dir
          node.delete()
          self._invoke_delegates('delete_path', lod, cvs_path)

  def initialize_project(self, project):
    """Create the basic structure for PROJECT."""

    self._invoke_delegates('initialize_project', project)

    # Don't invoke delegates.
    self._mirror.add_lod(project.get_trunk())

  def change_path(self, cvs_rev):
    """Register a change in self._youngest for the CVS_REV's svn_path."""

    # We do not have to update the nodes because our mirror is only
    # concerned with the presence or absence of paths, and a file
    # content change does not cause any path changes.
    self._invoke_delegates('change_path', SVNCommitItem(cvs_rev, False))

  def _mkdir_p(self, cvs_directory, lod):
    """Make sure that CVS_DIRECTORY exists in LOD.

    If not, create it, calling delegates.  Return the node for
    CVS_DIRECTORY."""

    try:
      node = self._mirror.get_current_lod_directory(lod)
    except KeyError:
      node = self._mirror.add_lod(lod)
      self._invoke_delegates('initialize_lod', lod)

    for sub_path in cvs_directory.get_ancestry()[1:]:
      try:
        node = node[sub_path]
      except KeyError:
        node = node.mkdir(sub_path)
        self._invoke_delegates('mkdir', lod, sub_path)
      if node is None:
        raise self.ExpectedDirectoryError(
            'File found at \'%s\' where directory was expected.' % (sub_path,)
            )

    return node

  def add_path(self, cvs_rev):
    """Add the CVS_REV's svn_path to the repository mirror.

    Create any missing intermediate paths."""

    cvs_file = cvs_rev.cvs_file
    parent_path = cvs_file.parent_directory
    lod = cvs_rev.lod
    parent_node = self._mkdir_p(parent_path, lod)
    parent_node.add_file(cvs_file)
    self._invoke_delegates('add_path', SVNCommitItem(cvs_rev, True))

  def copy_lod(self, src_lod, dest_lod, src_revnum):
    """Copy all of SRC_LOD at SRC_REVNUM to DST_LOD.

    In the youngest revision of the repository, the destination LOD
    *must not* already exist.

    Return the new node at DEST_LOD.  Note that this node is not
    necessarily writable, though its parent node necessarily is."""

    node = self._mirror.copy_lod(src_lod, dest_lod, src_revnum)
    self._invoke_delegates('copy_lod', src_lod, dest_lod, src_revnum)
    return node

  def copy_path(
        self, cvs_path, src_lod, dest_lod, src_revnum, create_parent=False
        ):
    """Copy CVS_PATH from SRC_LOD at SRC_REVNUM to DST_LOD.

    In the youngest revision of the repository, the destination's
    parent *must* exist unless CREATE_PARENT is specified.  But the
    destination itself *must not* exist.

    Return the new node at (CVS_PATH, DEST_LOD), as a
    CurrentMirrorDirectory."""

    if cvs_path.parent_directory is None:
      return self.copy_lod(src_lod, dest_lod, src_revnum)

    # Get the node of our source, or None if it is a file:
    src_node = self._mirror.get_old_path(cvs_path, src_lod, src_revnum)

    # Get the parent path of the destination:
    if create_parent:
      dest_parent_node = self._mkdir_p(cvs_path.parent_directory, dest_lod)
    else:
      try:
        dest_parent_node = self._mirror.get_current_path(
            cvs_path.parent_directory, dest_lod
            )
      except KeyError:
        raise self.ParentMissingError(
            'Attempt to add path \'%s\' to repository mirror, '
            'but its parent directory doesn\'t exist in the mirror.'
            % (dest_lod.get_path(cvs_path.cvs_path),)
            )

    if cvs_path in dest_parent_node:
      raise PathExistsError(
          'Attempt to add path \'%s\' to repository mirror '
          'when it already exists in the mirror.'
          % (dest_lod.get_path(cvs_path.cvs_path),)
          )

    dest_parent_node[cvs_path] = src_node
    self._invoke_delegates(
        'copy_path',
        cvs_path, src_lod, dest_lod, src_revnum
        )

    return dest_parent_node[cvs_path]

  def fill_symbol(self, svn_symbol_commit, fill_source):
    """Perform all copies for the CVSSymbols in SVN_SYMBOL_COMMIT.

    The symbolic name is guaranteed to exist in the Subversion
    repository by the end of this call, even if there are no paths
    under it."""

    symbol = svn_symbol_commit.symbol

    try:
      dest_node = self._mirror.get_current_lod_directory(symbol)
    except KeyError:
      self._fill_directory(symbol, None, fill_source, None)
    else:
      self._fill_directory(symbol, dest_node, fill_source, None)

  def _fill_directory(self, symbol, dest_node, fill_source, parent_source):
    """Fill the tag or branch SYMBOL at the path indicated by FILL_SOURCE.

    Use items from FILL_SOURCE, and recurse into the child items.

    Fill SYMBOL starting at the path FILL_SOURCE.cvs_path.  DEST_NODE
    is the node of this destination path, or None if the destination
    does not yet exist.  All directories above this path have already
    been filled.  FILL_SOURCE is a FillSource instance describing the
    items within a subtree of the repository that still need to be
    copied to the destination.

    PARENT_SOURCE is the SVNRevisionRange that was used to copy the
    parent directory, if it was copied in this commit.  We prefer to
    copy from the same source as was used for the parent, since it
    typically requires less touching-up.  If PARENT_SOURCE is None,
    then the parent directory was not copied in this commit, so no
    revision is preferable to any other."""

    copy_source = fill_source.compute_best_source(parent_source)

    # Figure out if we shall copy to this destination and delete any
    # destination path that is in the way.
    if dest_node is None:
      # The destination does not exist at all, so it definitely has to
      # be copied:
      dest_node = self.copy_path(
          fill_source.cvs_path, copy_source.source_lod,
          symbol, copy_source.opening_revnum
          )
    elif (parent_source is not None) and (
          copy_source.source_lod != parent_source.source_lod
          or copy_source.opening_revnum != parent_source.opening_revnum
          ):
      # The parent path was copied from a different source than we
      # need to use, so we have to delete the version that was copied
      # with the parent then re-copy from the correct source:
      self.delete_path(fill_source.cvs_path, symbol)
      dest_node = self.copy_path(
          fill_source.cvs_path, copy_source.source_lod,
          symbol, copy_source.opening_revnum
          )
    else:
      copy_source = parent_source

    # The map {CVSPath : FillSource} of entries within this directory
    # that need filling:
    src_entries = fill_source.get_subsource_map()

    if copy_source is not None:
      self._prune_extra_entries(
          fill_source.cvs_path, symbol, dest_node, src_entries
          )

    return self._cleanup_filled_directory(
        symbol, dest_node, src_entries, copy_source
        )

  def _cleanup_filled_directory(
        self, symbol, dest_node, src_entries, copy_source
        ):
    """The directory at DEST_NODE has been filled and pruned; recurse.

    Recurse into the SRC_ENTRIES, in alphabetical order.  If DEST_NODE
    was copied in this revision, COPY_SOURCE should indicate where it
    was copied from; otherwise, COPY_SOURCE should be None."""

    cvs_paths = src_entries.keys()
    cvs_paths.sort()
    for cvs_path in cvs_paths:
      if isinstance(cvs_path, CVSDirectory):
        # Path is a CVSDirectory:
        try:
          dest_subnode = dest_node[cvs_path]
        except KeyError:
          # Path doesn't exist yet; it has to be created:
          dest_node = self._fill_directory(
              symbol, None, src_entries[cvs_path], None
              ).parent_mirror_dir
        else:
          # Path already exists, but might have to be cleaned up:
          dest_node = self._fill_directory(
              symbol, dest_subnode, src_entries[cvs_path], copy_source
              ).parent_mirror_dir
      else:
        # Path is a CVSFile:
        self._fill_file(
            symbol, cvs_path in dest_node, src_entries[cvs_path], copy_source
            )
        # Reread dest_node since the call to _fill_file() might have
        # made it writable:
        dest_node = self._mirror.get_current_path(
            dest_node.cvs_path, dest_node.lod
            )

    return dest_node

  def _fill_file(self, symbol, dest_existed, fill_source, parent_source):
    """Fill the tag or branch SYMBOL at the path indicated by FILL_SOURCE.

    Use items from FILL_SOURCE.

    Fill SYMBOL at path FILL_SOURCE.cvs_path.  DEST_NODE is the node
    of this destination path, or None if the destination does not yet
    exist.  All directories above this path have already been filled
    as needed.  FILL_SOURCE is a FillSource instance describing the
    item that needs to be copied to the destination.

    PARENT_SOURCE is the source from which the parent directory was
    copied, or None if the parent directory was not copied during this
    commit.  We prefer to copy from PARENT_SOURCE, since it typically
    requires less touching-up.  If PARENT_SOURCE is None, then the
    parent directory was not copied in this commit, so no revision is
    preferable to any other."""

    copy_source = fill_source.compute_best_source(parent_source)

    # Figure out if we shall copy to this destination and delete any
    # destination path that is in the way.
    if not dest_existed:
      # The destination does not exist at all, so it definitely has to
      # be copied:
      self.copy_path(
          fill_source.cvs_path, copy_source.source_lod,
          symbol, copy_source.opening_revnum
          )
    elif (parent_source is not None) and (
          copy_source.source_lod != parent_source.source_lod
          or copy_source.opening_revnum != parent_source.opening_revnum
          ):
      # The parent path was copied from a different source than we
      # need to use, so we have to delete the version that was copied
      # with the parent and then re-copy from the correct source:
      self.delete_path(fill_source.cvs_path, symbol)
      self.copy_path(
          fill_source.cvs_path, copy_source.source_lod,
          symbol, copy_source.opening_revnum
          )

  def _prune_extra_entries(
        self, dest_cvs_path, symbol, dest_node, src_entries
        ):
    """Delete any entries in DEST_NODE that are not in SRC_ENTRIES."""

    delete_list = [
        cvs_path
        for cvs_path in dest_node
        if cvs_path not in src_entries
        ]

    # Sort the delete list so that the output is in a consistent
    # order:
    delete_list.sort()
    for cvs_path in delete_list:
      del dest_node[cvs_path]
      self._invoke_delegates('delete_path', symbol, cvs_path)

  def add_delegate(self, delegate):
    """Adds DELEGATE to self._delegates.

    For every delegate you add, whenever a repository action method is
    performed, delegate's corresponding repository action method is
    called.  Multiple delegates will be called in the order that they
    are added.  See SVNRepositoryDelegate for more information."""

    self._delegates.append(delegate)

  def _invoke_delegates(self, method, *args):
    """Invoke a method on each delegate.

    Iterate through each of our delegates, in the order that they were
    added, and call the delegate's method named METHOD with the
    arguments in ARGS."""

    for delegate in self._delegates:
      getattr(delegate, method)(*args)

  def process_initial_project_commit(self, svn_commit):
    self.start_commit(svn_commit.revnum, self._get_revprops(svn_commit))

    for project in svn_commit.projects:
      self.initialize_project(project)

    self.end_commit()

  def process_primary_commit(self, svn_commit):
    self.start_commit(svn_commit.revnum, self._get_revprops(svn_commit))

    # This actually commits CVSRevisions
    if len(svn_commit.cvs_revs) > 1:
      plural = "s"
    else:
      plural = ""
    Log().verbose("Committing %d CVSRevision%s"
                  % (len(svn_commit.cvs_revs), plural))
    for cvs_rev in svn_commit.cvs_revs:
      if isinstance(cvs_rev, CVSRevisionNoop):
        pass

      elif isinstance(cvs_rev, CVSRevisionDelete):
        self.delete_path(cvs_rev.cvs_file, cvs_rev.lod, Ctx().prune)

      elif isinstance(cvs_rev, CVSRevisionAdd):
        self.add_path(cvs_rev)

      elif isinstance(cvs_rev, CVSRevisionChange):
        self.change_path(cvs_rev)

    self.end_commit()

  def process_post_commit(self, svn_commit):
    self.start_commit(svn_commit.revnum, self._get_revprops(svn_commit))

    Log().verbose(
        'Synchronizing default branch motivated by %d'
        % (svn_commit.motivating_revnum,)
        )

    for cvs_rev in svn_commit.cvs_revs:
      trunk = cvs_rev.cvs_file.project.get_trunk()
      if isinstance(cvs_rev, CVSRevisionAdd):
        # Copy from branch to trunk:
        self.copy_path(
            cvs_rev.cvs_file, cvs_rev.lod, trunk,
            svn_commit.motivating_revnum, True
            )
      elif isinstance(cvs_rev, CVSRevisionChange):
        # Delete old version of the path on trunk...
        self.delete_path(cvs_rev.cvs_file, trunk)
        # ...and copy the new version over from branch:
        self.copy_path(
            cvs_rev.cvs_file, cvs_rev.lod, trunk,
            svn_commit.motivating_revnum, True
            )
      elif isinstance(cvs_rev, CVSRevisionDelete):
        # Delete trunk path:
        self.delete_path(cvs_rev.cvs_file, trunk)
      elif isinstance(cvs_rev, CVSRevisionNoop):
        # Do nothing
        pass
      else:
        raise InternalError('Unexpected CVSRevision type: %s' % (cvs_rev,))

    self.end_commit()

  def process_branch_commit(self, svn_commit):
    self.start_commit(svn_commit.revnum, self._get_revprops(svn_commit))
    Log().verbose('Filling branch:', svn_commit.symbol.name)

    # Get the set of sources for the symbolic name:
    source_set = get_source_set(
        svn_commit.symbol,
        self._symbolings_reader.get_range_map(svn_commit),
        )

    self.fill_symbol(svn_commit, source_set)

    self.end_commit()

  def process_tag_commit(self, svn_commit):
    self.start_commit(svn_commit.revnum, self._get_revprops(svn_commit))
    Log().verbose('Filling tag:', svn_commit.symbol.name)

    # Get the set of sources for the symbolic name:
    source_set = get_source_set(
        svn_commit.symbol,
        self._symbolings_reader.get_range_map(svn_commit),
        )

    self.fill_symbol(svn_commit, source_set)

    self.end_commit()

  def cleanup(self):
    self._invoke_delegates('finish')
    self._mirror.close()
    self._mirror = None
    Ctx().revision_reader.finish()
    self._symbolings_reader.close()
    del self._symbolings_reader


class DumpfileOutputOption(SVNOutputOption):
  """Output the result of the conversion into a dumpfile."""

  def __init__(self, dumpfile_path, author_transforms=None):
    SVNOutputOption.__init__(self, author_transforms)
    self.dumpfile_path = dumpfile_path

  def check(self):
    pass

  def setup(self, svn_rev_count):
    Log().quiet("Starting Subversion Dumpfile.")
    SVNOutputOption.setup(self, svn_rev_count)
    if not Ctx().dry_run:
      self.add_delegate(
          DumpfileDelegate(Ctx().revision_reader, self.dumpfile_path)
          )


class RepositoryOutputOption(SVNOutputOption):
  """Output the result of the conversion into an SVN repository."""

  def __init__(self, target, author_transforms=None):
    SVNOutputOption.__init__(self, author_transforms)
    self.target = target

  def check(self):
    if not Ctx().dry_run:
      # Verify that svnadmin can be executed.  The 'help' subcommand
      # should be harmless.
      try:
        check_command_runs([Ctx().svnadmin_executable, 'help'], 'svnadmin')
      except CommandFailedException, e:
        raise FatalError(
            '%s\n'
            'svnadmin could not be executed.  Please ensure that it is\n'
            'installed and/or use the --svnadmin option.' % (e,))

  def setup(self, svn_rev_count):
    Log().quiet("Starting Subversion Repository.")
    SVNOutputOption.setup(self, svn_rev_count)
    if not Ctx().dry_run:
      self.add_delegate(
          RepositoryDelegate(Ctx().revision_reader, self.target)
          )


class NewRepositoryOutputOption(RepositoryOutputOption):
  """Output the result of the conversion into a new SVN repository."""

  def __init__(
        self, target, fs_type=None, bdb_txn_nosync=None, author_transforms=None, create_options=[]
        ):
    RepositoryOutputOption.__init__(self, target, author_transforms)
    self.bdb_txn_nosync = bdb_txn_nosync

    # Determine the options to be passed to "svnadmin create":
    if not fs_type:
      # User didn't say what kind repository (bdb, fsfs, etc).  We
      # still pass --bdb-txn-nosync.  It's a no-op if the default
      # repository type doesn't support it, but we definitely want it
      # if BDB is the default.
      self.create_options = ['--bdb-txn-nosync']
    elif fs_type == 'bdb':
      # User explicitly specified bdb.
      #
      # Since this is a BDB repository, pass --bdb-txn-nosync, because
      # it gives us a 4-5x speed boost (if cvs2svn is creating the
      # repository, cvs2svn should be the only program accessing the
      # svn repository until cvs2svn is done).  But we'll turn no-sync
      # off in self.finish(), unless instructed otherwise.
      self.create_options = ['--fs-type=bdb', '--bdb-txn-nosync']
    else:
      # User specified something other than bdb.
      self.create_options = ['--fs-type=%s' % fs_type]

    # Now append the user's explicitly-set create options:
    self.create_options += create_options

  def check(self):
    RepositoryOutputOption.check(self)
    if not Ctx().dry_run and os.path.exists(self.target):
      raise FatalError("the svn-repos-path '%s' exists.\n"
                       "Remove it, or pass '--existing-svnrepos'."
                       % self.target)

  def setup(self, svn_rev_count):
    Log().normal("Creating new repository '%s'" % (self.target))
    if Ctx().dry_run:
      # Do not actually create repository:
      pass
    else:
      call_command([
          Ctx().svnadmin_executable, 'create',
          ] + self.create_options + [
          self.target
          ])

    RepositoryOutputOption.setup(self, svn_rev_count)

  def cleanup(self):
    RepositoryOutputOption.cleanup(self)

    # If this is a BDB repository, and we created the repository, and
    # --bdb-no-sync wasn't passed, then comment out the DB_TXN_NOSYNC
    # line in the DB_CONFIG file, because txn syncing should be on by
    # default in BDB repositories.
    #
    # We determine if this is a BDB repository by looking for the
    # DB_CONFIG file, which doesn't exist in FSFS, rather than by
    # checking self.fs_type.  That way this code will Do The Right
    # Thing in all circumstances.
    db_config = os.path.join(self.target, "db/DB_CONFIG")
    if Ctx().dry_run:
      # Do not change repository:
      pass
    elif not self.bdb_txn_nosync and os.path.exists(db_config):
      no_sync = 'set_flags DB_TXN_NOSYNC\n'

      contents = open(db_config, 'r').readlines()
      index = contents.index(no_sync)
      contents[index] = '# ' + no_sync
      open(db_config, 'w').writelines(contents)


class ExistingRepositoryOutputOption(RepositoryOutputOption):
  """Output the result of the conversion into an existing SVN repository."""

  def __init__(self, target, author_transforms=None):
    RepositoryOutputOption.__init__(self, target, author_transforms)

  def check(self):
    RepositoryOutputOption.check(self)
    if not os.path.isdir(self.target):
      raise FatalError("the svn-repos-path '%s' is not an "
                       "existing directory." % self.target)