~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 10:14:09 UTC
  • Revision ID: mbp@sourcefrog.net-20050511101409-25634506a9caacfd
- move commit code into its own module
- remove some doctest tests in favour of black-box tests
- specific-file parameters for diff and status now cover all
  files inside a directory

Show diffs side-by-side

added added

removed removed

Lines of Context:
322
322
        TODO: Adding a directory should optionally recurse down and
323
323
               add all non-ignored children.  Perhaps do that in a
324
324
               higher-level method.
325
 
 
326
 
        >>> b = ScratchBranch(files=['foo'])
327
 
        >>> 'foo' in b.unknowns()
328
 
        True
329
 
        >>> b.show_status()
330
 
        ?       foo
331
 
        >>> b.add('foo')
332
 
        >>> 'foo' in b.unknowns()
333
 
        False
334
 
        >>> bool(b.inventory.path2id('foo'))
335
 
        True
336
 
        >>> b.show_status()
337
 
        A       foo
338
 
 
339
 
        >>> b.add('foo')
340
 
        Traceback (most recent call last):
341
 
        ...
342
 
        BzrError: ('foo is already versioned', [])
343
 
 
344
 
        >>> b.add(['nothere'])
345
 
        Traceback (most recent call last):
346
 
        BzrError: ('cannot add: not a regular file or directory: nothere', [])
347
325
        """
348
326
        self._need_writelock()
349
327
 
402
380
 
403
381
        TODO: Refuse to remove modified files unless --force is given?
404
382
 
405
 
        >>> b = ScratchBranch(files=['foo'])
406
 
        >>> b.add('foo')
407
 
        >>> b.inventory.has_filename('foo')
408
 
        True
409
 
        >>> b.remove('foo')
410
 
        >>> b.working_tree().has_filename('foo')
411
 
        True
412
 
        >>> b.inventory.has_filename('foo')
413
 
        False
414
 
        
415
 
        >>> b = ScratchBranch(files=['foo'])
416
 
        >>> b.add('foo')
417
 
        >>> b.commit('one')
418
 
        >>> b.remove('foo')
419
 
        >>> b.commit('two')
420
 
        >>> b.inventory.has_filename('foo') 
421
 
        False
422
 
        >>> b.basis_tree().has_filename('foo') 
423
 
        False
424
 
        >>> b.working_tree().has_filename('foo') 
425
 
        True
426
 
 
427
383
        TODO: Do something useful with directories.
428
384
 
429
385
        TODO: Should this remove the text or not?  Tough call; not
478
434
        return self.working_tree().unknowns()
479
435
 
480
436
 
481
 
    def commit(self, message, timestamp=None, timezone=None,
482
 
               committer=None,
483
 
               verbose=False):
484
 
        """Commit working copy as a new revision.
485
 
        
486
 
        The basic approach is to add all the file texts into the
487
 
        store, then the inventory, then make a new revision pointing
488
 
        to that inventory and store that.
489
 
        
490
 
        This is not quite safe if the working copy changes during the
491
 
        commit; for the moment that is simply not allowed.  A better
492
 
        approach is to make a temporary copy of the files before
493
 
        computing their hashes, and then add those hashes in turn to
494
 
        the inventory.  This should mean at least that there are no
495
 
        broken hash pointers.  There is no way we can get a snapshot
496
 
        of the whole directory at an instant.  This would also have to
497
 
        be robust against files disappearing, moving, etc.  So the
498
 
        whole thing is a bit hard.
499
 
 
500
 
        timestamp -- if not None, seconds-since-epoch for a
501
 
             postdated/predated commit.
502
 
        """
503
 
        self._need_writelock()
504
 
 
505
 
        ## TODO: Show branch names
506
 
 
507
 
        # TODO: Don't commit if there are no changes, unless forced?
508
 
 
509
 
        # First walk over the working inventory; and both update that
510
 
        # and also build a new revision inventory.  The revision
511
 
        # inventory needs to hold the text-id, sha1 and size of the
512
 
        # actual file versions committed in the revision.  (These are
513
 
        # not present in the working inventory.)  We also need to
514
 
        # detect missing/deleted files, and remove them from the
515
 
        # working inventory.
516
 
 
517
 
        work_inv = self.read_working_inventory()
518
 
        inv = Inventory()
519
 
        basis = self.basis_tree()
520
 
        basis_inv = basis.inventory
521
 
        missing_ids = []
522
 
        for path, entry in work_inv.iter_entries():
523
 
            ## TODO: Cope with files that have gone missing.
524
 
 
525
 
            ## TODO: Check that the file kind has not changed from the previous
526
 
            ## revision of this file (if any).
527
 
 
528
 
            entry = entry.copy()
529
 
 
530
 
            p = self.abspath(path)
531
 
            file_id = entry.file_id
532
 
            mutter('commit prep file %s, id %r ' % (p, file_id))
533
 
 
534
 
            if not os.path.exists(p):
535
 
                mutter("    file is missing, removing from inventory")
536
 
                if verbose:
537
 
                    show_status('D', entry.kind, quotefn(path))
538
 
                missing_ids.append(file_id)
539
 
                continue
540
 
 
541
 
            # TODO: Handle files that have been deleted
542
 
 
543
 
            # TODO: Maybe a special case for empty files?  Seems a
544
 
            # waste to store them many times.
545
 
 
546
 
            inv.add(entry)
547
 
 
548
 
            if basis_inv.has_id(file_id):
549
 
                old_kind = basis_inv[file_id].kind
550
 
                if old_kind != entry.kind:
551
 
                    bailout("entry %r changed kind from %r to %r"
552
 
                            % (file_id, old_kind, entry.kind))
553
 
 
554
 
            if entry.kind == 'directory':
555
 
                if not isdir(p):
556
 
                    bailout("%s is entered as directory but not a directory" % quotefn(p))
557
 
            elif entry.kind == 'file':
558
 
                if not isfile(p):
559
 
                    bailout("%s is entered as file but is not a file" % quotefn(p))
560
 
 
561
 
                content = file(p, 'rb').read()
562
 
 
563
 
                entry.text_sha1 = sha_string(content)
564
 
                entry.text_size = len(content)
565
 
 
566
 
                old_ie = basis_inv.has_id(file_id) and basis_inv[file_id]
567
 
                if (old_ie
568
 
                    and (old_ie.text_size == entry.text_size)
569
 
                    and (old_ie.text_sha1 == entry.text_sha1)):
570
 
                    ## assert content == basis.get_file(file_id).read()
571
 
                    entry.text_id = basis_inv[file_id].text_id
572
 
                    mutter('    unchanged from previous text_id {%s}' %
573
 
                           entry.text_id)
574
 
                    
575
 
                else:
576
 
                    entry.text_id = gen_file_id(entry.name)
577
 
                    self.text_store.add(content, entry.text_id)
578
 
                    mutter('    stored with text_id {%s}' % entry.text_id)
579
 
                    if verbose:
580
 
                        if not old_ie:
581
 
                            state = 'A'
582
 
                        elif (old_ie.name == entry.name
583
 
                              and old_ie.parent_id == entry.parent_id):
584
 
                            state = 'M'
585
 
                        else:
586
 
                            state = 'R'
587
 
 
588
 
                        show_status(state, entry.kind, quotefn(path))
589
 
 
590
 
        for file_id in missing_ids:
591
 
            # have to do this later so we don't mess up the iterator.
592
 
            # since parents may be removed before their children we
593
 
            # have to test.
594
 
 
595
 
            # FIXME: There's probably a better way to do this; perhaps
596
 
            # the workingtree should know how to filter itself.
597
 
            if work_inv.has_id(file_id):
598
 
                del work_inv[file_id]
599
 
 
600
 
 
601
 
        inv_id = rev_id = _gen_revision_id(time.time())
602
 
        
603
 
        inv_tmp = tempfile.TemporaryFile()
604
 
        inv.write_xml(inv_tmp)
605
 
        inv_tmp.seek(0)
606
 
        self.inventory_store.add(inv_tmp, inv_id)
607
 
        mutter('new inventory_id is {%s}' % inv_id)
608
 
 
609
 
        self._write_inventory(work_inv)
610
 
 
611
 
        if timestamp == None:
612
 
            timestamp = time.time()
613
 
 
614
 
        if committer == None:
615
 
            committer = username()
616
 
 
617
 
        if timezone == None:
618
 
            timezone = local_time_offset()
619
 
 
620
 
        mutter("building commit log message")
621
 
        rev = Revision(timestamp=timestamp,
622
 
                       timezone=timezone,
623
 
                       committer=committer,
624
 
                       precursor = self.last_patch(),
625
 
                       message = message,
626
 
                       inventory_id=inv_id,
627
 
                       revision_id=rev_id)
628
 
 
629
 
        rev_tmp = tempfile.TemporaryFile()
630
 
        rev.write_xml(rev_tmp)
631
 
        rev_tmp.seek(0)
632
 
        self.revision_store.add(rev_tmp, rev_id)
633
 
        mutter("new revision_id is {%s}" % rev_id)
634
 
        
635
 
        ## XXX: Everything up to here can simply be orphaned if we abort
636
 
        ## the commit; it will leave junk files behind but that doesn't
637
 
        ## matter.
638
 
 
639
 
        ## TODO: Read back the just-generated changeset, and make sure it
640
 
        ## applies and recreates the right state.
641
 
 
642
 
        ## TODO: Also calculate and store the inventory SHA1
643
 
        mutter("committing patch r%d" % (self.revno() + 1))
644
 
 
645
 
 
646
 
        self.append_revision(rev_id)
647
 
        
648
 
        if verbose:
649
 
            note("commited r%d" % self.revno())
650
 
 
651
 
 
652
437
    def append_revision(self, revision_id):
653
438
        mutter("add {%s} to revision-history" % revision_id)
654
439
        rev_history = self.revision_history()
733
518
 
734
519
        That is equivalent to the number of revisions committed to
735
520
        this branch.
736
 
 
737
 
        >>> b = ScratchBranch()
738
 
        >>> b.revno()
739
 
        0
740
 
        >>> b.commit('no foo')
741
 
        >>> b.revno()
742
 
        1
743
521
        """
744
522
        return len(self.revision_history())
745
523
 
746
524
 
747
525
    def last_patch(self):
748
526
        """Return last patch hash, or None if no history.
749
 
 
750
 
        >>> ScratchBranch().last_patch() == None
751
 
        True
752
527
        """
753
528
        ph = self.revision_history()
754
529
        if ph:
755
530
            return ph[-1]
756
531
        else:
757
532
            return None
 
533
 
 
534
 
 
535
    def commit(self, *args, **kw):
 
536
        """Deprecated"""
 
537
        from bzrlib.commit import commit
 
538
        commit(self, *args, **kw)
758
539
        
759
540
 
760
541
    def lookup_revision(self, revno):
792
573
        """Return `Tree` object for last revision.
793
574
 
794
575
        If there are no revisions yet, return an `EmptyTree`.
795
 
 
796
 
        >>> b = ScratchBranch(files=['foo'])
797
 
        >>> b.basis_tree().has_filename('foo')
798
 
        False
799
 
        >>> b.working_tree().has_filename('foo')
800
 
        True
801
 
        >>> b.add('foo')
802
 
        >>> b.commit('add foo')
803
 
        >>> b.basis_tree().has_filename('foo')
804
 
        True
805
576
        """
806
577
        r = self.last_patch()
807
578
        if r == None:
988
759
 
989
760
 
990
761
 
991
 
def _gen_revision_id(when):
992
 
    """Return new revision-id."""
993
 
    s = '%s-%s-' % (user_email(), compact_date(when))
994
 
    s += hexlify(rand_bytes(8))
995
 
    return s
996
 
 
997
 
 
998
762
def gen_file_id(name):
999
763
    """Return new file id.
1000
764