~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Martin Pool
  • Date: 2005-07-12 01:44:23 UTC
  • Revision ID: mbp@sourcefrog.net-20050712014423-1d95eb47ce7ab510
- add simple test case for bzr status

- show_status takes to_file argument

Show diffs side-by-side

added added

removed removed

Lines of Context:
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
17
17
 
18
 
import sys
19
 
import os
 
18
import sys, os
20
19
 
21
20
import bzrlib
22
21
from bzrlib.trace import mutter, note
23
 
from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, \
24
 
     splitpath, \
 
22
from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, splitpath, \
25
23
     sha_file, appendpath, file_kind
26
 
 
27
 
from bzrlib.errors import BzrError, InvalidRevisionNumber, InvalidRevisionId
28
 
import bzrlib.errors
29
 
from bzrlib.textui import show_status
30
 
from bzrlib.revision import Revision
31
 
from bzrlib.xml import unpack_xml
32
 
from bzrlib.delta import compare_trees
33
 
from bzrlib.tree import EmptyTree, RevisionTree
34
 
import bzrlib.ui
35
 
 
36
 
 
 
24
from bzrlib.errors import BzrError
37
25
 
38
26
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
39
27
## TODO: Maybe include checks for common corruption of newlines, etc?
40
28
 
41
29
 
42
 
# TODO: Some operations like log might retrieve the same revisions
43
 
# repeatedly to calculate deltas.  We could perhaps have a weakref
44
 
# cache in memory to make this faster.
45
 
 
46
 
# TODO: please move the revision-string syntax stuff out of the branch
47
 
# object; it's clutter
48
 
 
49
30
 
50
31
def find_branch(f, **args):
51
32
    if f and (f.startswith('http://') or f.startswith('https://')):
108
89
    It is not necessary that f exists.
109
90
 
110
91
    Basically we keep looking up until we find the control directory or
111
 
    run into the root.  If there isn't one, raises NotBranchError.
112
 
    """
 
92
    run into the root."""
113
93
    if f == None:
114
94
        f = os.getcwd()
115
95
    elif hasattr(os.path, 'realpath'):
128
108
        head, tail = os.path.split(f)
129
109
        if head == f:
130
110
            # reached the root, whatever that may be
131
 
            raise bzrlib.errors.NotBranchError('%s is not in a branch' % orig_f)
 
111
            raise BzrError('%r is not in a branch' % orig_f)
132
112
        f = head
133
 
 
134
 
 
135
 
 
136
 
# XXX: move into bzrlib.errors; subclass BzrError    
 
113
    
137
114
class DivergedBranches(Exception):
138
115
    def __init__(self, branch1, branch2):
139
116
        self.branch1 = branch1
141
118
        Exception.__init__(self, "These branches have diverged.")
142
119
 
143
120
 
 
121
class NoSuchRevision(BzrError):
 
122
    def __init__(self, branch, revision):
 
123
        self.branch = branch
 
124
        self.revision = revision
 
125
        msg = "Branch %s has no revision %d" % (branch, revision)
 
126
        BzrError.__init__(self, msg)
 
127
 
 
128
 
144
129
######################################################################
145
130
# branch objects
146
131
 
219
204
            self._lock.unlock()
220
205
 
221
206
 
 
207
 
222
208
    def lock_write(self):
223
209
        if self._lock_mode:
224
210
            if self._lock_mode != 'w':
234
220
            self._lock_count = 1
235
221
 
236
222
 
 
223
 
237
224
    def lock_read(self):
238
225
        if self._lock_mode:
239
226
            assert self._lock_mode in ('r', 'w'), \
246
233
            self._lock_mode = 'r'
247
234
            self._lock_count = 1
248
235
                        
 
236
 
 
237
            
249
238
    def unlock(self):
250
239
        if not self._lock_mode:
251
240
            from errors import LockError
258
247
            self._lock = None
259
248
            self._lock_mode = self._lock_count = None
260
249
 
 
250
 
261
251
    def abspath(self, name):
262
252
        """Return absolute filename for something in the branch"""
263
253
        return os.path.join(self.base, name)
264
254
 
 
255
 
265
256
    def relpath(self, path):
266
257
        """Return path relative to this branch of something inside it.
267
258
 
268
259
        Raises an error if path is not in this branch."""
269
260
        return _relpath(self.base, path)
270
261
 
 
262
 
271
263
    def controlfilename(self, file_or_path):
272
264
        """Return location relative to branch."""
273
265
        if isinstance(file_or_path, basestring):
300
292
        else:
301
293
            raise BzrError("invalid controlfile mode %r" % mode)
302
294
 
 
295
 
 
296
 
303
297
    def _make_control(self):
304
298
        from bzrlib.inventory import Inventory
305
299
        from bzrlib.xml import pack_xml
318
312
            self.controlfile(f, 'w').write('')
319
313
        mutter('created control directory in ' + self.base)
320
314
 
321
 
        # if we want per-tree root ids then this is the place to set
322
 
        # them; they're not needed for now and so ommitted for
323
 
        # simplicity.
324
 
        pack_xml(Inventory(), self.controlfile('inventory','w'))
 
315
        pack_xml(Inventory(gen_root_id()), self.controlfile('inventory','w'))
 
316
 
325
317
 
326
318
    def _check_format(self):
327
319
        """Check this branch format is supported.
369
361
            # ElementTree does its own conversion from UTF-8, so open in
370
362
            # binary.
371
363
            inv = unpack_xml(Inventory,
372
 
                             self.controlfile('inventory', 'rb'))
 
364
                                  self.controlfile('inventory', 'rb'))
373
365
            mutter("loaded inventory of %d items in %f"
374
366
                   % (len(inv), time() - before))
375
367
            return inv
404
396
                         """Inventory for the working copy.""")
405
397
 
406
398
 
407
 
    def add(self, files, ids=None):
 
399
    def add(self, files, verbose=False, ids=None):
408
400
        """Make files versioned.
409
401
 
410
 
        Note that the command line normally calls smart_add instead,
411
 
        which can automatically recurse.
 
402
        Note that the command line normally calls smart_add instead.
412
403
 
413
404
        This puts the files in the Added state, so that they will be
414
405
        recorded by the next commit.
424
415
        TODO: Perhaps have an option to add the ids even if the files do
425
416
              not (yet) exist.
426
417
 
427
 
        TODO: Perhaps yield the ids and paths as they're added.
 
418
        TODO: Perhaps return the ids of the files?  But then again it
 
419
              is easy to retrieve them if they're needed.
 
420
 
 
421
        TODO: Adding a directory should optionally recurse down and
 
422
              add all non-ignored children.  Perhaps do that in a
 
423
              higher-level method.
428
424
        """
 
425
        from bzrlib.textui import show_status
429
426
        # TODO: Re-adding a file that is removed in the working copy
430
427
        # should probably put it back with the previous ID.
431
428
        if isinstance(files, basestring):
466
463
                    file_id = gen_file_id(f)
467
464
                inv.add_path(f, kind=kind, file_id=file_id)
468
465
 
 
466
                if verbose:
 
467
                    print 'added', quotefn(f)
 
468
 
469
469
                mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
470
470
 
471
471
            self._write_inventory(inv)
501
501
        is the opposite of add.  Removing it is consistent with most
502
502
        other tools.  Maybe an option.
503
503
        """
 
504
        from bzrlib.textui import show_status
504
505
        ## TODO: Normalize names
505
506
        ## TODO: Remove nested loops; better scalability
506
507
        if isinstance(files, basestring):
581
582
            f.close()
582
583
 
583
584
 
584
 
    def get_revision_xml(self, revision_id):
585
 
        """Return XML file object for revision object."""
586
 
        if not revision_id or not isinstance(revision_id, basestring):
587
 
            raise InvalidRevisionId(revision_id)
588
 
 
589
 
        self.lock_read()
590
 
        try:
591
 
            try:
592
 
                return self.revision_store[revision_id]
593
 
            except IndexError:
594
 
                raise bzrlib.errors.NoSuchRevision(self, revision_id)
595
 
        finally:
596
 
            self.unlock()
597
 
 
598
 
 
599
585
    def get_revision(self, revision_id):
600
586
        """Return the Revision object for a named revision"""
601
 
        xml_file = self.get_revision_xml(revision_id)
 
587
        from bzrlib.revision import Revision
 
588
        from bzrlib.xml import unpack_xml
602
589
 
 
590
        self.lock_read()
603
591
        try:
604
 
            r = unpack_xml(Revision, xml_file)
605
 
        except SyntaxError, e:
606
 
            raise bzrlib.errors.BzrError('failed to unpack revision_xml',
607
 
                                         [revision_id,
608
 
                                          str(e)])
 
592
            if not revision_id or not isinstance(revision_id, basestring):
 
593
                raise ValueError('invalid revision-id: %r' % revision_id)
 
594
            r = unpack_xml(Revision, self.revision_store[revision_id])
 
595
        finally:
 
596
            self.unlock()
609
597
            
610
598
        assert r.revision_id == revision_id
611
599
        return r
612
 
 
613
 
 
614
 
    def get_revision_delta(self, revno):
615
 
        """Return the delta for one revision.
616
 
 
617
 
        The delta is relative to its mainline predecessor, or the
618
 
        empty tree for revision 1.
619
 
        """
620
 
        assert isinstance(revno, int)
621
 
        rh = self.revision_history()
622
 
        if not (1 <= revno <= len(rh)):
623
 
            raise InvalidRevisionNumber(revno)
624
 
 
625
 
        # revno is 1-based; list is 0-based
626
 
 
627
 
        new_tree = self.revision_tree(rh[revno-1])
628
 
        if revno == 1:
629
 
            old_tree = EmptyTree()
630
 
        else:
631
 
            old_tree = self.revision_tree(rh[revno-2])
632
 
 
633
 
        return compare_trees(old_tree, new_tree)
634
 
 
635
600
        
636
601
 
637
602
    def get_revision_sha1(self, revision_id):
642
607
        # the revision, (add signatures/remove signatures) and still
643
608
        # have all hash pointers stay consistent.
644
609
        # But for now, just hash the contents.
645
 
        return bzrlib.osutils.sha_file(self.get_revision_xml(revision_id))
 
610
        return sha_file(self.revision_store[revision_id])
646
611
 
647
612
 
648
613
    def get_inventory(self, inventory_id):
654
619
        from bzrlib.inventory import Inventory
655
620
        from bzrlib.xml import unpack_xml
656
621
 
657
 
        return unpack_xml(Inventory, self.get_inventory_xml(inventory_id))
658
 
 
659
 
 
660
 
    def get_inventory_xml(self, inventory_id):
661
 
        """Get inventory XML as a file object."""
662
 
        return self.inventory_store[inventory_id]
 
622
        return unpack_xml(Inventory, self.inventory_store[inventory_id])
663
623
            
664
624
 
665
625
    def get_inventory_sha1(self, inventory_id):
666
626
        """Return the sha1 hash of the inventory entry
667
627
        """
668
 
        return sha_file(self.get_inventory_xml(inventory_id))
 
628
        return sha_file(self.inventory_store[inventory_id])
669
629
 
670
630
 
671
631
    def get_revision_inventory(self, revision_id):
737
697
                return r+1, my_history[r]
738
698
        return None, None
739
699
 
 
700
    def enum_history(self, direction):
 
701
        """Return (revno, revision_id) for history of branch.
 
702
 
 
703
        direction
 
704
            'forward' is from earliest to latest
 
705
            'reverse' is from latest to earliest
 
706
        """
 
707
        rh = self.revision_history()
 
708
        if direction == 'forward':
 
709
            i = 1
 
710
            for rid in rh:
 
711
                yield i, rid
 
712
                i += 1
 
713
        elif direction == 'reverse':
 
714
            i = len(rh)
 
715
            while i > 0:
 
716
                yield i, rh[i-1]
 
717
                i -= 1
 
718
        else:
 
719
            raise ValueError('invalid history direction', direction)
 
720
 
740
721
 
741
722
    def revno(self):
742
723
        """Return current revision number for this branch.
757
738
            return None
758
739
 
759
740
 
760
 
    def missing_revisions(self, other, stop_revision=None, diverged_ok=False):
 
741
    def missing_revisions(self, other, stop_revision=None):
761
742
        """
762
743
        If self and other have not diverged, return a list of the revisions
763
744
        present in other, but missing from self.
796
777
        if stop_revision is None:
797
778
            stop_revision = other_len
798
779
        elif stop_revision > other_len:
799
 
            raise bzrlib.errors.NoSuchRevision(self, stop_revision)
 
780
            raise NoSuchRevision(self, stop_revision)
800
781
        
801
782
        return other_history[self_len:stop_revision]
802
783
 
803
784
 
804
785
    def update_revisions(self, other, stop_revision=None):
805
786
        """Pull in all new revisions from other branch.
 
787
        
 
788
        >>> from bzrlib.commit import commit
 
789
        >>> bzrlib.trace.silent = True
 
790
        >>> br1 = ScratchBranch(files=['foo', 'bar'])
 
791
        >>> br1.add('foo')
 
792
        >>> br1.add('bar')
 
793
        >>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False)
 
794
        >>> br2 = ScratchBranch()
 
795
        >>> br2.update_revisions(br1)
 
796
        Added 2 texts.
 
797
        Added 1 inventories.
 
798
        Added 1 revisions.
 
799
        >>> br2.revision_history()
 
800
        [u'REVISION-ID-1']
 
801
        >>> br2.update_revisions(br1)
 
802
        Added 0 texts.
 
803
        Added 0 inventories.
 
804
        Added 0 revisions.
 
805
        >>> br1.text_store.total_size() == br2.text_store.total_size()
 
806
        True
806
807
        """
807
 
        from bzrlib.fetch import greedy_fetch
808
 
 
809
 
        pb = bzrlib.ui.ui_factory.progress_bar()
 
808
        from bzrlib.progress import ProgressBar
 
809
        try:
 
810
            set
 
811
        except NameError:
 
812
            from sets import Set as set
 
813
 
 
814
        pb = ProgressBar()
 
815
 
810
816
        pb.update('comparing histories')
811
 
 
812
817
        revision_ids = self.missing_revisions(other, stop_revision)
813
818
 
814
 
        if len(revision_ids) > 0:
815
 
            count = greedy_fetch(self, other, revision_ids[-1], pb)[0]
816
 
        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
819
        if hasattr(other.revision_store, "prefetch"):
824
820
            other.revision_store.prefetch(revision_ids)
825
821
        if hasattr(other.inventory_store, "prefetch"):
826
822
            inventory_ids = [other.get_revision(r).inventory_id
827
823
                             for r in revision_ids]
828
824
            other.inventory_store.prefetch(inventory_ids)
829
 
 
830
 
        if pb is None:
831
 
            pb = bzrlib.ui.ui_factory.progress_bar()
832
825
                
833
826
        revisions = []
834
827
        needed_texts = set()
835
828
        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
 
 
 
829
        for rev_id in revision_ids:
 
830
            i += 1
 
831
            pb.update('fetching revision', i, len(revision_ids))
 
832
            rev = other.get_revision(rev_id)
846
833
            revisions.append(rev)
847
834
            inv = other.get_inventory(str(rev.inventory_id))
848
835
            for key, entry in inv.iter_entries():
853
840
 
854
841
        pb.clear()
855
842
                    
856
 
        count, cp_fail = self.text_store.copy_multi(other.text_store, 
857
 
                                                    needed_texts)
858
 
        #print "Added %d texts." % count 
 
843
        count = self.text_store.copy_multi(other.text_store, needed_texts)
 
844
        print "Added %d texts." % count 
859
845
        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 
 
846
        count = self.inventory_store.copy_multi(other.inventory_store, 
 
847
                                                inventory_ids)
 
848
        print "Added %d inventories." % count 
863
849
        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
 
       
871
 
 
 
850
        count = self.revision_store.copy_multi(other.revision_store, 
 
851
                                               revision_ids)
 
852
        for revision_id in revision_ids:
 
853
            self.append_revision(revision_id)
 
854
        print "Added %d revisions." % count
 
855
                    
 
856
        
872
857
    def commit(self, *args, **kw):
873
858
        from bzrlib.commit import commit
874
859
        commit(self, *args, **kw)
879
864
        revno, info = self.get_revision_info(revision)
880
865
        return info
881
866
 
882
 
 
883
 
    def revision_id_to_revno(self, revision_id):
884
 
        """Given a revision id, return its revno"""
885
 
        history = self.revision_history()
886
 
        try:
887
 
            return history.index(revision_id) + 1
888
 
        except ValueError:
889
 
            raise bzrlib.errors.NoSuchRevision(self, revision_id)
890
 
 
891
 
 
892
867
    def get_revision_info(self, revision):
893
868
        """Return (revno, revision id) for revision identifier.
894
869
 
1047
1022
 
1048
1023
        `revision_id` may be None for the null revision, in which case
1049
1024
        an `EmptyTree` is returned."""
 
1025
        from bzrlib.tree import EmptyTree, RevisionTree
1050
1026
        # TODO: refactor this to use an existing revision object
1051
1027
        # so we don't need to read it in twice.
1052
1028
        if revision_id == None:
1053
 
            return EmptyTree()
 
1029
            return EmptyTree(self.get_root_id())
1054
1030
        else:
1055
1031
            inv = self.get_revision_inventory(revision_id)
1056
1032
            return RevisionTree(self.text_store, inv)
1067
1043
 
1068
1044
        If there are no revisions yet, return an `EmptyTree`.
1069
1045
        """
 
1046
        from bzrlib.tree import EmptyTree, RevisionTree
1070
1047
        r = self.last_patch()
1071
1048
        if r == None:
1072
 
            return EmptyTree()
 
1049
            return EmptyTree(self.get_root_id())
1073
1050
        else:
1074
1051
            return RevisionTree(self.text_store, self.get_revision_inventory(r))
1075
1052
 
1110
1087
 
1111
1088
            inv.rename(file_id, to_dir_id, to_tail)
1112
1089
 
 
1090
            print "%s => %s" % (from_rel, to_rel)
 
1091
 
1113
1092
            from_abs = self.abspath(from_rel)
1114
1093
            to_abs = self.abspath(to_rel)
1115
1094
            try:
1134
1113
 
1135
1114
        Note that to_name is only the last component of the new name;
1136
1115
        this doesn't change the directory.
1137
 
 
1138
 
        This returns a list of (from_path, to_path) pairs for each
1139
 
        entry that is moved.
1140
1116
        """
1141
 
        result = []
1142
1117
        self.lock_write()
1143
1118
        try:
1144
1119
            ## TODO: Option to move IDs only
1179
1154
            for f in from_paths:
1180
1155
                name_tail = splitpath(f)[-1]
1181
1156
                dest_path = appendpath(to_name, name_tail)
1182
 
                result.append((f, dest_path))
 
1157
                print "%s => %s" % (f, dest_path)
1183
1158
                inv.rename(inv.path2id(f), to_dir_id, name_tail)
1184
1159
                try:
1185
1160
                    os.rename(self.abspath(f), self.abspath(dest_path))
1191
1166
        finally:
1192
1167
            self.unlock()
1193
1168
 
1194
 
        return result
1195
 
 
1196
1169
 
1197
1170
    def revert(self, filenames, old_tree=None, backups=True):
1198
1171
        """Restore selected files to the versions from a previous tree.
1402
1375
    """Return a new tree-root file id."""
1403
1376
    return gen_file_id('TREE_ROOT')
1404
1377
 
1405
 
 
1406
 
def pull_loc(branch):
1407
 
    # TODO: Should perhaps just make attribute be 'base' in
1408
 
    # RemoteBranch and Branch?
1409
 
    if hasattr(branch, "baseurl"):
1410
 
        return branch.baseurl
1411
 
    else:
1412
 
        return branch.base
1413
 
 
1414
 
 
1415
 
def copy_branch(branch_from, to_location, revision=None):
1416
 
    """Copy branch_from into the existing directory to_location.
1417
 
 
1418
 
    If revision is not None, the head of the new branch will be revision.
1419
 
    """
1420
 
    from bzrlib.merge import merge
1421
 
    from bzrlib.branch import Branch
1422
 
    br_to = Branch(to_location, init=True)
1423
 
    br_to.set_root_id(branch_from.get_root_id())
1424
 
    if revision is None:
1425
 
        revno = branch_from.revno()
1426
 
    else:
1427
 
        revno, rev_id = branch_from.get_revision_info(revision)
1428
 
    br_to.update_revisions(branch_from, stop_revision=revno)
1429
 
    merge((to_location, -1), (to_location, 0), this_dir=to_location,
1430
 
          check_clean=False, ignore_zero=True)
1431
 
    from_location = pull_loc(branch_from)
1432
 
    br_to.controlfile("x-pull", "wb").write(from_location + "\n")
1433