~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Martin Pool
  • Date: 2005-07-22 19:44:09 UTC
  • Revision ID: mbp@sourcefrog.net-20050722194409-9f99ffe3d62cf8c0
- tidy up imports to use full module names

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, os, os.path, random, time, sha, sets, types, re, shutil, tempfile
19
 
import traceback, socket, fnmatch, difflib, time
20
 
from binascii import hexlify
 
18
import sys, os
21
19
 
22
20
import bzrlib
23
 
from inventory import Inventory
24
 
from trace import mutter, note
25
 
from tree import Tree, EmptyTree, RevisionTree
26
 
from inventory import InventoryEntry, Inventory
27
 
from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \
28
 
     format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \
29
 
     joinpath, sha_file, sha_string, file_kind, local_time_offset, appendpath
30
 
from store import ImmutableStore
31
 
from revision import Revision
32
 
from errors import BzrError
33
 
from textui import show_status
 
21
from bzrlib.trace import mutter, note
 
22
from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, splitpath, \
 
23
     sha_file, appendpath, file_kind
 
24
from bzrlib.errors import BzrError
34
25
 
35
26
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
36
27
## TODO: Maybe include checks for common corruption of newlines, etc?
45
36
        return Branch(f, **args)
46
37
 
47
38
 
 
39
def find_cached_branch(f, cache_root, **args):
 
40
    from remotebranch import RemoteBranch
 
41
    br = find_branch(f, **args)
 
42
    def cacheify(br, store_name):
 
43
        from meta_store import CachedStore
 
44
        cache_path = os.path.join(cache_root, store_name)
 
45
        os.mkdir(cache_path)
 
46
        new_store = CachedStore(getattr(br, store_name), cache_path)
 
47
        setattr(br, store_name, new_store)
 
48
 
 
49
    if isinstance(br, RemoteBranch):
 
50
        cacheify(br, 'inventory_store')
 
51
        cacheify(br, 'text_store')
 
52
        cacheify(br, 'revision_store')
 
53
    return br
 
54
 
48
55
 
49
56
def _relpath(base, path):
50
57
    """Return path relative to base, or raise exception.
143
150
    _lock_count = None
144
151
    _lock = None
145
152
    
 
153
    # Map some sort of prefix into a namespace
 
154
    # stuff like "revno:10", "revid:", etc.
 
155
    # This should match a prefix with a function which accepts
 
156
    REVISION_NAMESPACES = {}
 
157
 
146
158
    def __init__(self, base, init=False, find_root=True):
147
159
        """Create new branch object at a particular location.
148
160
 
158
170
        In the test suite, creation of new trees is tested using the
159
171
        `ScratchBranch` class.
160
172
        """
 
173
        from bzrlib.store import ImmutableStore
161
174
        if init:
162
175
            self.base = os.path.realpath(base)
163
176
            self._make_control()
249
262
 
250
263
    def controlfilename(self, file_or_path):
251
264
        """Return location relative to branch."""
252
 
        if isinstance(file_or_path, types.StringTypes):
 
265
        if isinstance(file_or_path, basestring):
253
266
            file_or_path = [file_or_path]
254
267
        return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
255
268
 
282
295
 
283
296
 
284
297
    def _make_control(self):
 
298
        from bzrlib.inventory import Inventory
 
299
        from bzrlib.xml import pack_xml
 
300
        
285
301
        os.mkdir(self.controlfilename([]))
286
302
        self.controlfile('README', 'w').write(
287
303
            "This is a Bazaar-NG control directory.\n"
291
307
            os.mkdir(self.controlfilename(d))
292
308
        for f in ('revision-history', 'merged-patches',
293
309
                  'pending-merged-patches', 'branch-name',
294
 
                  'branch-lock'):
 
310
                  'branch-lock',
 
311
                  'pending-merges'):
295
312
            self.controlfile(f, 'w').write('')
296
313
        mutter('created control directory in ' + self.base)
297
 
        Inventory().write_xml(self.controlfile('inventory','w'))
 
314
 
 
315
        pack_xml(Inventory(gen_root_id()), self.controlfile('inventory','w'))
298
316
 
299
317
 
300
318
    def _check_format(self):
315
333
                           ['use a different bzr version',
316
334
                            'or remove the .bzr directory and "bzr init" again'])
317
335
 
 
336
    def get_root_id(self):
 
337
        """Return the id of this branches root"""
 
338
        inv = self.read_working_inventory()
 
339
        return inv.root.file_id
318
340
 
 
341
    def set_root_id(self, file_id):
 
342
        inv = self.read_working_inventory()
 
343
        orig_root_id = inv.root.file_id
 
344
        del inv._byid[inv.root.file_id]
 
345
        inv.root.file_id = file_id
 
346
        inv._byid[inv.root.file_id] = inv.root
 
347
        for fid in inv:
 
348
            entry = inv[fid]
 
349
            if entry.parent_id in (None, orig_root_id):
 
350
                entry.parent_id = inv.root.file_id
 
351
        self._write_inventory(inv)
319
352
 
320
353
    def read_working_inventory(self):
321
354
        """Read the working inventory."""
322
 
        before = time.time()
323
 
        # ElementTree does its own conversion from UTF-8, so open in
324
 
        # binary.
 
355
        from bzrlib.inventory import Inventory
 
356
        from bzrlib.xml import unpack_xml
 
357
        from time import time
 
358
        before = time()
325
359
        self.lock_read()
326
360
        try:
327
 
            inv = Inventory.read_xml(self.controlfile('inventory', 'rb'))
 
361
            # ElementTree does its own conversion from UTF-8, so open in
 
362
            # binary.
 
363
            inv = unpack_xml(Inventory,
 
364
                                  self.controlfile('inventory', 'rb'))
328
365
            mutter("loaded inventory of %d items in %f"
329
 
                   % (len(inv), time.time() - before))
 
366
                   % (len(inv), time() - before))
330
367
            return inv
331
368
        finally:
332
369
            self.unlock()
338
375
        That is to say, the inventory describing changes underway, that
339
376
        will be committed to the next revision.
340
377
        """
341
 
        ## TODO: factor out to atomicfile?  is rename safe on windows?
342
 
        ## TODO: Maybe some kind of clean/dirty marker on inventory?
343
 
        tmpfname = self.controlfilename('inventory.tmp')
344
 
        tmpf = file(tmpfname, 'wb')
345
 
        inv.write_xml(tmpf)
346
 
        tmpf.close()
347
 
        inv_fname = self.controlfilename('inventory')
348
 
        if sys.platform == 'win32':
349
 
            os.remove(inv_fname)
350
 
        os.rename(tmpfname, inv_fname)
 
378
        from bzrlib.atomicfile import AtomicFile
 
379
        from bzrlib.xml import pack_xml
 
380
        
 
381
        self.lock_write()
 
382
        try:
 
383
            f = AtomicFile(self.controlfilename('inventory'), 'wb')
 
384
            try:
 
385
                pack_xml(inv, f)
 
386
                f.commit()
 
387
            finally:
 
388
                f.close()
 
389
        finally:
 
390
            self.unlock()
 
391
        
351
392
        mutter('wrote working inventory')
352
393
            
353
394
 
381
422
              add all non-ignored children.  Perhaps do that in a
382
423
              higher-level method.
383
424
        """
 
425
        from bzrlib.textui import show_status
384
426
        # TODO: Re-adding a file that is removed in the working copy
385
427
        # should probably put it back with the previous ID.
386
 
        if isinstance(files, types.StringTypes):
387
 
            assert(ids is None or isinstance(ids, types.StringTypes))
 
428
        if isinstance(files, basestring):
 
429
            assert(ids is None or isinstance(ids, basestring))
388
430
            files = [files]
389
431
            if ids is not None:
390
432
                ids = [ids]
422
464
                inv.add_path(f, kind=kind, file_id=file_id)
423
465
 
424
466
                if verbose:
425
 
                    show_status('A', kind, quotefn(f))
 
467
                    print 'added', quotefn(f)
426
468
 
427
469
                mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
428
470
 
439
481
            # use inventory as it was in that revision
440
482
            file_id = tree.inventory.path2id(file)
441
483
            if not file_id:
442
 
                raise BzrError("%r is not present in revision %d" % (file, revno))
 
484
                raise BzrError("%r is not present in revision %s" % (file, revno))
443
485
            tree.print_file(file_id)
444
486
        finally:
445
487
            self.unlock()
459
501
        is the opposite of add.  Removing it is consistent with most
460
502
        other tools.  Maybe an option.
461
503
        """
 
504
        from bzrlib.textui import show_status
462
505
        ## TODO: Normalize names
463
506
        ## TODO: Remove nested loops; better scalability
464
 
        if isinstance(files, types.StringTypes):
 
507
        if isinstance(files, basestring):
465
508
            files = [files]
466
509
 
467
510
        self.lock_write()
492
535
 
493
536
    # FIXME: this doesn't need to be a branch method
494
537
    def set_inventory(self, new_inventory_list):
495
 
        inv = Inventory()
 
538
        from bzrlib.inventory import Inventory, InventoryEntry
 
539
        inv = Inventory(self.get_root_id())
496
540
        for path, file_id, parent, kind in new_inventory_list:
497
541
            name = os.path.basename(path)
498
542
            if name == "":
520
564
        return self.working_tree().unknowns()
521
565
 
522
566
 
523
 
    def append_revision(self, revision_id):
524
 
        mutter("add {%s} to revision-history" % revision_id)
 
567
    def append_revision(self, *revision_ids):
 
568
        from bzrlib.atomicfile import AtomicFile
 
569
 
 
570
        for revision_id in revision_ids:
 
571
            mutter("add {%s} to revision-history" % revision_id)
 
572
 
525
573
        rev_history = self.revision_history()
526
 
 
527
 
        tmprhname = self.controlfilename('revision-history.tmp')
528
 
        rhname = self.controlfilename('revision-history')
529
 
        
530
 
        f = file(tmprhname, 'wt')
531
 
        rev_history.append(revision_id)
532
 
        f.write('\n'.join(rev_history))
533
 
        f.write('\n')
534
 
        f.close()
535
 
 
536
 
        if sys.platform == 'win32':
537
 
            os.remove(rhname)
538
 
        os.rename(tmprhname, rhname)
539
 
        
 
574
        rev_history.extend(revision_ids)
 
575
 
 
576
        f = AtomicFile(self.controlfilename('revision-history'))
 
577
        try:
 
578
            for rev_id in rev_history:
 
579
                print >>f, rev_id
 
580
            f.commit()
 
581
        finally:
 
582
            f.close()
540
583
 
541
584
 
542
585
    def get_revision(self, revision_id):
543
586
        """Return the Revision object for a named revision"""
544
 
        if not revision_id or not isinstance(revision_id, basestring):
545
 
            raise ValueError('invalid revision-id: %r' % revision_id)
546
 
        r = Revision.read_xml(self.revision_store[revision_id])
 
587
        from bzrlib.revision import Revision
 
588
        from bzrlib.xml import unpack_xml
 
589
 
 
590
        self.lock_read()
 
591
        try:
 
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()
 
597
            
547
598
        assert r.revision_id == revision_id
548
599
        return r
 
600
        
549
601
 
550
602
    def get_revision_sha1(self, revision_id):
551
603
        """Hash the stored value of a revision, and return it."""
564
616
        TODO: Perhaps for this and similar methods, take a revision
565
617
               parameter which can be either an integer revno or a
566
618
               string hash."""
567
 
        i = Inventory.read_xml(self.inventory_store[inventory_id])
568
 
        return i
 
619
        from bzrlib.inventory import Inventory
 
620
        from bzrlib.xml import unpack_xml
 
621
 
 
622
        return unpack_xml(Inventory, self.inventory_store[inventory_id])
 
623
            
569
624
 
570
625
    def get_inventory_sha1(self, inventory_id):
571
626
        """Return the sha1 hash of the inventory entry
575
630
 
576
631
    def get_revision_inventory(self, revision_id):
577
632
        """Return inventory of a past revision."""
 
633
        # bzr 0.0.6 imposes the constraint that the inventory_id
 
634
        # must be the same as its revision, so this is trivial.
578
635
        if revision_id == None:
579
 
            return Inventory()
 
636
            from bzrlib.inventory import Inventory
 
637
            return Inventory(self.get_root_id())
580
638
        else:
581
 
            return self.get_inventory(self.get_revision(revision_id).inventory_id)
 
639
            return self.get_inventory(revision_id)
582
640
 
583
641
 
584
642
    def revision_history(self):
748
806
        True
749
807
        """
750
808
        from bzrlib.progress import ProgressBar
 
809
        try:
 
810
            set
 
811
        except NameError:
 
812
            from sets import Set as set
751
813
 
752
814
        pb = ProgressBar()
753
815
 
754
816
        pb.update('comparing histories')
755
817
        revision_ids = self.missing_revisions(other, stop_revision)
 
818
 
 
819
        if hasattr(other.revision_store, "prefetch"):
 
820
            other.revision_store.prefetch(revision_ids)
 
821
        if hasattr(other.inventory_store, "prefetch"):
 
822
            inventory_ids = [other.get_revision(r).inventory_id
 
823
                             for r in revision_ids]
 
824
            other.inventory_store.prefetch(inventory_ids)
 
825
                
756
826
        revisions = []
757
 
        needed_texts = sets.Set()
 
827
        needed_texts = set()
758
828
        i = 0
759
829
        for rev_id in revision_ids:
760
830
            i += 1
785
855
                    
786
856
        
787
857
    def commit(self, *args, **kw):
788
 
        """Deprecated"""
789
858
        from bzrlib.commit import commit
790
859
        commit(self, *args, **kw)
791
860
        
792
861
 
793
 
    def lookup_revision(self, revno):
794
 
        """Return revision hash for revision number."""
795
 
        if revno == 0:
796
 
            return None
797
 
 
798
 
        try:
799
 
            # list is 0-based; revisions are 1-based
800
 
            return self.revision_history()[revno-1]
801
 
        except IndexError:
802
 
            raise BzrError("no such revision %s" % revno)
803
 
 
 
862
    def lookup_revision(self, revision):
 
863
        """Return the revision identifier for a given revision information."""
 
864
        revno, info = self.get_revision_info(revision)
 
865
        return info
 
866
 
 
867
    def get_revision_info(self, revision):
 
868
        """Return (revno, revision id) for revision identifier.
 
869
 
 
870
        revision can be an integer, in which case it is assumed to be revno (though
 
871
            this will translate negative values into positive ones)
 
872
        revision can also be a string, in which case it is parsed for something like
 
873
            'date:' or 'revid:' etc.
 
874
        """
 
875
        if revision is None:
 
876
            return 0, None
 
877
        revno = None
 
878
        try:# Convert to int if possible
 
879
            revision = int(revision)
 
880
        except ValueError:
 
881
            pass
 
882
        revs = self.revision_history()
 
883
        if isinstance(revision, int):
 
884
            if revision == 0:
 
885
                return 0, None
 
886
            # Mabye we should do this first, but we don't need it if revision == 0
 
887
            if revision < 0:
 
888
                revno = len(revs) + revision + 1
 
889
            else:
 
890
                revno = revision
 
891
        elif isinstance(revision, basestring):
 
892
            for prefix, func in Branch.REVISION_NAMESPACES.iteritems():
 
893
                if revision.startswith(prefix):
 
894
                    revno = func(self, revs, revision)
 
895
                    break
 
896
            else:
 
897
                raise BzrError('No namespace registered for string: %r' % revision)
 
898
 
 
899
        if revno is None or revno <= 0 or revno > len(revs):
 
900
            raise BzrError("no such revision %s" % revision)
 
901
        return revno, revs[revno-1]
 
902
 
 
903
    def _namespace_revno(self, revs, revision):
 
904
        """Lookup a revision by revision number"""
 
905
        assert revision.startswith('revno:')
 
906
        try:
 
907
            return int(revision[6:])
 
908
        except ValueError:
 
909
            return None
 
910
    REVISION_NAMESPACES['revno:'] = _namespace_revno
 
911
 
 
912
    def _namespace_revid(self, revs, revision):
 
913
        assert revision.startswith('revid:')
 
914
        try:
 
915
            return revs.index(revision[6:]) + 1
 
916
        except ValueError:
 
917
            return None
 
918
    REVISION_NAMESPACES['revid:'] = _namespace_revid
 
919
 
 
920
    def _namespace_last(self, revs, revision):
 
921
        assert revision.startswith('last:')
 
922
        try:
 
923
            offset = int(revision[5:])
 
924
        except ValueError:
 
925
            return None
 
926
        else:
 
927
            if offset <= 0:
 
928
                raise BzrError('You must supply a positive value for --revision last:XXX')
 
929
            return len(revs) - offset + 1
 
930
    REVISION_NAMESPACES['last:'] = _namespace_last
 
931
 
 
932
    def _namespace_tag(self, revs, revision):
 
933
        assert revision.startswith('tag:')
 
934
        raise BzrError('tag: namespace registered, but not implemented.')
 
935
    REVISION_NAMESPACES['tag:'] = _namespace_tag
 
936
 
 
937
    def _namespace_date(self, revs, revision):
 
938
        assert revision.startswith('date:')
 
939
        import datetime
 
940
        # Spec for date revisions:
 
941
        #   date:value
 
942
        #   value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
 
943
        #   it can also start with a '+/-/='. '+' says match the first
 
944
        #   entry after the given date. '-' is match the first entry before the date
 
945
        #   '=' is match the first entry after, but still on the given date.
 
946
        #
 
947
        #   +2005-05-12 says find the first matching entry after May 12th, 2005 at 0:00
 
948
        #   -2005-05-12 says find the first matching entry before May 12th, 2005 at 0:00
 
949
        #   =2005-05-12 says find the first match after May 12th, 2005 at 0:00 but before
 
950
        #       May 13th, 2005 at 0:00
 
951
        #
 
952
        #   So the proper way of saying 'give me all entries for today' is:
 
953
        #       -r {date:+today}:{date:-tomorrow}
 
954
        #   The default is '=' when not supplied
 
955
        val = revision[5:]
 
956
        match_style = '='
 
957
        if val[:1] in ('+', '-', '='):
 
958
            match_style = val[:1]
 
959
            val = val[1:]
 
960
 
 
961
        today = datetime.datetime.today().replace(hour=0,minute=0,second=0,microsecond=0)
 
962
        if val.lower() == 'yesterday':
 
963
            dt = today - datetime.timedelta(days=1)
 
964
        elif val.lower() == 'today':
 
965
            dt = today
 
966
        elif val.lower() == 'tomorrow':
 
967
            dt = today + datetime.timedelta(days=1)
 
968
        else:
 
969
            import re
 
970
            # This should be done outside the function to avoid recompiling it.
 
971
            _date_re = re.compile(
 
972
                    r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
 
973
                    r'(,|T)?\s*'
 
974
                    r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
 
975
                )
 
976
            m = _date_re.match(val)
 
977
            if not m or (not m.group('date') and not m.group('time')):
 
978
                raise BzrError('Invalid revision date %r' % revision)
 
979
 
 
980
            if m.group('date'):
 
981
                year, month, day = int(m.group('year')), int(m.group('month')), int(m.group('day'))
 
982
            else:
 
983
                year, month, day = today.year, today.month, today.day
 
984
            if m.group('time'):
 
985
                hour = int(m.group('hour'))
 
986
                minute = int(m.group('minute'))
 
987
                if m.group('second'):
 
988
                    second = int(m.group('second'))
 
989
                else:
 
990
                    second = 0
 
991
            else:
 
992
                hour, minute, second = 0,0,0
 
993
 
 
994
            dt = datetime.datetime(year=year, month=month, day=day,
 
995
                    hour=hour, minute=minute, second=second)
 
996
        first = dt
 
997
        last = None
 
998
        reversed = False
 
999
        if match_style == '-':
 
1000
            reversed = True
 
1001
        elif match_style == '=':
 
1002
            last = dt + datetime.timedelta(days=1)
 
1003
 
 
1004
        if reversed:
 
1005
            for i in range(len(revs)-1, -1, -1):
 
1006
                r = self.get_revision(revs[i])
 
1007
                # TODO: Handle timezone.
 
1008
                dt = datetime.datetime.fromtimestamp(r.timestamp)
 
1009
                if first >= dt and (last is None or dt >= last):
 
1010
                    return i+1
 
1011
        else:
 
1012
            for i in range(len(revs)):
 
1013
                r = self.get_revision(revs[i])
 
1014
                # TODO: Handle timezone.
 
1015
                dt = datetime.datetime.fromtimestamp(r.timestamp)
 
1016
                if first <= dt and (last is None or dt <= last):
 
1017
                    return i+1
 
1018
    REVISION_NAMESPACES['date:'] = _namespace_date
804
1019
 
805
1020
    def revision_tree(self, revision_id):
806
1021
        """Return Tree for a revision on this branch.
807
1022
 
808
1023
        `revision_id` may be None for the null revision, in which case
809
1024
        an `EmptyTree` is returned."""
 
1025
        from bzrlib.tree import EmptyTree, RevisionTree
810
1026
        # TODO: refactor this to use an existing revision object
811
1027
        # so we don't need to read it in twice.
812
1028
        if revision_id == None:
813
 
            return EmptyTree()
 
1029
            return EmptyTree(self.get_root_id())
814
1030
        else:
815
1031
            inv = self.get_revision_inventory(revision_id)
816
1032
            return RevisionTree(self.text_store, inv)
827
1043
 
828
1044
        If there are no revisions yet, return an `EmptyTree`.
829
1045
        """
 
1046
        from bzrlib.tree import EmptyTree, RevisionTree
830
1047
        r = self.last_patch()
831
1048
        if r == None:
832
 
            return EmptyTree()
 
1049
            return EmptyTree(self.get_root_id())
833
1050
        else:
834
1051
            return RevisionTree(self.text_store, self.get_revision_inventory(r))
835
1052
 
950
1167
            self.unlock()
951
1168
 
952
1169
 
 
1170
    def revert(self, filenames, old_tree=None, backups=True):
 
1171
        """Restore selected files to the versions from a previous tree.
 
1172
 
 
1173
        backups
 
1174
            If true (default) backups are made of files before
 
1175
            they're renamed.
 
1176
        """
 
1177
        from bzrlib.errors import NotVersionedError, BzrError
 
1178
        from bzrlib.atomicfile import AtomicFile
 
1179
        from bzrlib.osutils import backup_file
 
1180
        
 
1181
        inv = self.read_working_inventory()
 
1182
        if old_tree is None:
 
1183
            old_tree = self.basis_tree()
 
1184
        old_inv = old_tree.inventory
 
1185
 
 
1186
        nids = []
 
1187
        for fn in filenames:
 
1188
            file_id = inv.path2id(fn)
 
1189
            if not file_id:
 
1190
                raise NotVersionedError("not a versioned file", fn)
 
1191
            if not old_inv.has_id(file_id):
 
1192
                raise BzrError("file not present in old tree", fn, file_id)
 
1193
            nids.append((fn, file_id))
 
1194
            
 
1195
        # TODO: Rename back if it was previously at a different location
 
1196
 
 
1197
        # TODO: If given a directory, restore the entire contents from
 
1198
        # the previous version.
 
1199
 
 
1200
        # TODO: Make a backup to a temporary file.
 
1201
 
 
1202
        # TODO: If the file previously didn't exist, delete it?
 
1203
        for fn, file_id in nids:
 
1204
            backup_file(fn)
 
1205
            
 
1206
            f = AtomicFile(fn, 'wb')
 
1207
            try:
 
1208
                f.write(old_tree.get_file(file_id).read())
 
1209
                f.commit()
 
1210
            finally:
 
1211
                f.close()
 
1212
 
 
1213
 
 
1214
    def pending_merges(self):
 
1215
        """Return a list of pending merges.
 
1216
 
 
1217
        These are revisions that have been merged into the working
 
1218
        directory but not yet committed.
 
1219
        """
 
1220
        cfn = self.controlfilename('pending-merges')
 
1221
        if not os.path.exists(cfn):
 
1222
            return []
 
1223
        p = []
 
1224
        for l in self.controlfile('pending-merges', 'r').readlines():
 
1225
            p.append(l.rstrip('\n'))
 
1226
        return p
 
1227
 
 
1228
 
 
1229
    def add_pending_merge(self, revision_id):
 
1230
        from bzrlib.revision import validate_revision_id
 
1231
 
 
1232
        validate_revision_id(revision_id)
 
1233
 
 
1234
        p = self.pending_merges()
 
1235
        if revision_id in p:
 
1236
            return
 
1237
        p.append(revision_id)
 
1238
        self.set_pending_merges(p)
 
1239
 
 
1240
 
 
1241
    def set_pending_merges(self, rev_list):
 
1242
        from bzrlib.atomicfile import AtomicFile
 
1243
        self.lock_write()
 
1244
        try:
 
1245
            f = AtomicFile(self.controlfilename('pending-merges'))
 
1246
            try:
 
1247
                for l in rev_list:
 
1248
                    print >>f, l
 
1249
                f.commit()
 
1250
            finally:
 
1251
                f.close()
 
1252
        finally:
 
1253
            self.unlock()
 
1254
 
 
1255
 
953
1256
 
954
1257
class ScratchBranch(Branch):
955
1258
    """Special test class: a branch that cleans up after itself.
969
1272
 
970
1273
        If any files are listed, they are created in the working copy.
971
1274
        """
 
1275
        from tempfile import mkdtemp
972
1276
        init = False
973
1277
        if base is None:
974
 
            base = tempfile.mkdtemp()
 
1278
            base = mkdtemp()
975
1279
            init = True
976
1280
        Branch.__init__(self, base, init=init)
977
1281
        for d in dirs:
990
1294
        >>> os.path.isfile(os.path.join(clone.base, "file1"))
991
1295
        True
992
1296
        """
993
 
        base = tempfile.mkdtemp()
 
1297
        from shutil import copytree
 
1298
        from tempfile import mkdtemp
 
1299
        base = mkdtemp()
994
1300
        os.rmdir(base)
995
 
        shutil.copytree(self.base, base, symlinks=True)
 
1301
        copytree(self.base, base, symlinks=True)
996
1302
        return ScratchBranch(base=base)
997
1303
        
998
1304
    def __del__(self):
1000
1306
 
1001
1307
    def destroy(self):
1002
1308
        """Destroy the test branch, removing the scratch directory."""
 
1309
        from shutil import rmtree
1003
1310
        try:
1004
1311
            if self.base:
1005
1312
                mutter("delete ScratchBranch %s" % self.base)
1006
 
                shutil.rmtree(self.base)
 
1313
                rmtree(self.base)
1007
1314
        except OSError, e:
1008
1315
            # Work around for shutil.rmtree failing on Windows when
1009
1316
            # readonly files are encountered
1011
1318
            for root, dirs, files in os.walk(self.base, topdown=False):
1012
1319
                for name in files:
1013
1320
                    os.chmod(os.path.join(root, name), 0700)
1014
 
            shutil.rmtree(self.base)
 
1321
            rmtree(self.base)
1015
1322
        self.base = None
1016
1323
 
1017
1324
    
1042
1349
    cope with just randomness because running uuidgen every time is
1043
1350
    slow."""
1044
1351
    import re
 
1352
    from binascii import hexlify
 
1353
    from time import time
1045
1354
 
1046
1355
    # get last component
1047
1356
    idx = name.rfind('/')
1059
1368
    name = re.sub(r'[^\w.]', '', name)
1060
1369
 
1061
1370
    s = hexlify(rand_bytes(8))
1062
 
    return '-'.join((name, compact_date(time.time()), s))
 
1371
    return '-'.join((name, compact_date(time()), s))
 
1372
 
 
1373
 
 
1374
def gen_root_id():
 
1375
    """Return a new tree-root file id."""
 
1376
    return gen_file_id('TREE_ROOT')
 
1377