~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Martin Pool
  • Date: 2005-05-19 09:59:49 UTC
  • Revision ID: mbp@sourcefrog.net-20050519095949-2aaed7613265e594
- More cleanups for set type

- Clean up Inventory cmp method

- Remove the Inventory.id_set and Tree.id_set methods: don't built
  sets when just using the dictionaries will do.

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
 
 
20
18
import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile
21
19
import traceback, socket, fnmatch, difflib, time
22
20
from binascii import hexlify
24
22
import bzrlib
25
23
from inventory import Inventory
26
24
from trace import mutter, note
27
 
from tree import Tree, EmptyTree, RevisionTree, WorkingTree
 
25
from tree import Tree, EmptyTree, RevisionTree
28
26
from inventory import InventoryEntry, Inventory
29
 
from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \
 
27
from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \
30
28
     format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \
31
29
     joinpath, sha_string, file_kind, local_time_offset, appendpath
32
30
from store import ImmutableStore
33
31
from revision import Revision
34
32
from errors import bailout, BzrError
35
33
from textui import show_status
36
 
from diff import diff_trees
37
34
 
38
35
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
39
36
## TODO: Maybe include checks for common corruption of newlines, etc?
40
37
 
41
38
 
42
39
 
 
40
def find_branch(f, **args):
 
41
    if f and (f.startswith('http://') or f.startswith('https://')):
 
42
        import remotebranch 
 
43
        return remotebranch.RemoteBranch(f, **args)
 
44
    else:
 
45
        return Branch(f, **args)
 
46
        
 
47
 
43
48
def find_branch_root(f=None):
44
49
    """Find the branch root enclosing f, or pwd.
45
50
 
 
51
    f may be a filename or a URL.
 
52
 
46
53
    It is not necessary that f exists.
47
54
 
48
55
    Basically we keep looking up until we find the control directory or
53
60
        f = os.path.realpath(f)
54
61
    else:
55
62
        f = os.path.abspath(f)
 
63
    if not os.path.exists(f):
 
64
        raise BzrError('%r does not exist' % f)
 
65
        
56
66
 
57
67
    orig_f = f
58
68
 
73
83
class Branch:
74
84
    """Branch holding a history of revisions.
75
85
 
76
 
    TODO: Perhaps use different stores for different classes of object,
77
 
           so that we can keep track of how much space each one uses,
78
 
           or garbage-collect them.
79
 
 
80
 
    TODO: Add a RemoteBranch subclass.  For the basic case of read-only
81
 
           HTTP access this should be very easy by, 
82
 
           just redirecting controlfile access into HTTP requests.
83
 
           We would need a RemoteStore working similarly.
84
 
 
85
 
    TODO: Keep the on-disk branch locked while the object exists.
86
 
 
87
 
    TODO: mkdir() method.
 
86
    base
 
87
        Base directory of the branch.
88
88
    """
89
 
    def __init__(self, base, init=False, find_root=True):
 
89
    _lockmode = None
 
90
    
 
91
    def __init__(self, base, init=False, find_root=True, lock_mode='w'):
90
92
        """Create new branch object at a particular location.
91
93
 
92
94
        base -- Base directory for the branch.
113
115
                        ['use "bzr init" to initialize a new working tree',
114
116
                         'current bzr can only operate from top-of-tree'])
115
117
        self._check_format()
 
118
        self.lock(lock_mode)
116
119
 
117
120
        self.text_store = ImmutableStore(self.controlfilename('text-store'))
118
121
        self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
126
129
    __repr__ = __str__
127
130
 
128
131
 
 
132
 
 
133
    def lock(self, mode='w'):
 
134
        """Lock the on-disk branch, excluding other processes."""
 
135
        try:
 
136
            import fcntl, errno
 
137
 
 
138
            if mode == 'w':
 
139
                lm = fcntl.LOCK_EX
 
140
                om = os.O_WRONLY | os.O_CREAT
 
141
            elif mode == 'r':
 
142
                lm = fcntl.LOCK_SH
 
143
                om = os.O_RDONLY
 
144
            else:
 
145
                raise BzrError("invalid locking mode %r" % mode)
 
146
 
 
147
            try:
 
148
                lockfile = os.open(self.controlfilename('branch-lock'), om)
 
149
            except OSError, e:
 
150
                if e.errno == errno.ENOENT:
 
151
                    # might not exist on branches from <0.0.4
 
152
                    self.controlfile('branch-lock', 'w').close()
 
153
                    lockfile = os.open(self.controlfilename('branch-lock'), om)
 
154
                else:
 
155
                    raise e
 
156
            
 
157
            fcntl.lockf(lockfile, lm)
 
158
            def unlock():
 
159
                fcntl.lockf(lockfile, fcntl.LOCK_UN)
 
160
                os.close(lockfile)
 
161
                self._lockmode = None
 
162
            self.unlock = unlock
 
163
            self._lockmode = mode
 
164
        except ImportError:
 
165
            warning("please write a locking method for platform %r" % sys.platform)
 
166
            def unlock():
 
167
                self._lockmode = None
 
168
            self.unlock = unlock
 
169
            self._lockmode = mode
 
170
 
 
171
 
 
172
    def _need_readlock(self):
 
173
        if self._lockmode not in ['r', 'w']:
 
174
            raise BzrError('need read lock on branch, only have %r' % self._lockmode)
 
175
 
 
176
    def _need_writelock(self):
 
177
        if self._lockmode not in ['w']:
 
178
            raise BzrError('need write lock on branch, only have %r' % self._lockmode)
 
179
 
 
180
 
129
181
    def abspath(self, name):
130
182
        """Return absolute filename for something in the branch"""
131
183
        return os.path.join(self.base, name)
158
210
        and binary.  binary files are untranslated byte streams.  Text
159
211
        control files are stored with Unix newlines and in UTF-8, even
160
212
        if the platform or locale defaults are different.
 
213
 
 
214
        Controlfiles should almost never be opened in write mode but
 
215
        rather should be atomically copied and replaced using atomicfile.
161
216
        """
162
217
 
163
218
        fn = self.controlfilename(file_or_path)
184
239
        for d in ('text-store', 'inventory-store', 'revision-store'):
185
240
            os.mkdir(self.controlfilename(d))
186
241
        for f in ('revision-history', 'merged-patches',
187
 
                  'pending-merged-patches', 'branch-name'):
 
242
                  'pending-merged-patches', 'branch-name',
 
243
                  'branch-lock'):
188
244
            self.controlfile(f, 'w').write('')
189
245
        mutter('created control directory in ' + self.base)
190
246
        Inventory().write_xml(self.controlfile('inventory','w'))
211
267
 
212
268
    def read_working_inventory(self):
213
269
        """Read the working inventory."""
 
270
        self._need_readlock()
214
271
        before = time.time()
215
272
        # ElementTree does its own conversion from UTF-8, so open in
216
273
        # binary.
226
283
        That is to say, the inventory describing changes underway, that
227
284
        will be committed to the next revision.
228
285
        """
 
286
        self._need_writelock()
229
287
        ## TODO: factor out to atomicfile?  is rename safe on windows?
230
288
        ## TODO: Maybe some kind of clean/dirty marker on inventory?
231
289
        tmpfname = self.controlfilename('inventory.tmp')
243
301
                         """Inventory for the working copy.""")
244
302
 
245
303
 
246
 
    def add(self, files, verbose=False):
 
304
    def add(self, files, verbose=False, ids=None):
247
305
        """Make files versioned.
248
306
 
249
307
        Note that the command line normally calls smart_add instead.
262
320
        TODO: Adding a directory should optionally recurse down and
263
321
               add all non-ignored children.  Perhaps do that in a
264
322
               higher-level method.
265
 
 
266
 
        >>> b = ScratchBranch(files=['foo'])
267
 
        >>> 'foo' in b.unknowns()
268
 
        True
269
 
        >>> b.show_status()
270
 
        ?       foo
271
 
        >>> b.add('foo')
272
 
        >>> 'foo' in b.unknowns()
273
 
        False
274
 
        >>> bool(b.inventory.path2id('foo'))
275
 
        True
276
 
        >>> b.show_status()
277
 
        A       foo
278
 
 
279
 
        >>> b.add('foo')
280
 
        Traceback (most recent call last):
281
 
        ...
282
 
        BzrError: ('foo is already versioned', [])
283
 
 
284
 
        >>> b.add(['nothere'])
285
 
        Traceback (most recent call last):
286
 
        BzrError: ('cannot add: not a regular file or directory: nothere', [])
287
323
        """
 
324
        self._need_writelock()
288
325
 
289
326
        # TODO: Re-adding a file that is removed in the working copy
290
327
        # should probably put it back with the previous ID.
291
328
        if isinstance(files, types.StringTypes):
 
329
            assert(ids is None or isinstance(ids, types.StringTypes))
292
330
            files = [files]
 
331
            if ids is not None:
 
332
                ids = [ids]
 
333
 
 
334
        if ids is None:
 
335
            ids = [None] * len(files)
 
336
        else:
 
337
            assert(len(ids) == len(files))
293
338
        
294
339
        inv = self.read_working_inventory()
295
 
        for f in files:
 
340
        for f,file_id in zip(files, ids):
296
341
            if is_control_file(f):
297
342
                bailout("cannot add control file %s" % quotefn(f))
298
343
 
312
357
            if kind != 'file' and kind != 'directory':
313
358
                bailout('cannot add: not a regular file or directory: %s' % quotefn(f))
314
359
 
315
 
            file_id = gen_file_id(f)
 
360
            if file_id is None:
 
361
                file_id = gen_file_id(f)
316
362
            inv.add_path(f, kind=kind, file_id=file_id)
317
363
 
318
364
            if verbose:
325
371
 
326
372
    def print_file(self, file, revno):
327
373
        """Print `file` to stdout."""
 
374
        self._need_readlock()
328
375
        tree = self.revision_tree(self.lookup_revision(revno))
329
376
        # use inventory as it was in that revision
330
377
        file_id = tree.inventory.path2id(file)
340
387
 
341
388
        TODO: Refuse to remove modified files unless --force is given?
342
389
 
343
 
        >>> b = ScratchBranch(files=['foo'])
344
 
        >>> b.add('foo')
345
 
        >>> b.inventory.has_filename('foo')
346
 
        True
347
 
        >>> b.remove('foo')
348
 
        >>> b.working_tree().has_filename('foo')
349
 
        True
350
 
        >>> b.inventory.has_filename('foo')
351
 
        False
352
 
        
353
 
        >>> b = ScratchBranch(files=['foo'])
354
 
        >>> b.add('foo')
355
 
        >>> b.commit('one')
356
 
        >>> b.remove('foo')
357
 
        >>> b.commit('two')
358
 
        >>> b.inventory.has_filename('foo') 
359
 
        False
360
 
        >>> b.basis_tree().has_filename('foo') 
361
 
        False
362
 
        >>> b.working_tree().has_filename('foo') 
363
 
        True
364
 
 
365
390
        TODO: Do something useful with directories.
366
391
 
367
392
        TODO: Should this remove the text or not?  Tough call; not
371
396
        """
372
397
        ## TODO: Normalize names
373
398
        ## TODO: Remove nested loops; better scalability
 
399
        self._need_writelock()
374
400
 
375
401
        if isinstance(files, types.StringTypes):
376
402
            files = [files]
395
421
 
396
422
        self._write_inventory(inv)
397
423
 
 
424
    def set_inventory(self, new_inventory_list):
 
425
        inv = Inventory()
 
426
        for path, file_id, parent, kind in new_inventory_list:
 
427
            name = os.path.basename(path)
 
428
            if name == "":
 
429
                continue
 
430
            inv.add(InventoryEntry(file_id, name, kind, parent))
 
431
        self._write_inventory(inv)
 
432
 
398
433
 
399
434
    def unknowns(self):
400
435
        """Return all unknown files.
415
450
        return self.working_tree().unknowns()
416
451
 
417
452
 
418
 
    def commit(self, message, timestamp=None, timezone=None,
419
 
               committer=None,
420
 
               verbose=False):
421
 
        """Commit working copy as a new revision.
422
 
        
423
 
        The basic approach is to add all the file texts into the
424
 
        store, then the inventory, then make a new revision pointing
425
 
        to that inventory and store that.
426
 
        
427
 
        This is not quite safe if the working copy changes during the
428
 
        commit; for the moment that is simply not allowed.  A better
429
 
        approach is to make a temporary copy of the files before
430
 
        computing their hashes, and then add those hashes in turn to
431
 
        the inventory.  This should mean at least that there are no
432
 
        broken hash pointers.  There is no way we can get a snapshot
433
 
        of the whole directory at an instant.  This would also have to
434
 
        be robust against files disappearing, moving, etc.  So the
435
 
        whole thing is a bit hard.
436
 
 
437
 
        timestamp -- if not None, seconds-since-epoch for a
438
 
             postdated/predated commit.
439
 
        """
440
 
 
441
 
        ## TODO: Show branch names
442
 
 
443
 
        # TODO: Don't commit if there are no changes, unless forced?
444
 
 
445
 
        # First walk over the working inventory; and both update that
446
 
        # and also build a new revision inventory.  The revision
447
 
        # inventory needs to hold the text-id, sha1 and size of the
448
 
        # actual file versions committed in the revision.  (These are
449
 
        # not present in the working inventory.)  We also need to
450
 
        # detect missing/deleted files, and remove them from the
451
 
        # working inventory.
452
 
 
453
 
        work_inv = self.read_working_inventory()
454
 
        inv = Inventory()
455
 
        basis = self.basis_tree()
456
 
        basis_inv = basis.inventory
457
 
        missing_ids = []
458
 
        for path, entry in work_inv.iter_entries():
459
 
            ## TODO: Cope with files that have gone missing.
460
 
 
461
 
            ## TODO: Check that the file kind has not changed from the previous
462
 
            ## revision of this file (if any).
463
 
 
464
 
            entry = entry.copy()
465
 
 
466
 
            p = self.abspath(path)
467
 
            file_id = entry.file_id
468
 
            mutter('commit prep file %s, id %r ' % (p, file_id))
469
 
 
470
 
            if not os.path.exists(p):
471
 
                mutter("    file is missing, removing from inventory")
472
 
                if verbose:
473
 
                    show_status('D', entry.kind, quotefn(path))
474
 
                missing_ids.append(file_id)
475
 
                continue
476
 
 
477
 
            # TODO: Handle files that have been deleted
478
 
 
479
 
            # TODO: Maybe a special case for empty files?  Seems a
480
 
            # waste to store them many times.
481
 
 
482
 
            inv.add(entry)
483
 
 
484
 
            if basis_inv.has_id(file_id):
485
 
                old_kind = basis_inv[file_id].kind
486
 
                if old_kind != entry.kind:
487
 
                    bailout("entry %r changed kind from %r to %r"
488
 
                            % (file_id, old_kind, entry.kind))
489
 
 
490
 
            if entry.kind == 'directory':
491
 
                if not isdir(p):
492
 
                    bailout("%s is entered as directory but not a directory" % quotefn(p))
493
 
            elif entry.kind == 'file':
494
 
                if not isfile(p):
495
 
                    bailout("%s is entered as file but is not a file" % quotefn(p))
496
 
 
497
 
                content = file(p, 'rb').read()
498
 
 
499
 
                entry.text_sha1 = sha_string(content)
500
 
                entry.text_size = len(content)
501
 
 
502
 
                old_ie = basis_inv.has_id(file_id) and basis_inv[file_id]
503
 
                if (old_ie
504
 
                    and (old_ie.text_size == entry.text_size)
505
 
                    and (old_ie.text_sha1 == entry.text_sha1)):
506
 
                    ## assert content == basis.get_file(file_id).read()
507
 
                    entry.text_id = basis_inv[file_id].text_id
508
 
                    mutter('    unchanged from previous text_id {%s}' %
509
 
                           entry.text_id)
510
 
                    
511
 
                else:
512
 
                    entry.text_id = gen_file_id(entry.name)
513
 
                    self.text_store.add(content, entry.text_id)
514
 
                    mutter('    stored with text_id {%s}' % entry.text_id)
515
 
                    if verbose:
516
 
                        if not old_ie:
517
 
                            state = 'A'
518
 
                        elif (old_ie.name == entry.name
519
 
                              and old_ie.parent_id == entry.parent_id):
520
 
                            state = 'M'
521
 
                        else:
522
 
                            state = 'R'
523
 
 
524
 
                        show_status(state, entry.kind, quotefn(path))
525
 
 
526
 
        for file_id in missing_ids:
527
 
            # have to do this later so we don't mess up the iterator.
528
 
            # since parents may be removed before their children we
529
 
            # have to test.
530
 
 
531
 
            # FIXME: There's probably a better way to do this; perhaps
532
 
            # the workingtree should know how to filter itself.
533
 
            if work_inv.has_id(file_id):
534
 
                del work_inv[file_id]
535
 
 
536
 
 
537
 
        inv_id = rev_id = _gen_revision_id(time.time())
538
 
        
539
 
        inv_tmp = tempfile.TemporaryFile()
540
 
        inv.write_xml(inv_tmp)
541
 
        inv_tmp.seek(0)
542
 
        self.inventory_store.add(inv_tmp, inv_id)
543
 
        mutter('new inventory_id is {%s}' % inv_id)
544
 
 
545
 
        self._write_inventory(work_inv)
546
 
 
547
 
        if timestamp == None:
548
 
            timestamp = time.time()
549
 
 
550
 
        if committer == None:
551
 
            committer = username()
552
 
 
553
 
        if timezone == None:
554
 
            timezone = local_time_offset()
555
 
 
556
 
        mutter("building commit log message")
557
 
        rev = Revision(timestamp=timestamp,
558
 
                       timezone=timezone,
559
 
                       committer=committer,
560
 
                       precursor = self.last_patch(),
561
 
                       message = message,
562
 
                       inventory_id=inv_id,
563
 
                       revision_id=rev_id)
564
 
 
565
 
        rev_tmp = tempfile.TemporaryFile()
566
 
        rev.write_xml(rev_tmp)
567
 
        rev_tmp.seek(0)
568
 
        self.revision_store.add(rev_tmp, rev_id)
569
 
        mutter("new revision_id is {%s}" % rev_id)
570
 
        
571
 
        ## XXX: Everything up to here can simply be orphaned if we abort
572
 
        ## the commit; it will leave junk files behind but that doesn't
573
 
        ## matter.
574
 
 
575
 
        ## TODO: Read back the just-generated changeset, and make sure it
576
 
        ## applies and recreates the right state.
577
 
 
578
 
        ## TODO: Also calculate and store the inventory SHA1
579
 
        mutter("committing patch r%d" % (self.revno() + 1))
580
 
 
581
 
 
582
 
        self.append_revision(rev_id)
583
 
        
584
 
        if verbose:
585
 
            note("commited r%d" % self.revno())
586
 
 
587
 
 
588
453
    def append_revision(self, revision_id):
589
454
        mutter("add {%s} to revision-history" % revision_id)
590
455
        rev_history = self.revision_history()
606
471
 
607
472
    def get_revision(self, revision_id):
608
473
        """Return the Revision object for a named revision"""
 
474
        self._need_readlock()
609
475
        r = Revision.read_xml(self.revision_store[revision_id])
610
476
        assert r.revision_id == revision_id
611
477
        return r
617
483
        TODO: Perhaps for this and similar methods, take a revision
618
484
               parameter which can be either an integer revno or a
619
485
               string hash."""
 
486
        self._need_readlock()
620
487
        i = Inventory.read_xml(self.inventory_store[inventory_id])
621
488
        return i
622
489
 
623
490
 
624
491
    def get_revision_inventory(self, revision_id):
625
492
        """Return inventory of a past revision."""
 
493
        self._need_readlock()
626
494
        if revision_id == None:
627
495
            return Inventory()
628
496
        else:
635
503
        >>> ScratchBranch().revision_history()
636
504
        []
637
505
        """
638
 
        return [chomp(l) for l in self.controlfile('revision-history', 'r').readlines()]
 
506
        self._need_readlock()
 
507
        return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()]
 
508
 
 
509
 
 
510
    def enum_history(self, direction):
 
511
        """Return (revno, revision_id) for history of branch.
 
512
 
 
513
        direction
 
514
            'forward' is from earliest to latest
 
515
            'reverse' is from latest to earliest
 
516
        """
 
517
        rh = self.revision_history()
 
518
        if direction == 'forward':
 
519
            i = 1
 
520
            for rid in rh:
 
521
                yield i, rid
 
522
                i += 1
 
523
        elif direction == 'reverse':
 
524
            i = len(rh)
 
525
            while i > 0:
 
526
                yield i, rh[i-1]
 
527
                i -= 1
 
528
        else:
 
529
            raise ValueError('invalid history direction', direction)
639
530
 
640
531
 
641
532
    def revno(self):
643
534
 
644
535
        That is equivalent to the number of revisions committed to
645
536
        this branch.
646
 
 
647
 
        >>> b = ScratchBranch()
648
 
        >>> b.revno()
649
 
        0
650
 
        >>> b.commit('no foo')
651
 
        >>> b.revno()
652
 
        1
653
537
        """
654
538
        return len(self.revision_history())
655
539
 
656
540
 
657
541
    def last_patch(self):
658
542
        """Return last patch hash, or None if no history.
659
 
 
660
 
        >>> ScratchBranch().last_patch() == None
661
 
        True
662
543
        """
663
544
        ph = self.revision_history()
664
545
        if ph:
665
546
            return ph[-1]
666
547
        else:
667
548
            return None
 
549
 
 
550
 
 
551
    def commit(self, *args, **kw):
 
552
        """Deprecated"""
 
553
        from bzrlib.commit import commit
 
554
        commit(self, *args, **kw)
668
555
        
669
556
 
670
557
    def lookup_revision(self, revno):
684
571
 
685
572
        `revision_id` may be None for the null revision, in which case
686
573
        an `EmptyTree` is returned."""
687
 
 
 
574
        # TODO: refactor this to use an existing revision object
 
575
        # so we don't need to read it in twice.
 
576
        self._need_readlock()
688
577
        if revision_id == None:
689
578
            return EmptyTree()
690
579
        else:
694
583
 
695
584
    def working_tree(self):
696
585
        """Return a `Tree` for the working copy."""
 
586
        from workingtree import WorkingTree
697
587
        return WorkingTree(self.base, self.read_working_inventory())
698
588
 
699
589
 
701
591
        """Return `Tree` object for last revision.
702
592
 
703
593
        If there are no revisions yet, return an `EmptyTree`.
704
 
 
705
 
        >>> b = ScratchBranch(files=['foo'])
706
 
        >>> b.basis_tree().has_filename('foo')
707
 
        False
708
 
        >>> b.working_tree().has_filename('foo')
709
 
        True
710
 
        >>> b.add('foo')
711
 
        >>> b.commit('add foo')
712
 
        >>> b.basis_tree().has_filename('foo')
713
 
        True
714
594
        """
715
595
        r = self.last_patch()
716
596
        if r == None:
720
600
 
721
601
 
722
602
 
723
 
    def write_log(self, show_timezone='original', verbose=False):
724
 
        """Write out human-readable log of commits to this branch
725
 
 
726
 
        utc -- If true, show dates in universal time, not local time."""
727
 
        ## TODO: Option to choose either original, utc or local timezone
728
 
        revno = 1
729
 
        precursor = None
730
 
        for p in self.revision_history():
731
 
            print '-' * 40
732
 
            print 'revno:', revno
733
 
            ## TODO: Show hash if --id is given.
734
 
            ##print 'revision-hash:', p
735
 
            rev = self.get_revision(p)
736
 
            print 'committer:', rev.committer
737
 
            print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0,
738
 
                                                 show_timezone))
739
 
 
740
 
            ## opportunistic consistency check, same as check_patch_chaining
741
 
            if rev.precursor != precursor:
742
 
                bailout("mismatched precursor!")
743
 
 
744
 
            print 'message:'
745
 
            if not rev.message:
746
 
                print '  (no message)'
747
 
            else:
748
 
                for l in rev.message.split('\n'):
749
 
                    print '  ' + l
750
 
 
751
 
            if verbose == True and precursor != None:
752
 
                print 'changed files:'
753
 
                tree = self.revision_tree(p)
754
 
                prevtree = self.revision_tree(precursor)
755
 
                
756
 
                for file_state, fid, old_name, new_name, kind in \
757
 
                                        diff_trees(prevtree, tree, ):
758
 
                    if file_state == 'A' or file_state == 'M':
759
 
                        show_status(file_state, kind, new_name)
760
 
                    elif file_state == 'D':
761
 
                        show_status(file_state, kind, old_name)
762
 
                    elif file_state == 'R':
763
 
                        show_status(file_state, kind,
764
 
                            old_name + ' => ' + new_name)
765
 
                
766
 
            revno += 1
767
 
            precursor = p
768
 
 
769
 
 
770
603
    def rename_one(self, from_rel, to_rel):
 
604
        """Rename one file.
 
605
 
 
606
        This can change the directory or the filename or both.
 
607
        """
 
608
        self._need_writelock()
771
609
        tree = self.working_tree()
772
610
        inv = tree.inventory
773
611
        if not tree.has_filename(from_rel):
822
660
        Note that to_name is only the last component of the new name;
823
661
        this doesn't change the directory.
824
662
        """
 
663
        self._need_writelock()
825
664
        ## TODO: Option to move IDs only
826
665
        assert not isinstance(from_paths, basestring)
827
666
        tree = self.working_tree()
838
677
        if to_dir_ie.kind not in ('directory', 'root_directory'):
839
678
            bailout("destination %r is not a directory" % to_abs)
840
679
 
841
 
        to_idpath = Set(inv.get_idpath(to_dir_id))
 
680
        to_idpath = inv.get_idpath(to_dir_id)
842
681
 
843
682
        for f in from_paths:
844
683
            if not tree.has_filename(f):
872
711
 
873
712
 
874
713
 
875
 
    def show_status(self, show_all=False):
876
 
        """Display single-line status for non-ignored working files.
877
 
 
878
 
        The list is show sorted in order by file name.
879
 
 
880
 
        >>> b = ScratchBranch(files=['foo', 'foo~'])
881
 
        >>> b.show_status()
882
 
        ?       foo
883
 
        >>> b.add('foo')
884
 
        >>> b.show_status()
885
 
        A       foo
886
 
        >>> b.commit("add foo")
887
 
        >>> b.show_status()
888
 
        >>> os.unlink(b.abspath('foo'))
889
 
        >>> b.show_status()
890
 
        D       foo
891
 
        
892
 
        TODO: Get state for single files.
893
 
        """
894
 
 
895
 
        # We have to build everything into a list first so that it can
896
 
        # sorted by name, incorporating all the different sources.
897
 
 
898
 
        # FIXME: Rather than getting things in random order and then sorting,
899
 
        # just step through in order.
900
 
 
901
 
        # Interesting case: the old ID for a file has been removed,
902
 
        # but a new file has been created under that name.
903
 
 
904
 
        old = self.basis_tree()
905
 
        new = self.working_tree()
906
 
 
907
 
        for fs, fid, oldname, newname, kind in diff_trees(old, new):
908
 
            if fs == 'R':
909
 
                show_status(fs, kind,
910
 
                            oldname + ' => ' + newname)
911
 
            elif fs == 'A' or fs == 'M':
912
 
                show_status(fs, kind, newname)
913
 
            elif fs == 'D':
914
 
                show_status(fs, kind, oldname)
915
 
            elif fs == '.':
916
 
                if show_all:
917
 
                    show_status(fs, kind, newname)
918
 
            elif fs == 'I':
919
 
                if show_all:
920
 
                    show_status(fs, kind, newname)
921
 
            elif fs == '?':
922
 
                show_status(fs, kind, newname)
923
 
            else:
924
 
                bailout("weird file state %r" % ((fs, fid),))
925
 
                
926
 
 
927
714
 
928
715
class ScratchBranch(Branch):
929
716
    """Special test class: a branch that cleans up after itself.
932
719
    >>> isdir(b.base)
933
720
    True
934
721
    >>> bd = b.base
935
 
    >>> del b
 
722
    >>> b.destroy()
936
723
    >>> isdir(bd)
937
724
    False
938
725
    """
952
739
 
953
740
 
954
741
    def __del__(self):
 
742
        self.destroy()
 
743
 
 
744
    def destroy(self):
955
745
        """Destroy the test branch, removing the scratch directory."""
956
746
        try:
 
747
            mutter("delete ScratchBranch %s" % self.base)
957
748
            shutil.rmtree(self.base)
958
 
        except OSError:
 
749
        except OSError, e:
959
750
            # Work around for shutil.rmtree failing on Windows when
960
751
            # readonly files are encountered
 
752
            mutter("hit exception in destroying ScratchBranch: %s" % e)
961
753
            for root, dirs, files in os.walk(self.base, topdown=False):
962
754
                for name in files:
963
755
                    os.chmod(os.path.join(root, name), 0700)
964
756
            shutil.rmtree(self.base)
 
757
        self.base = None
965
758
 
966
759
    
967
760
 
984
777
 
985
778
 
986
779
 
987
 
def _gen_revision_id(when):
988
 
    """Return new revision-id."""
989
 
    s = '%s-%s-' % (user_email(), compact_date(when))
990
 
    s += hexlify(rand_bytes(8))
991
 
    return s
992
 
 
993
 
 
994
780
def gen_file_id(name):
995
781
    """Return new file id.
996
782
 
997
783
    This should probably generate proper UUIDs, but for the moment we
998
784
    cope with just randomness because running uuidgen every time is
999
785
    slow."""
 
786
    import re
 
787
 
 
788
    # get last component
1000
789
    idx = name.rfind('/')
1001
790
    if idx != -1:
1002
791
        name = name[idx+1 : ]
1004
793
    if idx != -1:
1005
794
        name = name[idx+1 : ]
1006
795
 
 
796
    # make it not a hidden file
1007
797
    name = name.lstrip('.')
1008
798
 
 
799
    # remove any wierd characters; we don't escape them but rather
 
800
    # just pull them out
 
801
    name = re.sub(r'[^\w.]', '', name)
 
802
 
1009
803
    s = hexlify(rand_bytes(8))
1010
804
    return '-'.join((name, compact_date(time.time()), s))