~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/repository.py

  • Committer: Martin Pool
  • Date: 2006-06-20 07:55:43 UTC
  • mfrom: (1798 +trunk)
  • mto: This revision was merged to the branch mainline in revision 1799.
  • Revision ID: mbp@sourcefrog.net-20060620075543-b10f6575d4a4fa32
[merge] bzr.dev

Show diffs side-by-side

added added

removed removed

Lines of Context:
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
 
17
from binascii import hexlify
17
18
from copy import deepcopy
18
19
from cStringIO import StringIO
 
20
import re
 
21
import time
19
22
from unittest import TestSuite
20
23
 
21
 
import bzrlib.bzrdir as bzrdir
 
24
from bzrlib import bzrdir, check, delta, gpg, errors, xml5, ui, transactions, osutils
22
25
from bzrlib.decorators import needs_read_lock, needs_write_lock
23
 
import bzrlib.errors as errors
24
26
from bzrlib.errors import InvalidRevisionId
25
 
import bzrlib.gpg as gpg
26
27
from bzrlib.graph import Graph
27
28
from bzrlib.inter import InterObject
 
29
from bzrlib.inventory import Inventory
28
30
from bzrlib.knit import KnitVersionedFile, KnitPlainFactory
29
31
from bzrlib.lockable_files import LockableFiles, TransportLock
30
32
from bzrlib.lockdir import LockDir
31
 
from bzrlib.osutils import safe_unicode
32
 
from bzrlib.revision import NULL_REVISION
 
33
from bzrlib.osutils import (safe_unicode, rand_bytes, compact_date, 
 
34
                            local_time_offset)
 
35
from bzrlib.revision import NULL_REVISION, Revision
33
36
from bzrlib.store.versioned import VersionedFileStore, WeaveStore
34
37
from bzrlib.store.text import TextStore
35
 
from bzrlib.symbol_versioning import *
 
38
from bzrlib.symbol_versioning import (deprecated_method,
 
39
        zero_nine, 
 
40
        )
36
41
from bzrlib.trace import mutter, note
37
 
from bzrlib.tree import RevisionTree
 
42
from bzrlib.tree import RevisionTree, EmptyTree
38
43
from bzrlib.tsort import topo_sort
39
44
from bzrlib.testament import Testament
40
45
from bzrlib.tree import EmptyTree
41
 
import bzrlib.ui
42
46
from bzrlib.weave import WeaveFile
43
 
import bzrlib.xml5
44
47
 
45
48
 
46
49
class Repository(object):
64
67
 
65
68
        returns the sha1 of the serialized inventory.
66
69
        """
67
 
        inv_text = bzrlib.xml5.serializer_v5.write_inventory_to_string(inv)
68
 
        inv_sha1 = bzrlib.osutils.sha_string(inv_text)
 
70
        assert inv.revision_id is None or inv.revision_id == revid, \
 
71
            "Mismatch between inventory revision" \
 
72
            " id and insertion revid (%r, %r)" % (inv.revision_id, revid)
 
73
        inv_text = xml5.serializer_v5.write_inventory_to_string(inv)
 
74
        inv_sha1 = osutils.sha_string(inv_text)
69
75
        inv_vf = self.control_weaves.get_weave('inventory',
70
76
                                               self.get_transaction())
71
 
        inv_vf.add_lines(revid, parents, bzrlib.osutils.split_lines(inv_text))
 
77
        self._inventory_add_lines(inv_vf, revid, parents, osutils.split_lines(inv_text))
72
78
        return inv_sha1
73
79
 
 
80
    def _inventory_add_lines(self, inv_vf, revid, parents, lines):
 
81
        final_parents = []
 
82
        for parent in parents:
 
83
            if parent in inv_vf:
 
84
                final_parents.append(parent)
 
85
 
 
86
        inv_vf.add_lines(revid, final_parents, lines)
 
87
 
74
88
    @needs_write_lock
75
89
    def add_revision(self, rev_id, rev, inv=None, config=None):
76
90
        """Add rev to the revision store as rev_id.
103
117
        """Return all the possible revisions that we could find."""
104
118
        return self.get_inventory_weave().versions()
105
119
 
 
120
    def all_revision_ids(self):
 
121
        """Returns a list of all the revision ids in the repository. 
 
122
 
 
123
        This is deprecated because code should generally work on the graph
 
124
        reachable from a particular revision, and ignore any other revisions
 
125
        that might be present.  There is no direct replacement method.
 
126
        """
 
127
        return self._all_revision_ids()
 
128
 
106
129
    @needs_read_lock
107
 
    def all_revision_ids(self):
 
130
    def _all_revision_ids(self):
108
131
        """Returns a list of all the revision ids in the repository. 
109
132
 
110
133
        These are in as much topological order as the underlying store can 
158
181
        self.control_files = control_files
159
182
        self._revision_store = _revision_store
160
183
        self.text_store = text_store
161
 
        # backwards compatability
 
184
        # backwards compatibility
162
185
        self.weave_store = text_store
163
186
        # not right yet - should be more semantically clear ? 
164
187
        # 
200
223
        For instance, if the repository is at URL/.bzr/repository,
201
224
        Repository.open(URL) -> a Repository instance.
202
225
        """
203
 
        control = bzrlib.bzrdir.BzrDir.open(base)
 
226
        control = bzrdir.BzrDir.open(base)
204
227
        return control.open_repository()
205
228
 
206
229
    def copy_content_into(self, destination, revision_id=None, basis=None):
219
242
        return InterRepository.get(source, self).fetch(revision_id=revision_id,
220
243
                                                       pb=pb)
221
244
 
 
245
    def get_commit_builder(self, branch, parents, config, timestamp=None, 
 
246
                           timezone=None, committer=None, revprops=None, 
 
247
                           revision_id=None):
 
248
        """Obtain a CommitBuilder for this repository.
 
249
        
 
250
        :param branch: Branch to commit to.
 
251
        :param parents: Revision ids of the parents of the new revision.
 
252
        :param config: Configuration to use.
 
253
        :param timestamp: Optional timestamp recorded for commit.
 
254
        :param timezone: Optional timezone for timestamp.
 
255
        :param committer: Optional committer to set for commit.
 
256
        :param revprops: Optional dictionary of revision properties.
 
257
        :param revision_id: Optional revision id.
 
258
        """
 
259
        return CommitBuilder(self, parents, config, timestamp, timezone,
 
260
                             committer, revprops, revision_id)
 
261
 
222
262
    def unlock(self):
223
263
        self.control_files.unlock()
224
264
 
234
274
            result = a_bzrdir.create_repository()
235
275
        # FIXME RBC 20060209 split out the repository type to avoid this check ?
236
276
        elif isinstance(a_bzrdir._format,
237
 
                      (bzrlib.bzrdir.BzrDirFormat4,
238
 
                       bzrlib.bzrdir.BzrDirFormat5,
239
 
                       bzrlib.bzrdir.BzrDirFormat6)):
 
277
                      (bzrdir.BzrDirFormat4,
 
278
                       bzrdir.BzrDirFormat5,
 
279
                       bzrdir.BzrDirFormat6)):
240
280
            result = a_bzrdir.open_repository()
241
281
        else:
242
282
            result = self._format.initialize(a_bzrdir, shared=self.is_shared())
260
300
        """
261
301
        if not revision_id or not isinstance(revision_id, basestring):
262
302
            raise InvalidRevisionId(revision_id=revision_id, branch=self)
263
 
        return self._revision_store.get_revision(revision_id,
264
 
                                                 self.get_transaction())
 
303
        return self._revision_store.get_revisions([revision_id],
 
304
                                                  self.get_transaction())[0]
 
305
    @needs_read_lock
 
306
    def get_revisions(self, revision_ids):
 
307
        return self._revision_store.get_revisions(revision_ids,
 
308
                                                  self.get_transaction())
265
309
 
266
310
    @needs_read_lock
267
311
    def get_revision_xml(self, revision_id):
287
331
        self._check_revision_parents(r, inv)
288
332
        return r
289
333
 
 
334
    def get_revision_delta(self, revision_id):
 
335
        """Return the delta for one revision.
 
336
 
 
337
        The delta is relative to the left-hand predecessor of the
 
338
        revision.
 
339
        """
 
340
        revision = self.get_revision(revision_id)
 
341
        new_tree = self.revision_tree(revision_id)
 
342
        if not revision.parent_ids:
 
343
            old_tree = EmptyTree()
 
344
        else:
 
345
            old_tree = self.revision_tree(revision.parent_ids[0])
 
346
        return delta.compare_trees(old_tree, new_tree)
 
347
 
290
348
    def _check_revision_parents(self, revision, inventory):
291
349
        """Private to Repository and Fetch.
292
350
        
322
380
                                         RepositoryFormat6,
323
381
                                         RepositoryFormat7,
324
382
                                         RepositoryFormatKnit1)), \
325
 
            "fileid_involved only supported for branches which store inventory as unnested xml"
 
383
            ("fileids_altered_by_revision_ids only supported for branches " 
 
384
             "which store inventory as unnested xml, not on %r" % self)
326
385
        selected_revision_ids = set(revision_ids)
327
386
        w = self.get_inventory_weave()
328
387
        result = {}
329
388
 
330
389
        # this code needs to read every new line in every inventory for the
331
390
        # inventories [revision_ids]. Seeing a line twice is ok. Seeing a line
332
 
        # not pesent in one of those inventories is unnecessary but not 
 
391
        # not present in one of those inventories is unnecessary but not 
333
392
        # harmful because we are filtering by the revision id marker in the
334
393
        # inventory lines : we only select file ids altered in one of those  
335
 
        # revisions. We dont need to see all lines in the inventory because
 
394
        # revisions. We don't need to see all lines in the inventory because
336
395
        # only those added in an inventory in rev X can contain a revision=X
337
396
        # line.
338
397
        for line in w.iter_lines_added_or_present_in_versions(selected_revision_ids):
359
418
    @needs_read_lock
360
419
    def get_inventory(self, revision_id):
361
420
        """Get Inventory object by hash."""
362
 
        xml = self.get_inventory_xml(revision_id)
363
 
        return bzrlib.xml5.serializer_v5.read_inventory_from_string(xml)
 
421
        return self.deserialise_inventory(
 
422
            revision_id, self.get_inventory_xml(revision_id))
 
423
 
 
424
    def deserialise_inventory(self, revision_id, xml):
 
425
        """Transform the xml into an inventory object. 
 
426
 
 
427
        :param revision_id: The expected revision id of the inventory.
 
428
        :param xml: A serialised inventory.
 
429
        """
 
430
        return xml5.serializer_v5.read_inventory_from_string(xml)
364
431
 
365
432
    @needs_read_lock
366
433
    def get_inventory_xml(self, revision_id):
370
437
            iw = self.get_inventory_weave()
371
438
            return iw.get_text(revision_id)
372
439
        except IndexError:
373
 
            raise bzrlib.errors.HistoryMissing(self, 'inventory', revision_id)
 
440
            raise errors.HistoryMissing(self, 'inventory', revision_id)
374
441
 
375
442
    @needs_read_lock
376
443
    def get_inventory_sha1(self, revision_id):
485
552
    @needs_read_lock
486
553
    def get_ancestry(self, revision_id):
487
554
        """Return a list of revision-ids integrated by a revision.
 
555
 
 
556
        The first element of the list is always None, indicating the origin 
 
557
        revision.  This might change when we have history horizons, or 
 
558
        perhaps we should have a new API.
488
559
        
489
560
        This is topologically sorted.
490
561
        """
508
579
        # use inventory as it was in that revision
509
580
        file_id = tree.inventory.path2id(file)
510
581
        if not file_id:
511
 
            raise BzrError("%r is not present in revision %s" % (file, revno))
512
 
            try:
513
 
                revno = self.revision_id_to_revno(revision_id)
514
 
            except errors.NoSuchRevision:
515
 
                # TODO: This should not be BzrError,
516
 
                # but NoSuchFile doesn't fit either
517
 
                raise BzrError('%r is not present in revision %s' 
518
 
                                % (file, revision_id))
519
 
            else:
520
 
                raise BzrError('%r is not present in revision %s'
521
 
                                % (file, revno))
 
582
            # TODO: jam 20060427 Write a test for this code path
 
583
            #       it had a bug in it, and was raising the wrong
 
584
            #       exception.
 
585
            raise errors.BzrError("%r is not present in revision %s" % (file, revision_id))
522
586
        tree.print_file(file_id)
523
587
 
524
588
    def get_transaction(self):
560
624
        return self._revision_store.get_signature_text(revision_id,
561
625
                                                       self.get_transaction())
562
626
 
 
627
    @needs_read_lock
 
628
    def check(self, revision_ids):
 
629
        """Check consistency of all history of given revision_ids.
 
630
 
 
631
        Different repository implementations should override _check().
 
632
 
 
633
        :param revision_ids: A non-empty list of revision_ids whose ancestry
 
634
             will be checked.  Typically the last revision_id of a branch.
 
635
        """
 
636
        if not revision_ids:
 
637
            raise ValueError("revision_ids must be non-empty in %s.check" 
 
638
                    % (self,))
 
639
        return self._check(revision_ids)
 
640
 
 
641
    def _check(self, revision_ids):
 
642
        result = check.Check(self)
 
643
        result.check()
 
644
        return result
 
645
 
563
646
 
564
647
class AllInOneRepository(Repository):
565
648
    """Legacy support - the repository behaviour for all-in-one branches."""
635
718
                repository.get_transaction())
636
719
        if ie.revision not in w:
637
720
            text_parents = []
 
721
            # FIXME: TODO: The following loop *may* be overlapping/duplicate
 
722
            # with InventoryEntry.find_previous_heads(). if it is, then there
 
723
            # is a latent bug here where the parents may have ancestors of each
 
724
            # other. RBC, AB
638
725
            for revision, tree in parent_trees.iteritems():
639
726
                if ie.file_id not in tree:
640
727
                    continue
700
787
class KnitRepository(MetaDirRepository):
701
788
    """Knit format repository."""
702
789
 
 
790
    def _inventory_add_lines(self, inv_vf, revid, parents, lines):
 
791
        inv_vf.add_lines_with_ghosts(revid, parents, lines)
 
792
 
703
793
    @needs_read_lock
704
 
    def all_revision_ids(self):
 
794
    def _all_revision_ids(self):
705
795
        """See Repository.all_revision_ids()."""
 
796
        # Knits get the revision graph from the index of the revision knit, so
 
797
        # it's always possible even if they're on an unlistable transport.
706
798
        return self._revision_store.all_revision_ids(self.get_transaction())
707
799
 
708
800
    def fileid_involved_between_revs(self, from_revid, to_revid):
797
889
                    raise errors.NoSuchRevision(self, revision_id)
798
890
                # a ghost
799
891
                result.add_ghost(revision_id)
800
 
                # mark it as done so we dont try for it again.
 
892
                # mark it as done so we don't try for it again.
801
893
                done.add(revision_id)
802
894
                continue
803
895
            parent_ids = vf.get_parents_with_ghosts(revision_id)
824
916
        reconciler.reconcile()
825
917
        return reconciler
826
918
    
827
 
    def revision_parents(self, revid):
828
 
        return self._get_revision_vf().get_parents(rev_id)
 
919
    def revision_parents(self, revision_id):
 
920
        return self._get_revision_vf().get_parents(revision_id)
 
921
 
829
922
 
830
923
class RepositoryFormat(object):
831
924
    """A repository format.
867
960
        except errors.NoSuchFile:
868
961
            raise errors.NoRepositoryPresent(a_bzrdir)
869
962
        except KeyError:
870
 
            raise errors.UnknownFormatError(format_string)
 
963
            raise errors.UnknownFormatError(format=format_string)
871
964
 
872
965
    def _get_control_store(self, repo_transport, control_files):
873
966
        """Return the control store for this repository."""
887
980
        raise NotImplementedError(self.get_format_string)
888
981
 
889
982
    def get_format_description(self):
890
 
        """Return the short desciption for this format."""
 
983
        """Return the short description for this format."""
891
984
        raise NotImplementedError(self.get_format_description)
892
985
 
893
986
    def _get_revision_store(self, repo_transport, control_files):
994
1087
        
995
1088
        # Create an empty weave
996
1089
        sio = StringIO()
997
 
        bzrlib.weavefile.write_weave_v5(Weave(), sio)
 
1090
        write_weave_v5(Weave(), sio)
998
1091
        empty_weave = sio.getvalue()
999
1092
 
1000
1093
        mutter('creating repository in %s.', a_bzrdir.transport.base)
1002
1095
        files = [('inventory.weave', StringIO(empty_weave)),
1003
1096
                 ]
1004
1097
        
1005
 
        # FIXME: RBC 20060125 dont peek under the covers
 
1098
        # FIXME: RBC 20060125 don't peek under the covers
1006
1099
        # NB: no need to escape relative paths that are url safe.
1007
1100
        control_files = LockableFiles(a_bzrdir.transport, 'branch-lock',
1008
1101
                                      TransportLock)
1054
1147
     - TextStores for texts, inventories,revisions.
1055
1148
 
1056
1149
    This format is deprecated: it indexes texts using a text id which is
1057
 
    removed in format 5; initializationa and write support for this format
 
1150
    removed in format 5; initialization and write support for this format
1058
1151
    has been removed.
1059
1152
    """
1060
1153
 
1061
1154
    def __init__(self):
1062
1155
        super(RepositoryFormat4, self).__init__()
1063
 
        self._matchingbzrdir = bzrlib.bzrdir.BzrDirFormat4()
 
1156
        self._matchingbzrdir = bzrdir.BzrDirFormat4()
1064
1157
 
1065
1158
    def get_format_description(self):
1066
1159
        """See RepositoryFormat.get_format_description()."""
1109
1202
 
1110
1203
    def __init__(self):
1111
1204
        super(RepositoryFormat5, self).__init__()
1112
 
        self._matchingbzrdir = bzrlib.bzrdir.BzrDirFormat5()
 
1205
        self._matchingbzrdir = bzrdir.BzrDirFormat5()
1113
1206
 
1114
1207
    def get_format_description(self):
1115
1208
        """See RepositoryFormat.get_format_description()."""
1139
1232
 
1140
1233
    def __init__(self):
1141
1234
        super(RepositoryFormat6, self).__init__()
1142
 
        self._matchingbzrdir = bzrlib.bzrdir.BzrDirFormat6()
 
1235
        self._matchingbzrdir = bzrdir.BzrDirFormat6()
1143
1236
 
1144
1237
    def get_format_description(self):
1145
1238
        """See RepositoryFormat.get_format_description()."""
1159
1252
 
1160
1253
 
1161
1254
class MetaDirRepositoryFormat(RepositoryFormat):
1162
 
    """Common base class for the new repositories using the metadir layour."""
 
1255
    """Common base class for the new repositories using the metadir layout."""
1163
1256
 
1164
1257
    def __init__(self):
1165
1258
        super(MetaDirRepositoryFormat, self).__init__()
1166
 
        self._matchingbzrdir = bzrlib.bzrdir.BzrDirMetaFormat1()
 
1259
        self._matchingbzrdir = bzrdir.BzrDirMetaFormat1()
1167
1260
 
1168
1261
    def _create_control_files(self, a_bzrdir):
1169
1262
        """Create the required files and the initial control_files object."""
1170
 
        # FIXME: RBC 20060125 dont peek under the covers
 
1263
        # FIXME: RBC 20060125 don't peek under the covers
1171
1264
        # NB: no need to escape relative paths that are url safe.
1172
1265
        repository_transport = a_bzrdir.get_repository_transport(self)
1173
1266
        control_files = LockableFiles(repository_transport, 'lock', LockDir)
1244
1337
 
1245
1338
        # Create an empty weave
1246
1339
        sio = StringIO()
1247
 
        bzrlib.weavefile.write_weave_v5(Weave(), sio)
 
1340
        write_weave_v5(Weave(), sio)
1248
1341
        empty_weave = sio.getvalue()
1249
1342
 
1250
1343
        mutter('creating repository in %s.', a_bzrdir.transport.base)
1355
1448
        repo_transport = a_bzrdir.get_repository_transport(None)
1356
1449
        control_files = LockableFiles(repo_transport, 'lock', LockDir)
1357
1450
        control_store = self._get_control_store(repo_transport, control_files)
1358
 
        transaction = bzrlib.transactions.WriteTransaction()
 
1451
        transaction = transactions.WriteTransaction()
1359
1452
        # trigger a write of the inventory store.
1360
1453
        control_store.get_weave_or_empty('inventory', transaction)
1361
1454
        _revision_store = self._get_revision_store(repo_transport, control_files)
1433
1526
        # grab the basis available data
1434
1527
        if basis is not None:
1435
1528
            self.target.fetch(basis, revision_id=revision_id)
1436
 
        # but dont bother fetching if we have the needed data now.
 
1529
        # but don't bother fetching if we have the needed data now.
1437
1530
        if (revision_id not in (None, NULL_REVISION) and 
1438
1531
            self.target.has_revision(revision_id)):
1439
1532
            return
1530
1623
    def is_compatible(source, target):
1531
1624
        """Be compatible with known Weave formats.
1532
1625
        
1533
 
        We dont test for the stores being of specific types becase that
 
1626
        We don't test for the stores being of specific types because that
1534
1627
        could lead to confusing results, and there is no need to be 
1535
1628
        overly general.
1536
1629
        """
1551
1644
        if basis is not None:
1552
1645
            # copy the basis in, then fetch remaining data.
1553
1646
            basis.copy_content_into(self.target, revision_id)
1554
 
            # the basis copy_content_into could misset this.
 
1647
            # the basis copy_content_into could miss-set this.
1555
1648
            try:
1556
1649
                self.target.set_make_working_trees(self.source.make_working_trees())
1557
1650
            except NotImplementedError:
1564
1657
                pass
1565
1658
            # FIXME do not peek!
1566
1659
            if self.source.control_files._transport.listable():
1567
 
                pb = bzrlib.ui.ui_factory.nested_progress_bar()
 
1660
                pb = ui.ui_factory.nested_progress_bar()
1568
1661
                try:
1569
1662
                    self.target.weave_store.copy_all_ids(
1570
1663
                        self.source.weave_store,
1600
1693
    def missing_revision_ids(self, revision_id=None):
1601
1694
        """See InterRepository.missing_revision_ids()."""
1602
1695
        # we want all revisions to satisfy revision_id in source.
1603
 
        # but we dont want to stat every file here and there.
 
1696
        # but we don't want to stat every file here and there.
1604
1697
        # we want then, all revisions other needs to satisfy revision_id 
1605
1698
        # checked, but not those that we have locally.
1606
1699
        # so the first thing is to get a subset of the revisions to 
1619
1712
        source_ids_set = set(source_ids)
1620
1713
        # source_ids is the worst possible case we may need to pull.
1621
1714
        # now we want to filter source_ids against what we actually
1622
 
        # have in target, but dont try to check for existence where we know
 
1715
        # have in target, but don't try to check for existence where we know
1623
1716
        # we do not have a revision as that would be pointless.
1624
1717
        target_ids = set(self.target._all_possible_ids())
1625
1718
        possibly_present_revisions = target_ids.intersection(source_ids_set)
1648
1741
    def is_compatible(source, target):
1649
1742
        """Be compatible with known Knit formats.
1650
1743
        
1651
 
        We dont test for the stores being of specific types becase that
 
1744
        We don't test for the stores being of specific types because that
1652
1745
        could lead to confusing results, and there is no need to be 
1653
1746
        overly general.
1654
1747
        """
1682
1775
        source_ids_set = set(source_ids)
1683
1776
        # source_ids is the worst possible case we may need to pull.
1684
1777
        # now we want to filter source_ids against what we actually
1685
 
        # have in target, but dont try to check for existence where we know
 
1778
        # have in target, but don't try to check for existence where we know
1686
1779
        # we do not have a revision as that would be pointless.
1687
1780
        target_ids = set(self.target._all_possible_ids())
1688
1781
        possibly_present_revisions = target_ids.intersection(source_ids_set)
1836
1929
        self.pb.update(message, self.count, self.total)
1837
1930
 
1838
1931
 
 
1932
class CommitBuilder(object):
 
1933
    """Provides an interface to build up a commit.
 
1934
 
 
1935
    This allows describing a tree to be committed without needing to 
 
1936
    know the internals of the format of the repository.
 
1937
    """
 
1938
    def __init__(self, repository, parents, config, timestamp=None, 
 
1939
                 timezone=None, committer=None, revprops=None, 
 
1940
                 revision_id=None):
 
1941
        """Initiate a CommitBuilder.
 
1942
 
 
1943
        :param repository: Repository to commit to.
 
1944
        :param parents: Revision ids of the parents of the new revision.
 
1945
        :param config: Configuration to use.
 
1946
        :param timestamp: Optional timestamp recorded for commit.
 
1947
        :param timezone: Optional timezone for timestamp.
 
1948
        :param committer: Optional committer to set for commit.
 
1949
        :param revprops: Optional dictionary of revision properties.
 
1950
        :param revision_id: Optional revision id.
 
1951
        """
 
1952
        self._config = config
 
1953
 
 
1954
        if committer is None:
 
1955
            self._committer = self._config.username()
 
1956
        else:
 
1957
            assert isinstance(committer, basestring), type(committer)
 
1958
            self._committer = committer
 
1959
 
 
1960
        self.new_inventory = Inventory()
 
1961
        self._new_revision_id = revision_id
 
1962
        self.parents = parents
 
1963
        self.repository = repository
 
1964
 
 
1965
        self._revprops = {}
 
1966
        if revprops is not None:
 
1967
            self._revprops.update(revprops)
 
1968
 
 
1969
        if timestamp is None:
 
1970
            self._timestamp = time.time()
 
1971
        else:
 
1972
            self._timestamp = long(timestamp)
 
1973
 
 
1974
        if timezone is None:
 
1975
            self._timezone = local_time_offset()
 
1976
        else:
 
1977
            self._timezone = int(timezone)
 
1978
 
 
1979
        self._generate_revision_if_needed()
 
1980
 
 
1981
    def commit(self, message):
 
1982
        """Make the actual commit.
 
1983
 
 
1984
        :return: The revision id of the recorded revision.
 
1985
        """
 
1986
        rev = Revision(timestamp=self._timestamp,
 
1987
                       timezone=self._timezone,
 
1988
                       committer=self._committer,
 
1989
                       message=message,
 
1990
                       inventory_sha1=self.inv_sha1,
 
1991
                       revision_id=self._new_revision_id,
 
1992
                       properties=self._revprops)
 
1993
        rev.parent_ids = self.parents
 
1994
        self.repository.add_revision(self._new_revision_id, rev, 
 
1995
            self.new_inventory, self._config)
 
1996
        return self._new_revision_id
 
1997
 
 
1998
    def finish_inventory(self):
 
1999
        """Tell the builder that the inventory is finished."""
 
2000
        self.new_inventory.revision_id = self._new_revision_id
 
2001
        self.inv_sha1 = self.repository.add_inventory(
 
2002
            self._new_revision_id,
 
2003
            self.new_inventory,
 
2004
            self.parents
 
2005
            )
 
2006
 
 
2007
    def _gen_revision_id(self):
 
2008
        """Return new revision-id."""
 
2009
        s = '%s-%s-' % (self._config.user_email(), 
 
2010
                        compact_date(self._timestamp))
 
2011
        s += hexlify(rand_bytes(8))
 
2012
        return s
 
2013
 
 
2014
    def _generate_revision_if_needed(self):
 
2015
        """Create a revision id if None was supplied.
 
2016
        
 
2017
        If the repository can not support user-specified revision ids
 
2018
        they should override this function and raise UnsupportedOperation
 
2019
        if _new_revision_id is not None.
 
2020
 
 
2021
        :raises: UnsupportedOperation
 
2022
        """
 
2023
        if self._new_revision_id is None:
 
2024
            self._new_revision_id = self._gen_revision_id()
 
2025
 
 
2026
    def record_entry_contents(self, ie, parent_invs, path, tree):
 
2027
        """Record the content of ie from tree into the commit if needed.
 
2028
 
 
2029
        :param ie: An inventory entry present in the commit.
 
2030
        :param parent_invs: The inventories of the parent revisions of the
 
2031
            commit.
 
2032
        :param path: The path the entry is at in the tree.
 
2033
        :param tree: The tree which contains this entry and should be used to 
 
2034
        obtain content.
 
2035
        """
 
2036
        self.new_inventory.add(ie)
 
2037
 
 
2038
        # ie.revision is always None if the InventoryEntry is considered
 
2039
        # for committing. ie.snapshot will record the correct revision 
 
2040
        # which may be the sole parent if it is untouched.
 
2041
        if ie.revision is not None:
 
2042
            return
 
2043
        previous_entries = ie.find_previous_heads(
 
2044
            parent_invs,
 
2045
            self.repository.weave_store,
 
2046
            self.repository.get_transaction())
 
2047
        # we are creating a new revision for ie in the history store
 
2048
        # and inventory.
 
2049
        ie.snapshot(self._new_revision_id, path, previous_entries, tree, self)
 
2050
 
 
2051
    def modified_directory(self, file_id, file_parents):
 
2052
        """Record the presence of a symbolic link.
 
2053
 
 
2054
        :param file_id: The file_id of the link to record.
 
2055
        :param file_parents: The per-file parent revision ids.
 
2056
        """
 
2057
        self._add_text_to_weave(file_id, [], file_parents.keys())
 
2058
    
 
2059
    def modified_file_text(self, file_id, file_parents,
 
2060
                           get_content_byte_lines, text_sha1=None,
 
2061
                           text_size=None):
 
2062
        """Record the text of file file_id
 
2063
 
 
2064
        :param file_id: The file_id of the file to record the text of.
 
2065
        :param file_parents: The per-file parent revision ids.
 
2066
        :param get_content_byte_lines: A callable which will return the byte
 
2067
            lines for the file.
 
2068
        :param text_sha1: Optional SHA1 of the file contents.
 
2069
        :param text_size: Optional size of the file contents.
 
2070
        """
 
2071
        mutter('storing text of file {%s} in revision {%s} into %r',
 
2072
               file_id, self._new_revision_id, self.repository.weave_store)
 
2073
        # special case to avoid diffing on renames or 
 
2074
        # reparenting
 
2075
        if (len(file_parents) == 1
 
2076
            and text_sha1 == file_parents.values()[0].text_sha1
 
2077
            and text_size == file_parents.values()[0].text_size):
 
2078
            previous_ie = file_parents.values()[0]
 
2079
            versionedfile = self.repository.weave_store.get_weave(file_id, 
 
2080
                self.repository.get_transaction())
 
2081
            versionedfile.clone_text(self._new_revision_id, 
 
2082
                previous_ie.revision, file_parents.keys())
 
2083
            return text_sha1, text_size
 
2084
        else:
 
2085
            new_lines = get_content_byte_lines()
 
2086
            # TODO: Rather than invoking sha_strings here, _add_text_to_weave
 
2087
            # should return the SHA1 and size
 
2088
            self._add_text_to_weave(file_id, new_lines, file_parents.keys())
 
2089
            return osutils.sha_strings(new_lines), \
 
2090
                sum(map(len, new_lines))
 
2091
 
 
2092
    def modified_link(self, file_id, file_parents, link_target):
 
2093
        """Record the presence of a symbolic link.
 
2094
 
 
2095
        :param file_id: The file_id of the link to record.
 
2096
        :param file_parents: The per-file parent revision ids.
 
2097
        :param link_target: Target location of this link.
 
2098
        """
 
2099
        self._add_text_to_weave(file_id, [], file_parents.keys())
 
2100
 
 
2101
    def _add_text_to_weave(self, file_id, new_lines, parents):
 
2102
        versionedfile = self.repository.weave_store.get_weave_or_empty(
 
2103
            file_id, self.repository.get_transaction())
 
2104
        versionedfile.add_lines(self._new_revision_id, parents, new_lines)
 
2105
        versionedfile.clear_cache()
 
2106
 
 
2107
 
1839
2108
# Copied from xml.sax.saxutils
1840
2109
def _unescape_xml(data):
1841
2110
    """Unescape &, <, and > in a string of data.