~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Martin Pool
  • Date: 2005-05-11 02:30:01 UTC
  • Revision ID: mbp@sourcefrog.net-20050511023000-5580a38e987bb915
- tests for bzr root

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
from sets import Set
 
19
 
18
20
import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile
19
21
import traceback, socket, fnmatch, difflib, time
20
22
from binascii import hexlify
31
33
from revision import Revision
32
34
from errors import bailout, BzrError
33
35
from textui import show_status
 
36
from diff import diff_trees
34
37
 
35
38
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
36
39
## TODO: Maybe include checks for common corruption of newlines, etc?
80
83
######################################################################
81
84
# branch objects
82
85
 
83
 
class Branch(object):
 
86
class Branch:
84
87
    """Branch holding a history of revisions.
85
88
 
86
89
    base
87
90
        Base directory of the branch.
88
91
    """
89
92
    _lockmode = None
90
 
    base = None
91
93
    
92
94
    def __init__(self, base, init=False, find_root=True, lock_mode='w'):
93
95
        """Create new branch object at a particular location.
302
304
                         """Inventory for the working copy.""")
303
305
 
304
306
 
305
 
    def add(self, files, verbose=False, ids=None):
 
307
    def add(self, files, verbose=False):
306
308
        """Make files versioned.
307
309
 
308
310
        Note that the command line normally calls smart_add instead.
321
323
        TODO: Adding a directory should optionally recurse down and
322
324
               add all non-ignored children.  Perhaps do that in a
323
325
               higher-level method.
 
326
 
 
327
        >>> b = ScratchBranch(files=['foo'])
 
328
        >>> 'foo' in b.unknowns()
 
329
        True
 
330
        >>> b.show_status()
 
331
        ?       foo
 
332
        >>> b.add('foo')
 
333
        >>> 'foo' in b.unknowns()
 
334
        False
 
335
        >>> bool(b.inventory.path2id('foo'))
 
336
        True
 
337
        >>> b.show_status()
 
338
        A       foo
 
339
 
 
340
        >>> b.add('foo')
 
341
        Traceback (most recent call last):
 
342
        ...
 
343
        BzrError: ('foo is already versioned', [])
 
344
 
 
345
        >>> b.add(['nothere'])
 
346
        Traceback (most recent call last):
 
347
        BzrError: ('cannot add: not a regular file or directory: nothere', [])
324
348
        """
325
349
        self._need_writelock()
326
350
 
327
351
        # TODO: Re-adding a file that is removed in the working copy
328
352
        # should probably put it back with the previous ID.
329
353
        if isinstance(files, types.StringTypes):
330
 
            assert(ids is None or isinstance(ids, types.StringTypes))
331
354
            files = [files]
332
 
            if ids is not None:
333
 
                ids = [ids]
334
 
 
335
 
        if ids is None:
336
 
            ids = [None] * len(files)
337
 
        else:
338
 
            assert(len(ids) == len(files))
339
355
        
340
356
        inv = self.read_working_inventory()
341
 
        for f,file_id in zip(files, ids):
 
357
        for f in files:
342
358
            if is_control_file(f):
343
359
                bailout("cannot add control file %s" % quotefn(f))
344
360
 
358
374
            if kind != 'file' and kind != 'directory':
359
375
                bailout('cannot add: not a regular file or directory: %s' % quotefn(f))
360
376
 
361
 
            if file_id is None:
362
 
                file_id = gen_file_id(f)
 
377
            file_id = gen_file_id(f)
363
378
            inv.add_path(f, kind=kind, file_id=file_id)
364
379
 
365
380
            if verbose:
388
403
 
389
404
        TODO: Refuse to remove modified files unless --force is given?
390
405
 
 
406
        >>> b = ScratchBranch(files=['foo'])
 
407
        >>> b.add('foo')
 
408
        >>> b.inventory.has_filename('foo')
 
409
        True
 
410
        >>> b.remove('foo')
 
411
        >>> b.working_tree().has_filename('foo')
 
412
        True
 
413
        >>> b.inventory.has_filename('foo')
 
414
        False
 
415
        
 
416
        >>> b = ScratchBranch(files=['foo'])
 
417
        >>> b.add('foo')
 
418
        >>> b.commit('one')
 
419
        >>> b.remove('foo')
 
420
        >>> b.commit('two')
 
421
        >>> b.inventory.has_filename('foo') 
 
422
        False
 
423
        >>> b.basis_tree().has_filename('foo') 
 
424
        False
 
425
        >>> b.working_tree().has_filename('foo') 
 
426
        True
 
427
 
391
428
        TODO: Do something useful with directories.
392
429
 
393
430
        TODO: Should this remove the text or not?  Tough call; not
422
459
 
423
460
        self._write_inventory(inv)
424
461
 
425
 
    def set_inventory(self, new_inventory_list):
426
 
        inv = Inventory()
427
 
        for path, file_id, parent, kind in new_inventory_list:
428
 
            name = os.path.basename(path)
429
 
            if name == "":
430
 
                continue
431
 
            inv.add(InventoryEntry(file_id, name, kind, parent))
432
 
        self._write_inventory(inv)
433
 
 
434
462
 
435
463
    def unknowns(self):
436
464
        """Return all unknown files.
451
479
        return self.working_tree().unknowns()
452
480
 
453
481
 
 
482
    def commit(self, message, timestamp=None, timezone=None,
 
483
               committer=None,
 
484
               verbose=False):
 
485
        """Commit working copy as a new revision.
 
486
        
 
487
        The basic approach is to add all the file texts into the
 
488
        store, then the inventory, then make a new revision pointing
 
489
        to that inventory and store that.
 
490
        
 
491
        This is not quite safe if the working copy changes during the
 
492
        commit; for the moment that is simply not allowed.  A better
 
493
        approach is to make a temporary copy of the files before
 
494
        computing their hashes, and then add those hashes in turn to
 
495
        the inventory.  This should mean at least that there are no
 
496
        broken hash pointers.  There is no way we can get a snapshot
 
497
        of the whole directory at an instant.  This would also have to
 
498
        be robust against files disappearing, moving, etc.  So the
 
499
        whole thing is a bit hard.
 
500
 
 
501
        timestamp -- if not None, seconds-since-epoch for a
 
502
             postdated/predated commit.
 
503
        """
 
504
        self._need_writelock()
 
505
 
 
506
        ## TODO: Show branch names
 
507
 
 
508
        # TODO: Don't commit if there are no changes, unless forced?
 
509
 
 
510
        # First walk over the working inventory; and both update that
 
511
        # and also build a new revision inventory.  The revision
 
512
        # inventory needs to hold the text-id, sha1 and size of the
 
513
        # actual file versions committed in the revision.  (These are
 
514
        # not present in the working inventory.)  We also need to
 
515
        # detect missing/deleted files, and remove them from the
 
516
        # working inventory.
 
517
 
 
518
        work_inv = self.read_working_inventory()
 
519
        inv = Inventory()
 
520
        basis = self.basis_tree()
 
521
        basis_inv = basis.inventory
 
522
        missing_ids = []
 
523
        for path, entry in work_inv.iter_entries():
 
524
            ## TODO: Cope with files that have gone missing.
 
525
 
 
526
            ## TODO: Check that the file kind has not changed from the previous
 
527
            ## revision of this file (if any).
 
528
 
 
529
            entry = entry.copy()
 
530
 
 
531
            p = self.abspath(path)
 
532
            file_id = entry.file_id
 
533
            mutter('commit prep file %s, id %r ' % (p, file_id))
 
534
 
 
535
            if not os.path.exists(p):
 
536
                mutter("    file is missing, removing from inventory")
 
537
                if verbose:
 
538
                    show_status('D', entry.kind, quotefn(path))
 
539
                missing_ids.append(file_id)
 
540
                continue
 
541
 
 
542
            # TODO: Handle files that have been deleted
 
543
 
 
544
            # TODO: Maybe a special case for empty files?  Seems a
 
545
            # waste to store them many times.
 
546
 
 
547
            inv.add(entry)
 
548
 
 
549
            if basis_inv.has_id(file_id):
 
550
                old_kind = basis_inv[file_id].kind
 
551
                if old_kind != entry.kind:
 
552
                    bailout("entry %r changed kind from %r to %r"
 
553
                            % (file_id, old_kind, entry.kind))
 
554
 
 
555
            if entry.kind == 'directory':
 
556
                if not isdir(p):
 
557
                    bailout("%s is entered as directory but not a directory" % quotefn(p))
 
558
            elif entry.kind == 'file':
 
559
                if not isfile(p):
 
560
                    bailout("%s is entered as file but is not a file" % quotefn(p))
 
561
 
 
562
                content = file(p, 'rb').read()
 
563
 
 
564
                entry.text_sha1 = sha_string(content)
 
565
                entry.text_size = len(content)
 
566
 
 
567
                old_ie = basis_inv.has_id(file_id) and basis_inv[file_id]
 
568
                if (old_ie
 
569
                    and (old_ie.text_size == entry.text_size)
 
570
                    and (old_ie.text_sha1 == entry.text_sha1)):
 
571
                    ## assert content == basis.get_file(file_id).read()
 
572
                    entry.text_id = basis_inv[file_id].text_id
 
573
                    mutter('    unchanged from previous text_id {%s}' %
 
574
                           entry.text_id)
 
575
                    
 
576
                else:
 
577
                    entry.text_id = gen_file_id(entry.name)
 
578
                    self.text_store.add(content, entry.text_id)
 
579
                    mutter('    stored with text_id {%s}' % entry.text_id)
 
580
                    if verbose:
 
581
                        if not old_ie:
 
582
                            state = 'A'
 
583
                        elif (old_ie.name == entry.name
 
584
                              and old_ie.parent_id == entry.parent_id):
 
585
                            state = 'M'
 
586
                        else:
 
587
                            state = 'R'
 
588
 
 
589
                        show_status(state, entry.kind, quotefn(path))
 
590
 
 
591
        for file_id in missing_ids:
 
592
            # have to do this later so we don't mess up the iterator.
 
593
            # since parents may be removed before their children we
 
594
            # have to test.
 
595
 
 
596
            # FIXME: There's probably a better way to do this; perhaps
 
597
            # the workingtree should know how to filter itself.
 
598
            if work_inv.has_id(file_id):
 
599
                del work_inv[file_id]
 
600
 
 
601
 
 
602
        inv_id = rev_id = _gen_revision_id(time.time())
 
603
        
 
604
        inv_tmp = tempfile.TemporaryFile()
 
605
        inv.write_xml(inv_tmp)
 
606
        inv_tmp.seek(0)
 
607
        self.inventory_store.add(inv_tmp, inv_id)
 
608
        mutter('new inventory_id is {%s}' % inv_id)
 
609
 
 
610
        self._write_inventory(work_inv)
 
611
 
 
612
        if timestamp == None:
 
613
            timestamp = time.time()
 
614
 
 
615
        if committer == None:
 
616
            committer = username()
 
617
 
 
618
        if timezone == None:
 
619
            timezone = local_time_offset()
 
620
 
 
621
        mutter("building commit log message")
 
622
        rev = Revision(timestamp=timestamp,
 
623
                       timezone=timezone,
 
624
                       committer=committer,
 
625
                       precursor = self.last_patch(),
 
626
                       message = message,
 
627
                       inventory_id=inv_id,
 
628
                       revision_id=rev_id)
 
629
 
 
630
        rev_tmp = tempfile.TemporaryFile()
 
631
        rev.write_xml(rev_tmp)
 
632
        rev_tmp.seek(0)
 
633
        self.revision_store.add(rev_tmp, rev_id)
 
634
        mutter("new revision_id is {%s}" % rev_id)
 
635
        
 
636
        ## XXX: Everything up to here can simply be orphaned if we abort
 
637
        ## the commit; it will leave junk files behind but that doesn't
 
638
        ## matter.
 
639
 
 
640
        ## TODO: Read back the just-generated changeset, and make sure it
 
641
        ## applies and recreates the right state.
 
642
 
 
643
        ## TODO: Also calculate and store the inventory SHA1
 
644
        mutter("committing patch r%d" % (self.revno() + 1))
 
645
 
 
646
 
 
647
        self.append_revision(rev_id)
 
648
        
 
649
        if verbose:
 
650
            note("commited r%d" % self.revno())
 
651
 
 
652
 
454
653
    def append_revision(self, revision_id):
455
654
        mutter("add {%s} to revision-history" % revision_id)
456
655
        rev_history = self.revision_history()
527
726
                yield i, rh[i-1]
528
727
                i -= 1
529
728
        else:
530
 
            raise ValueError('invalid history direction', direction)
 
729
            raise BzrError('invalid history direction %r' % direction)
531
730
 
532
731
 
533
732
    def revno(self):
535
734
 
536
735
        That is equivalent to the number of revisions committed to
537
736
        this branch.
 
737
 
 
738
        >>> b = ScratchBranch()
 
739
        >>> b.revno()
 
740
        0
 
741
        >>> b.commit('no foo')
 
742
        >>> b.revno()
 
743
        1
538
744
        """
539
745
        return len(self.revision_history())
540
746
 
541
747
 
542
748
    def last_patch(self):
543
749
        """Return last patch hash, or None if no history.
 
750
 
 
751
        >>> ScratchBranch().last_patch() == None
 
752
        True
544
753
        """
545
754
        ph = self.revision_history()
546
755
        if ph:
547
756
            return ph[-1]
548
757
        else:
549
758
            return None
550
 
 
551
 
 
552
 
    def commit(self, *args, **kw):
553
 
        """Deprecated"""
554
 
        from bzrlib.commit import commit
555
 
        commit(self, *args, **kw)
556
759
        
557
760
 
558
761
    def lookup_revision(self, revno):
572
775
 
573
776
        `revision_id` may be None for the null revision, in which case
574
777
        an `EmptyTree` is returned."""
575
 
        # TODO: refactor this to use an existing revision object
576
 
        # so we don't need to read it in twice.
577
778
        self._need_readlock()
578
779
        if revision_id == None:
579
780
            return EmptyTree()
592
793
        """Return `Tree` object for last revision.
593
794
 
594
795
        If there are no revisions yet, return an `EmptyTree`.
 
796
 
 
797
        >>> b = ScratchBranch(files=['foo'])
 
798
        >>> b.basis_tree().has_filename('foo')
 
799
        False
 
800
        >>> b.working_tree().has_filename('foo')
 
801
        True
 
802
        >>> b.add('foo')
 
803
        >>> b.commit('add foo')
 
804
        >>> b.basis_tree().has_filename('foo')
 
805
        True
595
806
        """
596
807
        r = self.last_patch()
597
808
        if r == None:
678
889
        if to_dir_ie.kind not in ('directory', 'root_directory'):
679
890
            bailout("destination %r is not a directory" % to_abs)
680
891
 
681
 
        to_idpath = inv.get_idpath(to_dir_id)
 
892
        to_idpath = Set(inv.get_idpath(to_dir_id))
682
893
 
683
894
        for f in from_paths:
684
895
            if not tree.has_filename(f):
712
923
 
713
924
 
714
925
 
 
926
    def show_status(self, show_all=False, file_list=None):
 
927
        """Display single-line status for non-ignored working files.
 
928
 
 
929
        The list is show sorted in order by file name.
 
930
 
 
931
        >>> b = ScratchBranch(files=['foo', 'foo~'])
 
932
        >>> b.show_status()
 
933
        ?       foo
 
934
        >>> b.add('foo')
 
935
        >>> b.show_status()
 
936
        A       foo
 
937
        >>> b.commit("add foo")
 
938
        >>> b.show_status()
 
939
        >>> os.unlink(b.abspath('foo'))
 
940
        >>> b.show_status()
 
941
        D       foo
 
942
        """
 
943
        self._need_readlock()
 
944
 
 
945
        # We have to build everything into a list first so that it can
 
946
        # sorted by name, incorporating all the different sources.
 
947
 
 
948
        # FIXME: Rather than getting things in random order and then sorting,
 
949
        # just step through in order.
 
950
 
 
951
        # Interesting case: the old ID for a file has been removed,
 
952
        # but a new file has been created under that name.
 
953
 
 
954
        old = self.basis_tree()
 
955
        new = self.working_tree()
 
956
 
 
957
        items = diff_trees(old, new)
 
958
        # We want to filter out only if any file was provided in the file_list.
 
959
        if isinstance(file_list, list) and len(file_list):
 
960
            items = [item for item in items if item[3] in file_list]
 
961
 
 
962
        for fs, fid, oldname, newname, kind in items:
 
963
            if fs == 'R':
 
964
                show_status(fs, kind,
 
965
                            oldname + ' => ' + newname)
 
966
            elif fs == 'A' or fs == 'M':
 
967
                show_status(fs, kind, newname)
 
968
            elif fs == 'D':
 
969
                show_status(fs, kind, oldname)
 
970
            elif fs == '.':
 
971
                if show_all:
 
972
                    show_status(fs, kind, newname)
 
973
            elif fs == 'I':
 
974
                if show_all:
 
975
                    show_status(fs, kind, newname)
 
976
            elif fs == '?':
 
977
                show_status(fs, kind, newname)
 
978
            else:
 
979
                bailout("weird file state %r" % ((fs, fid),))
 
980
                
 
981
 
715
982
 
716
983
class ScratchBranch(Branch):
717
984
    """Special test class: a branch that cleans up after itself.
778
1045
 
779
1046
 
780
1047
 
 
1048
def _gen_revision_id(when):
 
1049
    """Return new revision-id."""
 
1050
    s = '%s-%s-' % (user_email(), compact_date(when))
 
1051
    s += hexlify(rand_bytes(8))
 
1052
    return s
 
1053
 
 
1054
 
781
1055
def gen_file_id(name):
782
1056
    """Return new file id.
783
1057
 
784
1058
    This should probably generate proper UUIDs, but for the moment we
785
1059
    cope with just randomness because running uuidgen every time is
786
1060
    slow."""
787
 
    import re
788
 
 
789
 
    # get last component
790
1061
    idx = name.rfind('/')
791
1062
    if idx != -1:
792
1063
        name = name[idx+1 : ]
794
1065
    if idx != -1:
795
1066
        name = name[idx+1 : ]
796
1067
 
797
 
    # make it not a hidden file
798
1068
    name = name.lstrip('.')
799
1069
 
800
 
    # remove any wierd characters; we don't escape them but rather
801
 
    # just pull them out
802
 
    name = re.sub(r'[^\w.]', '', name)
803
 
 
804
1070
    s = hexlify(rand_bytes(8))
805
1071
    return '-'.join((name, compact_date(time.time()), s))