~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Martin Pool
  • Date: 2005-09-16 09:19:54 UTC
  • Revision ID: mbp@sourcefrog.net-20050916091954-aee6d7be00db6354
- more docs in commit code

Show diffs side-by-side

added added

removed removed

Lines of Context:
17
17
 
18
18
import sys
19
19
import os
 
20
from cStringIO import StringIO
20
21
 
21
22
import bzrlib
22
23
from bzrlib.trace import mutter, note
24
25
     splitpath, \
25
26
     sha_file, appendpath, file_kind
26
27
 
27
 
from bzrlib.errors import BzrError, InvalidRevisionNumber, InvalidRevisionId
28
 
import bzrlib.errors
 
28
from bzrlib.errors import (BzrError, InvalidRevisionNumber, InvalidRevisionId,
 
29
                           NoSuchRevision, HistoryMissing, NotBranchError,
 
30
                           LockError)
29
31
from bzrlib.textui import show_status
30
 
from bzrlib.revision import Revision
31
 
from bzrlib.xml import unpack_xml
 
32
from bzrlib.revision import Revision, validate_revision_id
32
33
from bzrlib.delta import compare_trees
33
34
from bzrlib.tree import EmptyTree, RevisionTree
 
35
from bzrlib.inventory import Inventory
 
36
from bzrlib.weavestore import WeaveStore
 
37
from bzrlib.store import ImmutableStore
 
38
import bzrlib.xml5
34
39
import bzrlib.ui
35
40
 
36
41
 
37
 
 
38
 
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
 
42
INVENTORY_FILEID = '__inventory'
 
43
ANCESTRY_FILEID = '__ancestry'
 
44
 
 
45
 
 
46
BZR_BRANCH_FORMAT_4 = "Bazaar-NG branch, format 0.0.4\n"
 
47
BZR_BRANCH_FORMAT_5 = "Bazaar-NG branch, format 5\n"
39
48
## TODO: Maybe include checks for common corruption of newlines, etc?
40
49
 
41
50
 
42
51
# TODO: Some operations like log might retrieve the same revisions
43
52
# repeatedly to calculate deltas.  We could perhaps have a weakref
44
 
# cache in memory to make this faster.
 
53
# cache in memory to make this faster.  In general anything can be
 
54
# cached in memory between lock and unlock operations.
45
55
 
46
56
# TODO: please move the revision-string syntax stuff out of the branch
47
57
# object; it's clutter
94
104
        if tail:
95
105
            s.insert(0, tail)
96
106
    else:
97
 
        from errors import NotBranchError
98
107
        raise NotBranchError("path %r is not within branch %r" % (rp, base))
99
108
 
100
109
    return os.sep.join(s)
128
137
        head, tail = os.path.split(f)
129
138
        if head == f:
130
139
            # reached the root, whatever that may be
131
 
            raise bzrlib.errors.NotBranchError('%s is not in a branch' % orig_f)
 
140
            raise NotBranchError('%s is not in a branch' % orig_f)
132
141
        f = head
133
142
 
134
143
 
164
173
    _lock_mode = None
165
174
    _lock_count = None
166
175
    _lock = None
 
176
    _inventory_weave = None
167
177
    
168
178
    # Map some sort of prefix into a namespace
169
179
    # stuff like "revno:10", "revid:", etc.
170
180
    # This should match a prefix with a function which accepts
171
181
    REVISION_NAMESPACES = {}
172
182
 
173
 
    def __init__(self, base, init=False, find_root=True):
 
183
    def __init__(self, base, init=False, find_root=True,
 
184
                 relax_version_check=False):
174
185
        """Create new branch object at a particular location.
175
186
 
176
187
        base -- Base directory for the branch.
182
193
        find_root -- If true and init is false, find the root of the
183
194
             existing branch containing base.
184
195
 
 
196
        relax_version_check -- If true, the usual check for the branch
 
197
            version is not applied.  This is intended only for
 
198
            upgrade/recovery type use; it's not guaranteed that
 
199
            all operations will work on old format branches.
 
200
 
185
201
        In the test suite, creation of new trees is tested using the
186
202
        `ScratchBranch` class.
187
203
        """
188
 
        from bzrlib.store import ImmutableStore
189
204
        if init:
190
205
            self.base = os.path.realpath(base)
191
206
            self._make_control()
194
209
        else:
195
210
            self.base = os.path.realpath(base)
196
211
            if not isdir(self.controlfilename('.')):
197
 
                from errors import NotBranchError
198
 
                raise NotBranchError("not a bzr branch: %s" % quotefn(base),
199
 
                                     ['use "bzr init" to initialize a new working tree',
200
 
                                      'current bzr can only operate from top-of-tree'])
201
 
        self._check_format()
202
 
 
203
 
        self.text_store = ImmutableStore(self.controlfilename('text-store'))
204
 
        self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
205
 
        self.inventory_store = ImmutableStore(self.controlfilename('inventory-store'))
 
212
                raise NotBranchError('not a bzr branch: %s' % quotefn(base),
 
213
                                     ['use "bzr init" to initialize a '
 
214
                                      'new working tree'])
 
215
        
 
216
        self._check_format(relax_version_check)
 
217
        if self._branch_format == 4:
 
218
            self.inventory_store = \
 
219
                ImmutableStore(self.controlfilename('inventory-store'))
 
220
            self.text_store = \
 
221
                ImmutableStore(self.controlfilename('text-store'))
 
222
        self.weave_store = WeaveStore(self.controlfilename('weaves'))
 
223
        self.revision_store = \
 
224
            ImmutableStore(self.controlfilename('revision-store'))
206
225
 
207
226
 
208
227
    def __str__(self):
222
241
    def lock_write(self):
223
242
        if self._lock_mode:
224
243
            if self._lock_mode != 'w':
225
 
                from errors import LockError
226
244
                raise LockError("can't upgrade to a write lock from %r" %
227
245
                                self._lock_mode)
228
246
            self._lock_count += 1
248
266
                        
249
267
    def unlock(self):
250
268
        if not self._lock_mode:
251
 
            from errors import LockError
252
269
            raise LockError('branch %r is not locked' % (self))
253
270
 
254
271
        if self._lock_count > 1:
301
318
            raise BzrError("invalid controlfile mode %r" % mode)
302
319
 
303
320
    def _make_control(self):
304
 
        from bzrlib.inventory import Inventory
305
 
        from bzrlib.xml import pack_xml
306
 
        
307
321
        os.mkdir(self.controlfilename([]))
308
322
        self.controlfile('README', 'w').write(
309
323
            "This is a Bazaar-NG control directory.\n"
310
324
            "Do not change any files in this directory.\n")
311
 
        self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT)
312
 
        for d in ('text-store', 'inventory-store', 'revision-store'):
 
325
        self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT_5)
 
326
        for d in ('text-store', 'revision-store',
 
327
                  'weaves'):
313
328
            os.mkdir(self.controlfilename(d))
314
329
        for f in ('revision-history', 'merged-patches',
315
330
                  'pending-merged-patches', 'branch-name',
321
336
        # if we want per-tree root ids then this is the place to set
322
337
        # them; they're not needed for now and so ommitted for
323
338
        # simplicity.
324
 
        pack_xml(Inventory(), self.controlfile('inventory','w'))
325
 
 
326
 
    def _check_format(self):
 
339
        f = self.controlfile('inventory','w')
 
340
        bzrlib.xml5.serializer_v5.write_inventory(Inventory(), f)
 
341
        
 
342
 
 
343
 
 
344
    def _check_format(self, relax_version_check):
327
345
        """Check this branch format is supported.
328
346
 
329
 
        The current tool only supports the current unstable format.
 
347
        The format level is stored, as an integer, in
 
348
        self._branch_format for code that needs to check it later.
330
349
 
331
350
        In the future, we might need different in-memory Branch
332
351
        classes to support downlevel branches.  But not yet.
333
352
        """
334
 
        # This ignores newlines so that we can open branches created
335
 
        # on Windows from Linux and so on.  I think it might be better
336
 
        # to always make all internal files in unix format.
337
353
        fmt = self.controlfile('branch-format', 'r').read()
338
 
        fmt.replace('\r\n', '')
339
 
        if fmt != BZR_BRANCH_FORMAT:
340
 
            raise BzrError('sorry, branch format %r not supported' % fmt,
341
 
                           ['use a different bzr version',
342
 
                            'or remove the .bzr directory and "bzr init" again'])
 
354
        if fmt == BZR_BRANCH_FORMAT_5:
 
355
            self._branch_format = 5
 
356
        elif fmt == BZR_BRANCH_FORMAT_4:
 
357
            self._branch_format = 4
 
358
 
 
359
        if (not relax_version_check
 
360
            and self._branch_format != 5):
 
361
            raise BzrError('sorry, branch format "%s" not supported; ' 
 
362
                           'use a different bzr version, '
 
363
                           'or run "bzr upgrade"'
 
364
                           % fmt.rstrip('\n\r'))
 
365
        
343
366
 
344
367
    def get_root_id(self):
345
368
        """Return the id of this branches root"""
360
383
 
361
384
    def read_working_inventory(self):
362
385
        """Read the working inventory."""
363
 
        from bzrlib.inventory import Inventory
364
 
        from bzrlib.xml import unpack_xml
365
 
        from time import time
366
 
        before = time()
367
386
        self.lock_read()
368
387
        try:
369
388
            # ElementTree does its own conversion from UTF-8, so open in
370
389
            # binary.
371
 
            inv = unpack_xml(Inventory,
372
 
                             self.controlfile('inventory', 'rb'))
373
 
            mutter("loaded inventory of %d items in %f"
374
 
                   % (len(inv), time() - before))
375
 
            return inv
 
390
            f = self.controlfile('inventory', 'rb')
 
391
            return bzrlib.xml5.serializer_v5.read_inventory(f)
376
392
        finally:
377
393
            self.unlock()
378
394
            
384
400
        will be committed to the next revision.
385
401
        """
386
402
        from bzrlib.atomicfile import AtomicFile
387
 
        from bzrlib.xml import pack_xml
388
403
        
389
404
        self.lock_write()
390
405
        try:
391
406
            f = AtomicFile(self.controlfilename('inventory'), 'wb')
392
407
            try:
393
 
                pack_xml(inv, f)
 
408
                bzrlib.xml5.serializer_v5.write_inventory(inv, f)
394
409
                f.commit()
395
410
            finally:
396
411
                f.close()
581
596
            f.close()
582
597
 
583
598
 
584
 
    def get_revision_xml(self, revision_id):
 
599
    def has_revision(self, revision_id):
 
600
        """True if this branch has a copy of the revision.
 
601
 
 
602
        This does not necessarily imply the revision is merge
 
603
        or on the mainline."""
 
604
        return revision_id in self.revision_store
 
605
 
 
606
 
 
607
    def get_revision_xml_file(self, revision_id):
585
608
        """Return XML file object for revision object."""
586
609
        if not revision_id or not isinstance(revision_id, basestring):
587
610
            raise InvalidRevisionId(revision_id)
596
619
            self.unlock()
597
620
 
598
621
 
 
622
    def get_revision_xml(self, revision_id):
 
623
        return self.get_revision_xml_file(revision_id).read()
 
624
 
 
625
 
599
626
    def get_revision(self, revision_id):
600
627
        """Return the Revision object for a named revision"""
601
 
        xml_file = self.get_revision_xml(revision_id)
 
628
        xml_file = self.get_revision_xml_file(revision_id)
602
629
 
603
630
        try:
604
 
            r = unpack_xml(Revision, xml_file)
 
631
            r = bzrlib.xml5.serializer_v5.read_revision(xml_file)
605
632
        except SyntaxError, e:
606
633
            raise bzrlib.errors.BzrError('failed to unpack revision_xml',
607
634
                                         [revision_id,
636
663
 
637
664
    def get_revision_sha1(self, revision_id):
638
665
        """Hash the stored value of a revision, and return it."""
639
 
        # In the future, revision entries will be signed. At that
640
 
        # point, it is probably best *not* to include the signature
641
 
        # in the revision hash. Because that lets you re-sign
642
 
        # the revision, (add signatures/remove signatures) and still
643
 
        # have all hash pointers stay consistent.
644
 
        # But for now, just hash the contents.
645
 
        return bzrlib.osutils.sha_file(self.get_revision_xml(revision_id))
646
 
 
647
 
 
648
 
    def get_inventory(self, inventory_id):
649
 
        """Get Inventory object by hash.
650
 
 
651
 
        TODO: Perhaps for this and similar methods, take a revision
652
 
               parameter which can be either an integer revno or a
653
 
               string hash."""
654
 
        from bzrlib.inventory import Inventory
655
 
        from bzrlib.xml import unpack_xml
656
 
 
657
 
        return unpack_xml(Inventory, self.get_inventory_xml(inventory_id))
658
 
 
659
 
 
660
 
    def get_inventory_xml(self, inventory_id):
 
666
        return bzrlib.osutils.sha_file(self.get_revision_xml_file(revision_id))
 
667
 
 
668
 
 
669
    def get_ancestry(self, revision_id):
 
670
        """Return a list of revision-ids integrated by a revision.
 
671
        """
 
672
        w = self.weave_store.get_weave(ANCESTRY_FILEID)
 
673
        # strip newlines
 
674
        return [l[:-1] for l in w.get_iter(w.lookup(revision_id))]
 
675
 
 
676
 
 
677
    def get_inventory_weave(self):
 
678
        return self.weave_store.get_weave(INVENTORY_FILEID)
 
679
 
 
680
 
 
681
    def get_inventory(self, revision_id):
 
682
        """Get Inventory object by hash."""
 
683
        # FIXME: The text gets passed around a lot coming from the weave.
 
684
        f = StringIO(self.get_inventory_xml(revision_id))
 
685
        return bzrlib.xml5.serializer_v5.read_inventory(f)
 
686
 
 
687
 
 
688
    def get_inventory_xml(self, revision_id):
661
689
        """Get inventory XML as a file object."""
662
 
        return self.inventory_store[inventory_id]
663
 
            
664
 
 
665
 
    def get_inventory_sha1(self, inventory_id):
 
690
        try:
 
691
            assert isinstance(revision_id, basestring), type(revision_id)
 
692
            iw = self.get_inventory_weave()
 
693
            return iw.get_text(iw.lookup(revision_id))
 
694
        except IndexError:
 
695
            raise bzrlib.errors.HistoryMissing(self, 'inventory', revision_id)
 
696
 
 
697
 
 
698
    def get_inventory_sha1(self, revision_id):
666
699
        """Return the sha1 hash of the inventory entry
667
700
        """
668
 
        return sha_file(self.get_inventory_xml(inventory_id))
 
701
        return self.get_revision(revision_id).inventory_sha1
669
702
 
670
703
 
671
704
    def get_revision_inventory(self, revision_id):
672
705
        """Return inventory of a past revision."""
673
 
        # bzr 0.0.6 imposes the constraint that the inventory_id
 
706
        # bzr 0.0.6 and later imposes the constraint that the inventory_id
674
707
        # must be the same as its revision, so this is trivial.
675
708
        if revision_id == None:
676
 
            from bzrlib.inventory import Inventory
677
709
            return Inventory(self.get_root_id())
678
710
        else:
679
711
            return self.get_inventory(revision_id)
680
712
 
681
713
 
682
714
    def revision_history(self):
683
 
        """Return sequence of revision hashes on to this branch.
684
 
 
685
 
        >>> ScratchBranch().revision_history()
686
 
        []
687
 
        """
 
715
        """Return sequence of revision hashes on to this branch."""
688
716
        self.lock_read()
689
717
        try:
690
718
            return [l.rstrip('\r\n') for l in
699
727
        >>> sb = ScratchBranch(files=['foo', 'foo~'])
700
728
        >>> sb.common_ancestor(sb) == (None, None)
701
729
        True
702
 
        >>> commit.commit(sb, "Committing first revision", verbose=False)
 
730
        >>> commit.commit(sb, "Committing first revision")
703
731
        >>> sb.common_ancestor(sb)[0]
704
732
        1
705
733
        >>> clone = sb.clone()
706
 
        >>> commit.commit(sb, "Committing second revision", verbose=False)
 
734
        >>> commit.commit(sb, "Committing second revision")
707
735
        >>> sb.common_ancestor(sb)[0]
708
736
        2
709
737
        >>> sb.common_ancestor(clone)[0]
710
738
        1
711
 
        >>> commit.commit(clone, "Committing divergent second revision", 
712
 
        ...               verbose=False)
 
739
        >>> commit.commit(clone, "Committing divergent second revision")
713
740
        >>> sb.common_ancestor(clone)[0]
714
741
        1
715
742
        >>> sb.common_ancestor(clone) == clone.common_ancestor(sb)
747
774
        return len(self.revision_history())
748
775
 
749
776
 
750
 
    def last_patch(self):
 
777
    def last_revision(self):
751
778
        """Return last patch hash, or None if no history.
752
779
        """
753
780
        ph = self.revision_history()
758
785
 
759
786
 
760
787
    def missing_revisions(self, other, stop_revision=None, diverged_ok=False):
761
 
        """
 
788
        """Return a list of new revisions that would perfectly fit.
 
789
        
762
790
        If self and other have not diverged, return a list of the revisions
763
791
        present in other, but missing from self.
764
792
 
784
812
        Traceback (most recent call last):
785
813
        DivergedBranches: These branches have diverged.
786
814
        """
 
815
        # FIXME: If the branches have diverged, but the latest
 
816
        # revision in this branch is completely merged into the other,
 
817
        # then we should still be able to pull.
787
818
        self_history = self.revision_history()
788
819
        self_len = len(self_history)
789
820
        other_history = other.revision_history()
795
826
 
796
827
        if stop_revision is None:
797
828
            stop_revision = other_len
798
 
        elif stop_revision > other_len:
799
 
            raise bzrlib.errors.NoSuchRevision(self, stop_revision)
 
829
        else:
 
830
            assert isinstance(stop_revision, int)
 
831
            if stop_revision > other_len:
 
832
                raise bzrlib.errors.NoSuchRevision(self, stop_revision)
800
833
        
801
834
        return other_history[self_len:stop_revision]
802
835
 
803
836
 
804
 
    def update_revisions(self, other, stop_revision=None):
805
 
        """Pull in all new revisions from other branch.
 
837
    def update_revisions(self, other, stop_revno=None):
 
838
        """Pull in new perfect-fit revisions.
806
839
        """
807
840
        from bzrlib.fetch import greedy_fetch
808
841
 
809
 
        pb = bzrlib.ui.ui_factory.progress_bar()
810
 
        pb.update('comparing histories')
811
 
 
812
 
        revision_ids = self.missing_revisions(other, stop_revision)
813
 
 
814
 
        if len(revision_ids) > 0:
815
 
            count = greedy_fetch(self, other, revision_ids[-1], pb)[0]
 
842
        if stop_revno:
 
843
            stop_revision = other.lookup_revision(stop_revno)
816
844
        else:
817
 
            count = 0
818
 
        self.append_revision(*revision_ids)
819
 
        ## note("Added %d revisions." % count)
820
 
        pb.clear()
821
 
 
822
 
    def install_revisions(self, other, revision_ids, pb):
823
 
        if hasattr(other.revision_store, "prefetch"):
824
 
            other.revision_store.prefetch(revision_ids)
825
 
        if hasattr(other.inventory_store, "prefetch"):
826
 
            inventory_ids = [other.get_revision(r).inventory_id
827
 
                             for r in revision_ids]
828
 
            other.inventory_store.prefetch(inventory_ids)
829
 
 
830
 
        if pb is None:
831
 
            pb = bzrlib.ui.ui_factory.progress_bar()
832
 
                
833
 
        revisions = []
834
 
        needed_texts = set()
835
 
        i = 0
836
 
 
837
 
        failures = set()
838
 
        for i, rev_id in enumerate(revision_ids):
839
 
            pb.update('fetching revision', i+1, len(revision_ids))
840
 
            try:
841
 
                rev = other.get_revision(rev_id)
842
 
            except bzrlib.errors.NoSuchRevision:
843
 
                failures.add(rev_id)
844
 
                continue
845
 
 
846
 
            revisions.append(rev)
847
 
            inv = other.get_inventory(str(rev.inventory_id))
848
 
            for key, entry in inv.iter_entries():
849
 
                if entry.text_id is None:
850
 
                    continue
851
 
                if entry.text_id not in self.text_store:
852
 
                    needed_texts.add(entry.text_id)
853
 
 
854
 
        pb.clear()
855
 
                    
856
 
        count, cp_fail = self.text_store.copy_multi(other.text_store, 
857
 
                                                    needed_texts)
858
 
        #print "Added %d texts." % count 
859
 
        inventory_ids = [ f.inventory_id for f in revisions ]
860
 
        count, cp_fail = self.inventory_store.copy_multi(other.inventory_store, 
861
 
                                                         inventory_ids)
862
 
        #print "Added %d inventories." % count 
863
 
        revision_ids = [ f.revision_id for f in revisions]
864
 
 
865
 
        count, cp_fail = self.revision_store.copy_multi(other.revision_store, 
866
 
                                                          revision_ids,
867
 
                                                          permit_failure=True)
868
 
        assert len(cp_fail) == 0 
869
 
        return count, failures
870
 
       
 
845
            stop_revision = None
 
846
        greedy_fetch(to_branch=self, from_branch=other,
 
847
                     revision=stop_revision)
 
848
 
 
849
        pullable_revs = self.missing_revisions(other, stop_revision)
 
850
 
 
851
        if pullable_revs:
 
852
            greedy_fetch(to_branch=self,
 
853
                         from_branch=other,
 
854
                         revision=pullable_revs[-1])
 
855
            self.append_revision(*pullable_revs)
 
856
 
871
857
 
872
858
    def commit(self, *args, **kw):
873
 
        from bzrlib.commit import commit
874
 
        commit(self, *args, **kw)
 
859
        from bzrlib.commit import Commit
 
860
        Commit().commit(self, *args, **kw)
875
861
        
876
862
 
877
863
    def lookup_revision(self, revision):
1089
1075
            return EmptyTree()
1090
1076
        else:
1091
1077
            inv = self.get_revision_inventory(revision_id)
1092
 
            return RevisionTree(self.text_store, inv)
 
1078
            return RevisionTree(self.weave_store, inv, revision_id)
1093
1079
 
1094
1080
 
1095
1081
    def working_tree(self):
1103
1089
 
1104
1090
        If there are no revisions yet, return an `EmptyTree`.
1105
1091
        """
1106
 
        r = self.last_patch()
1107
 
        if r == None:
1108
 
            return EmptyTree()
1109
 
        else:
1110
 
            return RevisionTree(self.text_store, self.get_revision_inventory(r))
1111
 
 
 
1092
        return self.revision_tree(self.last_revision())
1112
1093
 
1113
1094
 
1114
1095
    def rename_one(self, from_rel, to_rel):
1290
1271
 
1291
1272
 
1292
1273
    def add_pending_merge(self, revision_id):
1293
 
        from bzrlib.revision import validate_revision_id
1294
 
 
1295
1274
        validate_revision_id(revision_id)
1296
 
 
 
1275
        # TODO: Perhaps should check at this point that the
 
1276
        # history of the revision is actually present?
1297
1277
        p = self.pending_merges()
1298
1278
        if revision_id in p:
1299
1279
            return
1505
1485
 
1506
1486
    revision
1507
1487
        If not None, only revisions up to this point will be copied.
1508
 
        The head of the new branch will be that revision.
 
1488
        The head of the new branch will be that revision.  Can be a
 
1489
        revno or revid.
1509
1490
 
1510
1491
    to_location
1511
1492
        The name of a local directory that exists but is empty.
1512
1493
    """
 
1494
    # TODO: This could be done *much* more efficiently by just copying
 
1495
    # all the whole weaves and revisions, rather than getting one
 
1496
    # revision at a time.
1513
1497
    from bzrlib.merge import merge
1514
1498
    from bzrlib.branch import Branch
1515
1499
 
1519
1503
    br_to = Branch(to_location, init=True)
1520
1504
    br_to.set_root_id(branch_from.get_root_id())
1521
1505
    if revision is None:
1522
 
        revno = branch_from.revno()
 
1506
        revno = None
1523
1507
    else:
1524
1508
        revno, rev_id = branch_from.get_revision_info(revision)
1525
 
    br_to.update_revisions(branch_from, stop_revision=revno)
 
1509
    br_to.update_revisions(branch_from, stop_revno=revno)
1526
1510
    merge((to_location, -1), (to_location, 0), this_dir=to_location,
1527
1511
          check_clean=False, ignore_zero=True)
1528
1512