~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Martin Pool
  • Date: 2005-05-10 08:15:58 UTC
  • Revision ID: mbp@sourcefrog.net-20050510081558-9a38e2c46ba4ebc4
- Patch from Fredrik Lundh to check Python version and 
  try to find a better one if it's too old.

  Patched to try to prevent infinite loops in wierd configurations,
  and to log to stderr.

Show diffs side-by-side

added added

removed removed

Lines of Context:
26
26
from trace import mutter, note
27
27
from tree import Tree, EmptyTree, RevisionTree, WorkingTree
28
28
from inventory import InventoryEntry, Inventory
29
 
from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \
 
29
from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \
30
30
     format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \
31
 
     joinpath, sha_string, file_kind, local_time_offset
 
31
     joinpath, sha_string, file_kind, local_time_offset, appendpath
32
32
from store import ImmutableStore
33
33
from revision import Revision
34
 
from errors import bailout
 
34
from errors import bailout, BzrError
35
35
from textui import show_status
36
36
from diff import diff_trees
37
37
 
40
40
 
41
41
 
42
42
 
 
43
def find_branch(f, **args):
 
44
    if f.startswith('http://') or f.startswith('https://'):
 
45
        import remotebranch 
 
46
        return remotebranch.RemoteBranch(f, **args)
 
47
    else:
 
48
        return Branch(f, **args)
 
49
        
 
50
 
43
51
def find_branch_root(f=None):
44
52
    """Find the branch root enclosing f, or pwd.
45
53
 
 
54
    f may be a filename or a URL.
 
55
 
46
56
    It is not necessary that f exists.
47
57
 
48
58
    Basically we keep looking up until we find the control directory or
49
59
    run into the root."""
50
 
    if f is None:
 
60
    if f == None:
51
61
        f = os.getcwd()
52
62
    elif hasattr(os.path, 'realpath'):
53
63
        f = os.path.realpath(f)
54
64
    else:
55
65
        f = os.path.abspath(f)
 
66
    if not os.path.exists(f):
 
67
        raise BzrError('%r does not exist' % f)
 
68
        
56
69
 
57
70
    orig_f = f
58
71
 
59
 
    last_f = f
60
72
    while True:
61
73
        if os.path.exists(os.path.join(f, bzrlib.BZRDIR)):
62
74
            return f
63
75
        head, tail = os.path.split(f)
64
76
        if head == f:
65
77
            # reached the root, whatever that may be
66
 
            bailout('%r is not in a branch' % orig_f)
 
78
            raise BzrError('%r is not in a branch' % orig_f)
67
79
        f = head
68
80
    
69
81
 
74
86
class Branch:
75
87
    """Branch holding a history of revisions.
76
88
 
77
 
    :todo: Perhaps use different stores for different classes of object,
78
 
           so that we can keep track of how much space each one uses,
79
 
           or garbage-collect them.
80
 
 
81
 
    :todo: Add a RemoteBranch subclass.  For the basic case of read-only
82
 
           HTTP access this should be very easy by, 
83
 
           just redirecting controlfile access into HTTP requests.
84
 
           We would need a RemoteStore working similarly.
85
 
 
86
 
    :todo: Keep the on-disk branch locked while the object exists.
87
 
 
88
 
    :todo: mkdir() method.
 
89
    base
 
90
        Base directory of the branch.
89
91
    """
90
 
    def __init__(self, base, init=False, find_root=True):
 
92
    _lockmode = None
 
93
    
 
94
    def __init__(self, base, init=False, find_root=True, lock_mode='w'):
91
95
        """Create new branch object at a particular location.
92
96
 
93
 
        :param base: Base directory for the branch.
 
97
        base -- Base directory for the branch.
94
98
        
95
 
        :param init: If True, create new control files in a previously
 
99
        init -- If True, create new control files in a previously
96
100
             unversioned directory.  If False, the branch must already
97
101
             be versioned.
98
102
 
99
 
        :param find_root: If true and init is false, find the root of the
 
103
        find_root -- If true and init is false, find the root of the
100
104
             existing branch containing base.
101
105
 
102
106
        In the test suite, creation of new trees is tested using the
114
118
                        ['use "bzr init" to initialize a new working tree',
115
119
                         'current bzr can only operate from top-of-tree'])
116
120
        self._check_format()
 
121
        self.lock(lock_mode)
117
122
 
118
123
        self.text_store = ImmutableStore(self.controlfilename('text-store'))
119
124
        self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
127
132
    __repr__ = __str__
128
133
 
129
134
 
 
135
 
 
136
    def lock(self, mode='w'):
 
137
        """Lock the on-disk branch, excluding other processes."""
 
138
        try:
 
139
            import fcntl, errno
 
140
 
 
141
            if mode == 'w':
 
142
                lm = fcntl.LOCK_EX
 
143
                om = os.O_WRONLY | os.O_CREAT
 
144
            elif mode == 'r':
 
145
                lm = fcntl.LOCK_SH
 
146
                om = os.O_RDONLY
 
147
            else:
 
148
                raise BzrError("invalid locking mode %r" % mode)
 
149
 
 
150
            try:
 
151
                lockfile = os.open(self.controlfilename('branch-lock'), om)
 
152
            except OSError, e:
 
153
                if e.errno == errno.ENOENT:
 
154
                    # might not exist on branches from <0.0.4
 
155
                    self.controlfile('branch-lock', 'w').close()
 
156
                    lockfile = os.open(self.controlfilename('branch-lock'), om)
 
157
                else:
 
158
                    raise e
 
159
            
 
160
            fcntl.lockf(lockfile, lm)
 
161
            def unlock():
 
162
                fcntl.lockf(lockfile, fcntl.LOCK_UN)
 
163
                os.close(lockfile)
 
164
                self._lockmode = None
 
165
            self.unlock = unlock
 
166
            self._lockmode = mode
 
167
        except ImportError:
 
168
            warning("please write a locking method for platform %r" % sys.platform)
 
169
            def unlock():
 
170
                self._lockmode = None
 
171
            self.unlock = unlock
 
172
            self._lockmode = mode
 
173
 
 
174
 
 
175
    def _need_readlock(self):
 
176
        if self._lockmode not in ['r', 'w']:
 
177
            raise BzrError('need read lock on branch, only have %r' % self._lockmode)
 
178
 
 
179
    def _need_writelock(self):
 
180
        if self._lockmode not in ['w']:
 
181
            raise BzrError('need write lock on branch, only have %r' % self._lockmode)
 
182
 
 
183
 
130
184
    def abspath(self, name):
131
185
        """Return absolute filename for something in the branch"""
132
186
        return os.path.join(self.base, name)
153
207
 
154
208
 
155
209
    def controlfile(self, file_or_path, mode='r'):
156
 
        """Open a control file for this branch"""
157
 
        return file(self.controlfilename(file_or_path), mode)
 
210
        """Open a control file for this branch.
 
211
 
 
212
        There are two classes of file in the control directory: text
 
213
        and binary.  binary files are untranslated byte streams.  Text
 
214
        control files are stored with Unix newlines and in UTF-8, even
 
215
        if the platform or locale defaults are different.
 
216
 
 
217
        Controlfiles should almost never be opened in write mode but
 
218
        rather should be atomically copied and replaced using atomicfile.
 
219
        """
 
220
 
 
221
        fn = self.controlfilename(file_or_path)
 
222
 
 
223
        if mode == 'rb' or mode == 'wb':
 
224
            return file(fn, mode)
 
225
        elif mode == 'r' or mode == 'w':
 
226
            # open in binary mode anyhow so there's no newline translation;
 
227
            # codecs uses line buffering by default; don't want that.
 
228
            import codecs
 
229
            return codecs.open(fn, mode + 'b', 'utf-8',
 
230
                               buffering=60000)
 
231
        else:
 
232
            raise BzrError("invalid controlfile mode %r" % mode)
 
233
 
158
234
 
159
235
 
160
236
    def _make_control(self):
166
242
        for d in ('text-store', 'inventory-store', 'revision-store'):
167
243
            os.mkdir(self.controlfilename(d))
168
244
        for f in ('revision-history', 'merged-patches',
169
 
                  'pending-merged-patches', 'branch-name'):
 
245
                  'pending-merged-patches', 'branch-name',
 
246
                  'branch-lock'):
170
247
            self.controlfile(f, 'w').write('')
171
248
        mutter('created control directory in ' + self.base)
172
249
        Inventory().write_xml(self.controlfile('inventory','w'))
179
256
 
180
257
        In the future, we might need different in-memory Branch
181
258
        classes to support downlevel branches.  But not yet.
182
 
        """        
183
 
        # read in binary mode to detect newline wierdness.
184
 
        fmt = self.controlfile('branch-format', 'rb').read()
 
259
        """
 
260
        # This ignores newlines so that we can open branches created
 
261
        # on Windows from Linux and so on.  I think it might be better
 
262
        # to always make all internal files in unix format.
 
263
        fmt = self.controlfile('branch-format', 'r').read()
 
264
        fmt.replace('\r\n', '')
185
265
        if fmt != BZR_BRANCH_FORMAT:
186
266
            bailout('sorry, branch format %r not supported' % fmt,
187
267
                    ['use a different bzr version',
190
270
 
191
271
    def read_working_inventory(self):
192
272
        """Read the working inventory."""
 
273
        self._need_readlock()
193
274
        before = time.time()
194
 
        inv = Inventory.read_xml(self.controlfile('inventory', 'r'))
 
275
        # ElementTree does its own conversion from UTF-8, so open in
 
276
        # binary.
 
277
        inv = Inventory.read_xml(self.controlfile('inventory', 'rb'))
195
278
        mutter("loaded inventory of %d items in %f"
196
279
               % (len(inv), time.time() - before))
197
280
        return inv
203
286
        That is to say, the inventory describing changes underway, that
204
287
        will be committed to the next revision.
205
288
        """
 
289
        self._need_writelock()
206
290
        ## TODO: factor out to atomicfile?  is rename safe on windows?
207
291
        ## TODO: Maybe some kind of clean/dirty marker on inventory?
208
292
        tmpfname = self.controlfilename('inventory.tmp')
209
 
        tmpf = file(tmpfname, 'w')
 
293
        tmpf = file(tmpfname, 'wb')
210
294
        inv.write_xml(tmpf)
211
295
        tmpf.close()
212
 
        os.rename(tmpfname, self.controlfilename('inventory'))
 
296
        inv_fname = self.controlfilename('inventory')
 
297
        if sys.platform == 'win32':
 
298
            os.remove(inv_fname)
 
299
        os.rename(tmpfname, inv_fname)
213
300
        mutter('wrote working inventory')
214
301
 
215
302
 
220
307
    def add(self, files, verbose=False):
221
308
        """Make files versioned.
222
309
 
 
310
        Note that the command line normally calls smart_add instead.
 
311
 
223
312
        This puts the files in the Added state, so that they will be
224
313
        recorded by the next commit.
225
314
 
226
 
        :todo: Perhaps have an option to add the ids even if the files do
 
315
        TODO: Perhaps have an option to add the ids even if the files do
227
316
               not (yet) exist.
228
317
 
229
 
        :todo: Perhaps return the ids of the files?  But then again it
 
318
        TODO: Perhaps return the ids of the files?  But then again it
230
319
               is easy to retrieve them if they're needed.
231
320
 
232
 
        :todo: Option to specify file id.
 
321
        TODO: Option to specify file id.
233
322
 
234
 
        :todo: Adding a directory should optionally recurse down and
 
323
        TODO: Adding a directory should optionally recurse down and
235
324
               add all non-ignored children.  Perhaps do that in a
236
325
               higher-level method.
237
326
 
257
346
        Traceback (most recent call last):
258
347
        BzrError: ('cannot add: not a regular file or directory: nothere', [])
259
348
        """
 
349
        self._need_writelock()
260
350
 
261
351
        # TODO: Re-adding a file that is removed in the working copy
262
352
        # should probably put it back with the previous ID.
295
385
        self._write_inventory(inv)
296
386
 
297
387
 
 
388
    def print_file(self, file, revno):
 
389
        """Print `file` to stdout."""
 
390
        self._need_readlock()
 
391
        tree = self.revision_tree(self.lookup_revision(revno))
 
392
        # use inventory as it was in that revision
 
393
        file_id = tree.inventory.path2id(file)
 
394
        if not file_id:
 
395
            bailout("%r is not present in revision %d" % (file, revno))
 
396
        tree.print_file(file_id)
 
397
        
298
398
 
299
399
    def remove(self, files, verbose=False):
300
400
        """Mark nominated files for removal from the inventory.
301
401
 
302
402
        This does not remove their text.  This does not run on 
303
403
 
304
 
        :todo: Refuse to remove modified files unless --force is given?
 
404
        TODO: Refuse to remove modified files unless --force is given?
305
405
 
306
406
        >>> b = ScratchBranch(files=['foo'])
307
407
        >>> b.add('foo')
325
425
        >>> b.working_tree().has_filename('foo') 
326
426
        True
327
427
 
328
 
        :todo: Do something useful with directories.
 
428
        TODO: Do something useful with directories.
329
429
 
330
 
        :todo: Should this remove the text or not?  Tough call; not
 
430
        TODO: Should this remove the text or not?  Tough call; not
331
431
        removing may be useful and the user can just use use rm, and
332
432
        is the opposite of add.  Removing it is consistent with most
333
433
        other tools.  Maybe an option.
334
434
        """
335
435
        ## TODO: Normalize names
336
436
        ## TODO: Remove nested loops; better scalability
 
437
        self._need_writelock()
337
438
 
338
439
        if isinstance(files, types.StringTypes):
339
440
            files = [files]
397
498
        be robust against files disappearing, moving, etc.  So the
398
499
        whole thing is a bit hard.
399
500
 
400
 
        :param timestamp: if not None, seconds-since-epoch for a
 
501
        timestamp -- if not None, seconds-since-epoch for a
401
502
             postdated/predated commit.
402
503
        """
 
504
        self._need_writelock()
403
505
 
404
506
        ## TODO: Show branch names
405
507
 
480
582
                            state = 'A'
481
583
                        elif (old_ie.name == entry.name
482
584
                              and old_ie.parent_id == entry.parent_id):
 
585
                            state = 'M'
 
586
                        else:
483
587
                            state = 'R'
484
 
                        else:
485
 
                            state = 'M'
486
588
 
487
589
                        show_status(state, entry.kind, quotefn(path))
488
590
 
541
643
        ## TODO: Also calculate and store the inventory SHA1
542
644
        mutter("committing patch r%d" % (self.revno() + 1))
543
645
 
544
 
        mutter("append to revision-history")
545
 
        self.controlfile('revision-history', 'at').write(rev_id + '\n')
546
 
 
547
 
        mutter("done!")
 
646
 
 
647
        self.append_revision(rev_id)
 
648
        
 
649
        if verbose:
 
650
            note("commited r%d" % self.revno())
 
651
 
 
652
 
 
653
    def append_revision(self, revision_id):
 
654
        mutter("add {%s} to revision-history" % revision_id)
 
655
        rev_history = self.revision_history()
 
656
 
 
657
        tmprhname = self.controlfilename('revision-history.tmp')
 
658
        rhname = self.controlfilename('revision-history')
 
659
        
 
660
        f = file(tmprhname, 'wt')
 
661
        rev_history.append(revision_id)
 
662
        f.write('\n'.join(rev_history))
 
663
        f.write('\n')
 
664
        f.close()
 
665
 
 
666
        if sys.platform == 'win32':
 
667
            os.remove(rhname)
 
668
        os.rename(tmprhname, rhname)
 
669
        
548
670
 
549
671
 
550
672
    def get_revision(self, revision_id):
551
673
        """Return the Revision object for a named revision"""
 
674
        self._need_readlock()
552
675
        r = Revision.read_xml(self.revision_store[revision_id])
553
676
        assert r.revision_id == revision_id
554
677
        return r
557
680
    def get_inventory(self, inventory_id):
558
681
        """Get Inventory object by hash.
559
682
 
560
 
        :todo: Perhaps for this and similar methods, take a revision
 
683
        TODO: Perhaps for this and similar methods, take a revision
561
684
               parameter which can be either an integer revno or a
562
685
               string hash."""
 
686
        self._need_readlock()
563
687
        i = Inventory.read_xml(self.inventory_store[inventory_id])
564
688
        return i
565
689
 
566
690
 
567
691
    def get_revision_inventory(self, revision_id):
568
692
        """Return inventory of a past revision."""
 
693
        self._need_readlock()
569
694
        if revision_id == None:
570
695
            return Inventory()
571
696
        else:
578
703
        >>> ScratchBranch().revision_history()
579
704
        []
580
705
        """
581
 
        return [chomp(l) for l in self.controlfile('revision-history').readlines()]
 
706
        self._need_readlock()
 
707
        return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()]
 
708
 
 
709
 
 
710
    def enum_history(self, direction):
 
711
        """Return (revno, revision_id) for history of branch.
 
712
 
 
713
        direction
 
714
            'forward' is from earliest to latest
 
715
            'reverse' is from latest to earliest
 
716
        """
 
717
        rh = self.revision_history()
 
718
        if direction == 'forward':
 
719
            i = 1
 
720
            for rid in rh:
 
721
                yield i, rid
 
722
                i += 1
 
723
        elif direction == 'reverse':
 
724
            i = len(rh)
 
725
            while i > 0:
 
726
                yield i, rh[i-1]
 
727
                i -= 1
 
728
        else:
 
729
            raise BzrError('invalid history direction %r' % direction)
582
730
 
583
731
 
584
732
    def revno(self):
606
754
        ph = self.revision_history()
607
755
        if ph:
608
756
            return ph[-1]
609
 
 
 
757
        else:
 
758
            return None
 
759
        
610
760
 
611
761
    def lookup_revision(self, revno):
612
762
        """Return revision hash for revision number."""
617
767
            # list is 0-based; revisions are 1-based
618
768
            return self.revision_history()[revno-1]
619
769
        except IndexError:
620
 
            bailout("no such revision %s" % revno)
 
770
            raise BzrError("no such revision %s" % revno)
621
771
 
622
772
 
623
773
    def revision_tree(self, revision_id):
625
775
 
626
776
        `revision_id` may be None for the null revision, in which case
627
777
        an `EmptyTree` is returned."""
628
 
 
 
778
        self._need_readlock()
629
779
        if revision_id == None:
630
780
            return EmptyTree()
631
781
        else:
661
811
 
662
812
 
663
813
 
664
 
    def write_log(self, show_timezone='original'):
665
 
        """Write out human-readable log of commits to this branch
666
 
 
667
 
        :param utc: If true, show dates in universal time, not local time."""
668
 
        ## TODO: Option to choose either original, utc or local timezone
669
 
        revno = 1
670
 
        precursor = None
671
 
        for p in self.revision_history():
672
 
            print '-' * 40
673
 
            print 'revno:', revno
674
 
            ## TODO: Show hash if --id is given.
675
 
            ##print 'revision-hash:', p
676
 
            rev = self.get_revision(p)
677
 
            print 'committer:', rev.committer
678
 
            print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0,
679
 
                                                 show_timezone))
680
 
 
681
 
            ## opportunistic consistency check, same as check_patch_chaining
682
 
            if rev.precursor != precursor:
683
 
                bailout("mismatched precursor!")
684
 
 
685
 
            print 'message:'
686
 
            if not rev.message:
687
 
                print '  (no message)'
688
 
            else:
689
 
                for l in rev.message.split('\n'):
690
 
                    print '  ' + l
691
 
 
692
 
            revno += 1
693
 
            precursor = p
694
 
 
695
 
 
696
 
 
697
 
    def show_status(branch, show_all=False):
 
814
    def rename_one(self, from_rel, to_rel):
 
815
        """Rename one file.
 
816
 
 
817
        This can change the directory or the filename or both.
 
818
        """
 
819
        self._need_writelock()
 
820
        tree = self.working_tree()
 
821
        inv = tree.inventory
 
822
        if not tree.has_filename(from_rel):
 
823
            bailout("can't rename: old working file %r does not exist" % from_rel)
 
824
        if tree.has_filename(to_rel):
 
825
            bailout("can't rename: new working file %r already exists" % to_rel)
 
826
            
 
827
        file_id = inv.path2id(from_rel)
 
828
        if file_id == None:
 
829
            bailout("can't rename: old name %r is not versioned" % from_rel)
 
830
 
 
831
        if inv.path2id(to_rel):
 
832
            bailout("can't rename: new name %r is already versioned" % to_rel)
 
833
 
 
834
        to_dir, to_tail = os.path.split(to_rel)
 
835
        to_dir_id = inv.path2id(to_dir)
 
836
        if to_dir_id == None and to_dir != '':
 
837
            bailout("can't determine destination directory id for %r" % to_dir)
 
838
 
 
839
        mutter("rename_one:")
 
840
        mutter("  file_id    {%s}" % file_id)
 
841
        mutter("  from_rel   %r" % from_rel)
 
842
        mutter("  to_rel     %r" % to_rel)
 
843
        mutter("  to_dir     %r" % to_dir)
 
844
        mutter("  to_dir_id  {%s}" % to_dir_id)
 
845
            
 
846
        inv.rename(file_id, to_dir_id, to_tail)
 
847
 
 
848
        print "%s => %s" % (from_rel, to_rel)
 
849
        
 
850
        from_abs = self.abspath(from_rel)
 
851
        to_abs = self.abspath(to_rel)
 
852
        try:
 
853
            os.rename(from_abs, to_abs)
 
854
        except OSError, e:
 
855
            bailout("failed to rename %r to %r: %s"
 
856
                    % (from_abs, to_abs, e[1]),
 
857
                    ["rename rolled back"])
 
858
 
 
859
        self._write_inventory(inv)
 
860
            
 
861
 
 
862
 
 
863
    def move(self, from_paths, to_name):
 
864
        """Rename files.
 
865
 
 
866
        to_name must exist as a versioned directory.
 
867
 
 
868
        If to_name exists and is a directory, the files are moved into
 
869
        it, keeping their old names.  If it is a directory, 
 
870
 
 
871
        Note that to_name is only the last component of the new name;
 
872
        this doesn't change the directory.
 
873
        """
 
874
        self._need_writelock()
 
875
        ## TODO: Option to move IDs only
 
876
        assert not isinstance(from_paths, basestring)
 
877
        tree = self.working_tree()
 
878
        inv = tree.inventory
 
879
        to_abs = self.abspath(to_name)
 
880
        if not isdir(to_abs):
 
881
            bailout("destination %r is not a directory" % to_abs)
 
882
        if not tree.has_filename(to_name):
 
883
            bailout("destination %r not in working directory" % to_abs)
 
884
        to_dir_id = inv.path2id(to_name)
 
885
        if to_dir_id == None and to_name != '':
 
886
            bailout("destination %r is not a versioned directory" % to_name)
 
887
        to_dir_ie = inv[to_dir_id]
 
888
        if to_dir_ie.kind not in ('directory', 'root_directory'):
 
889
            bailout("destination %r is not a directory" % to_abs)
 
890
 
 
891
        to_idpath = Set(inv.get_idpath(to_dir_id))
 
892
 
 
893
        for f in from_paths:
 
894
            if not tree.has_filename(f):
 
895
                bailout("%r does not exist in working tree" % f)
 
896
            f_id = inv.path2id(f)
 
897
            if f_id == None:
 
898
                bailout("%r is not versioned" % f)
 
899
            name_tail = splitpath(f)[-1]
 
900
            dest_path = appendpath(to_name, name_tail)
 
901
            if tree.has_filename(dest_path):
 
902
                bailout("destination %r already exists" % dest_path)
 
903
            if f_id in to_idpath:
 
904
                bailout("can't move %r to a subdirectory of itself" % f)
 
905
 
 
906
        # OK, so there's a race here, it's possible that someone will
 
907
        # create a file in this interval and then the rename might be
 
908
        # left half-done.  But we should have caught most problems.
 
909
 
 
910
        for f in from_paths:
 
911
            name_tail = splitpath(f)[-1]
 
912
            dest_path = appendpath(to_name, name_tail)
 
913
            print "%s => %s" % (f, dest_path)
 
914
            inv.rename(inv.path2id(f), to_dir_id, name_tail)
 
915
            try:
 
916
                os.rename(self.abspath(f), self.abspath(dest_path))
 
917
            except OSError, e:
 
918
                bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
 
919
                        ["rename rolled back"])
 
920
 
 
921
        self._write_inventory(inv)
 
922
 
 
923
 
 
924
 
 
925
    def show_status(self, show_all=False, file_list=None):
698
926
        """Display single-line status for non-ignored working files.
699
927
 
700
928
        The list is show sorted in order by file name.
710
938
        >>> os.unlink(b.abspath('foo'))
711
939
        >>> b.show_status()
712
940
        D       foo
713
 
        
714
 
 
715
 
        :todo: Get state for single files.
716
 
 
717
 
        :todo: Perhaps show a slash at the end of directory names.        
718
 
 
719
941
        """
 
942
        self._need_readlock()
720
943
 
721
944
        # We have to build everything into a list first so that it can
722
945
        # sorted by name, incorporating all the different sources.
727
950
        # Interesting case: the old ID for a file has been removed,
728
951
        # but a new file has been created under that name.
729
952
 
730
 
        old = branch.basis_tree()
731
 
        old_inv = old.inventory
732
 
        new = branch.working_tree()
733
 
        new_inv = new.inventory
734
 
 
735
 
        for fs, fid, oldname, newname, kind in diff_trees(old, new):
 
953
        old = self.basis_tree()
 
954
        new = self.working_tree()
 
955
 
 
956
        items = diff_trees(old, new)
 
957
        # We want to filter out only if any file was provided in the file_list.
 
958
        if isinstance(file_list, list) and len(file_list):
 
959
            items = [item for item in items if item[3] in file_list]
 
960
 
 
961
        for fs, fid, oldname, newname, kind in items:
736
962
            if fs == 'R':
737
963
                show_status(fs, kind,
738
964
                            oldname + ' => ' + newname)
749
975
            elif fs == '?':
750
976
                show_status(fs, kind, newname)
751
977
            else:
752
 
                bailout("wierd file state %r" % ((fs, fid),))
 
978
                bailout("weird file state %r" % ((fs, fid),))
753
979
                
754
980
 
755
981
 
760
986
    >>> isdir(b.base)
761
987
    True
762
988
    >>> bd = b.base
763
 
    >>> del b
 
989
    >>> b.destroy()
764
990
    >>> isdir(bd)
765
991
    False
766
992
    """
767
 
    def __init__(self, files = []):
 
993
    def __init__(self, files=[], dirs=[]):
768
994
        """Make a test branch.
769
995
 
770
996
        This creates a temporary directory and runs init-tree in it.
772
998
        If any files are listed, they are created in the working copy.
773
999
        """
774
1000
        Branch.__init__(self, tempfile.mkdtemp(), init=True)
 
1001
        for d in dirs:
 
1002
            os.mkdir(self.abspath(d))
 
1003
            
775
1004
        for f in files:
776
1005
            file(os.path.join(self.base, f), 'w').write('content of %s' % f)
777
1006
 
778
1007
 
779
1008
    def __del__(self):
 
1009
        self.destroy()
 
1010
 
 
1011
    def destroy(self):
780
1012
        """Destroy the test branch, removing the scratch directory."""
781
 
        shutil.rmtree(self.base)
 
1013
        try:
 
1014
            mutter("delete ScratchBranch %s" % self.base)
 
1015
            shutil.rmtree(self.base)
 
1016
        except OSError, e:
 
1017
            # Work around for shutil.rmtree failing on Windows when
 
1018
            # readonly files are encountered
 
1019
            mutter("hit exception in destroying ScratchBranch: %s" % e)
 
1020
            for root, dirs, files in os.walk(self.base, topdown=False):
 
1021
                for name in files:
 
1022
                    os.chmod(os.path.join(root, name), 0700)
 
1023
            shutil.rmtree(self.base)
 
1024
        self.base = None
782
1025
 
783
1026
    
784
1027
 
817
1060
    idx = name.rfind('/')
818
1061
    if idx != -1:
819
1062
        name = name[idx+1 : ]
 
1063
    idx = name.rfind('\\')
 
1064
    if idx != -1:
 
1065
        name = name[idx+1 : ]
820
1066
 
821
1067
    name = name.lstrip('.')
822
1068
 
823
1069
    s = hexlify(rand_bytes(8))
824
1070
    return '-'.join((name, compact_date(time.time()), s))
825
 
 
826