~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Martin Pool
  • Date: 2005-05-30 03:05:03 UTC
  • Revision ID: mbp@sourcefrog.net-20050530030503-8fd9959e40c31eeb
todo

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
 
from errors import bailout, BzrError
 
32
from errors import 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
 
 
48
 
 
49
def with_writelock(method):
 
50
    """Method decorator for functions run with the branch locked."""
 
51
    def d(self, *a, **k):
 
52
        # called with self set to the branch
 
53
        self.lock('w')
 
54
        try:
 
55
            return method(self, *a, **k)
 
56
        finally:
 
57
            self.unlock()
 
58
    return d
 
59
 
 
60
 
 
61
def with_readlock(method):
 
62
    def d(self, *a, **k):
 
63
        self.lock('r')
 
64
        try:
 
65
            return method(self, *a, **k)
 
66
        finally:
 
67
            self.unlock()
 
68
    return d
 
69
        
 
70
 
43
71
def find_branch_root(f=None):
44
72
    """Find the branch root enclosing f, or pwd.
45
73
 
 
74
    f may be a filename or a URL.
 
75
 
46
76
    It is not necessary that f exists.
47
77
 
48
78
    Basically we keep looking up until we find the control directory or
53
83
        f = os.path.realpath(f)
54
84
    else:
55
85
        f = os.path.abspath(f)
 
86
    if not os.path.exists(f):
 
87
        raise BzrError('%r does not exist' % f)
 
88
        
56
89
 
57
90
    orig_f = f
58
91
 
70
103
######################################################################
71
104
# branch objects
72
105
 
73
 
class Branch:
 
106
class Branch(object):
74
107
    """Branch holding a history of revisions.
75
108
 
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.
 
109
    base
 
110
        Base directory of the branch.
 
111
 
 
112
    _lock_mode
 
113
        None, or 'r' or 'w'
 
114
 
 
115
    _lock_count
 
116
        If _lock_mode is true, a positive count of the number of times the
 
117
        lock has been taken.
 
118
 
 
119
    _lockfile
 
120
        Open file used for locking.
88
121
    """
 
122
    base = None
 
123
    _lock_mode = None
 
124
    _lock_count = None
 
125
    
89
126
    def __init__(self, base, init=False, find_root=True):
90
127
        """Create new branch object at a particular location.
91
128
 
109
146
        else:
110
147
            self.base = os.path.realpath(base)
111
148
            if not isdir(self.controlfilename('.')):
112
 
                bailout("not a bzr branch: %s" % quotefn(base),
113
 
                        ['use "bzr init" to initialize a new working tree',
114
 
                         'current bzr can only operate from top-of-tree'])
 
149
                from errors import NotBranchError
 
150
                raise NotBranchError("not a bzr branch: %s" % quotefn(base),
 
151
                                     ['use "bzr init" to initialize a new working tree',
 
152
                                      'current bzr can only operate from top-of-tree'])
115
153
        self._check_format()
 
154
        self._lockfile = self.controlfile('branch-lock', 'wb')
116
155
 
117
156
        self.text_store = ImmutableStore(self.controlfilename('text-store'))
118
157
        self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
126
165
    __repr__ = __str__
127
166
 
128
167
 
 
168
    def __del__(self):
 
169
        if self._lock_mode:
 
170
            from warnings import warn
 
171
            warn("branch %r was not explicitly unlocked" % self)
 
172
            self.unlock()
 
173
 
 
174
 
 
175
    def lock(self, mode):
 
176
        if self._lock_mode:
 
177
            if mode == 'w' and cur_lm == 'r':
 
178
                raise BzrError("can't upgrade to a write lock")
 
179
            
 
180
            assert self._lock_count >= 1
 
181
            self._lock_count += 1
 
182
        else:
 
183
            from bzrlib.lock import lock, LOCK_SH, LOCK_EX
 
184
            if mode == 'r':
 
185
                m = LOCK_SH
 
186
            elif mode == 'w':
 
187
                m = LOCK_EX
 
188
            else:
 
189
                raise ValueError('invalid lock mode %r' % mode)
 
190
 
 
191
            lock(self._lockfile, m)
 
192
            self._lock_mode = mode
 
193
            self._lock_count = 1
 
194
 
 
195
 
 
196
    def unlock(self):
 
197
        if not self._lock_mode:
 
198
            raise BzrError('branch %r is not locked' % (self))
 
199
 
 
200
        if self._lock_count > 1:
 
201
            self._lock_count -= 1
 
202
        else:
 
203
            assert self._lock_count == 1
 
204
            from bzrlib.lock import unlock
 
205
            unlock(self._lockfile)
 
206
            self._lock_mode = self._lock_count = None
 
207
 
 
208
 
129
209
    def abspath(self, name):
130
210
        """Return absolute filename for something in the branch"""
131
211
        return os.path.join(self.base, name)
138
218
        rp = os.path.realpath(path)
139
219
        # FIXME: windows
140
220
        if not rp.startswith(self.base):
141
 
            bailout("path %r is not within branch %r" % (rp, self.base))
 
221
            from errors import NotBranchError
 
222
            raise NotBranchError("path %r is not within branch %r" % (rp, self.base))
142
223
        rp = rp[len(self.base):]
143
224
        rp = rp.lstrip(os.sep)
144
225
        return rp
158
239
        and binary.  binary files are untranslated byte streams.  Text
159
240
        control files are stored with Unix newlines and in UTF-8, even
160
241
        if the platform or locale defaults are different.
 
242
 
 
243
        Controlfiles should almost never be opened in write mode but
 
244
        rather should be atomically copied and replaced using atomicfile.
161
245
        """
162
246
 
163
247
        fn = self.controlfilename(file_or_path)
165
249
        if mode == 'rb' or mode == 'wb':
166
250
            return file(fn, mode)
167
251
        elif mode == 'r' or mode == 'w':
168
 
            # open in binary mode anyhow so there's no newline translation
 
252
            # open in binary mode anyhow so there's no newline translation;
 
253
            # codecs uses line buffering by default; don't want that.
169
254
            import codecs
170
 
            return codecs.open(fn, mode + 'b', 'utf-8')
 
255
            return codecs.open(fn, mode + 'b', 'utf-8',
 
256
                               buffering=60000)
171
257
        else:
172
258
            raise BzrError("invalid controlfile mode %r" % mode)
173
259
 
182
268
        for d in ('text-store', 'inventory-store', 'revision-store'):
183
269
            os.mkdir(self.controlfilename(d))
184
270
        for f in ('revision-history', 'merged-patches',
185
 
                  'pending-merged-patches', 'branch-name'):
 
271
                  'pending-merged-patches', 'branch-name',
 
272
                  'branch-lock'):
186
273
            self.controlfile(f, 'w').write('')
187
274
        mutter('created control directory in ' + self.base)
188
275
        Inventory().write_xml(self.controlfile('inventory','w'))
202
289
        fmt = self.controlfile('branch-format', 'r').read()
203
290
        fmt.replace('\r\n', '')
204
291
        if fmt != BZR_BRANCH_FORMAT:
205
 
            bailout('sorry, branch format %r not supported' % fmt,
206
 
                    ['use a different bzr version',
207
 
                     'or remove the .bzr directory and "bzr init" again'])
208
 
 
209
 
 
 
292
            raise BzrError('sorry, branch format %r not supported' % fmt,
 
293
                           ['use a different bzr version',
 
294
                            'or remove the .bzr directory and "bzr init" again'])
 
295
 
 
296
 
 
297
 
 
298
    @with_readlock
210
299
    def read_working_inventory(self):
211
300
        """Read the working inventory."""
212
301
        before = time.time()
216
305
        mutter("loaded inventory of %d items in %f"
217
306
               % (len(inv), time.time() - before))
218
307
        return inv
219
 
 
 
308
            
220
309
 
221
310
    def _write_inventory(self, inv):
222
311
        """Update the working inventory.
235
324
            os.remove(inv_fname)
236
325
        os.rename(tmpfname, inv_fname)
237
326
        mutter('wrote working inventory')
238
 
 
 
327
            
239
328
 
240
329
    inventory = property(read_working_inventory, _write_inventory, None,
241
330
                         """Inventory for the working copy.""")
242
331
 
243
332
 
244
 
    def add(self, files, verbose=False):
 
333
    @with_writelock
 
334
    def add(self, files, verbose=False, ids=None):
245
335
        """Make files versioned.
246
336
 
247
337
        Note that the command line normally calls smart_add instead.
260
350
        TODO: Adding a directory should optionally recurse down and
261
351
               add all non-ignored children.  Perhaps do that in a
262
352
               higher-level method.
263
 
 
264
 
        >>> b = ScratchBranch(files=['foo'])
265
 
        >>> 'foo' in b.unknowns()
266
 
        True
267
 
        >>> b.show_status()
268
 
        ?       foo
269
 
        >>> b.add('foo')
270
 
        >>> 'foo' in b.unknowns()
271
 
        False
272
 
        >>> bool(b.inventory.path2id('foo'))
273
 
        True
274
 
        >>> b.show_status()
275
 
        A       foo
276
 
 
277
 
        >>> b.add('foo')
278
 
        Traceback (most recent call last):
279
 
        ...
280
 
        BzrError: ('foo is already versioned', [])
281
 
 
282
 
        >>> b.add(['nothere'])
283
 
        Traceback (most recent call last):
284
 
        BzrError: ('cannot add: not a regular file or directory: nothere', [])
285
353
        """
286
 
 
287
354
        # TODO: Re-adding a file that is removed in the working copy
288
355
        # should probably put it back with the previous ID.
289
356
        if isinstance(files, types.StringTypes):
 
357
            assert(ids is None or isinstance(ids, types.StringTypes))
290
358
            files = [files]
291
 
        
 
359
            if ids is not None:
 
360
                ids = [ids]
 
361
 
 
362
        if ids is None:
 
363
            ids = [None] * len(files)
 
364
        else:
 
365
            assert(len(ids) == len(files))
 
366
 
292
367
        inv = self.read_working_inventory()
293
 
        for f in files:
 
368
        for f,file_id in zip(files, ids):
294
369
            if is_control_file(f):
295
 
                bailout("cannot add control file %s" % quotefn(f))
 
370
                raise BzrError("cannot add control file %s" % quotefn(f))
296
371
 
297
372
            fp = splitpath(f)
298
373
 
299
374
            if len(fp) == 0:
300
 
                bailout("cannot add top-level %r" % f)
301
 
                
 
375
                raise BzrError("cannot add top-level %r" % f)
 
376
 
302
377
            fullpath = os.path.normpath(self.abspath(f))
303
378
 
304
379
            try:
305
380
                kind = file_kind(fullpath)
306
381
            except OSError:
307
382
                # maybe something better?
308
 
                bailout('cannot add: not a regular file or directory: %s' % quotefn(f))
309
 
            
 
383
                raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
 
384
 
310
385
            if kind != 'file' and kind != 'directory':
311
 
                bailout('cannot add: not a regular file or directory: %s' % quotefn(f))
 
386
                raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
312
387
 
313
 
            file_id = gen_file_id(f)
 
388
            if file_id is None:
 
389
                file_id = gen_file_id(f)
314
390
            inv.add_path(f, kind=kind, file_id=file_id)
315
391
 
316
392
            if verbose:
317
393
                show_status('A', kind, quotefn(f))
318
 
                
 
394
 
319
395
            mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
 
396
 
 
397
        self._write_inventory(inv)
320
398
            
321
 
        self._write_inventory(inv)
322
 
 
323
399
 
324
400
    def print_file(self, file, revno):
325
401
        """Print `file` to stdout."""
327
403
        # use inventory as it was in that revision
328
404
        file_id = tree.inventory.path2id(file)
329
405
        if not file_id:
330
 
            bailout("%r is not present in revision %d" % (file, revno))
 
406
            raise BzrError("%r is not present in revision %d" % (file, revno))
331
407
        tree.print_file(file_id)
332
 
        
333
 
 
 
408
 
 
409
 
 
410
    @with_writelock
334
411
    def remove(self, files, verbose=False):
335
412
        """Mark nominated files for removal from the inventory.
336
413
 
338
415
 
339
416
        TODO: Refuse to remove modified files unless --force is given?
340
417
 
341
 
        >>> b = ScratchBranch(files=['foo'])
342
 
        >>> b.add('foo')
343
 
        >>> b.inventory.has_filename('foo')
344
 
        True
345
 
        >>> b.remove('foo')
346
 
        >>> b.working_tree().has_filename('foo')
347
 
        True
348
 
        >>> b.inventory.has_filename('foo')
349
 
        False
350
 
        
351
 
        >>> b = ScratchBranch(files=['foo'])
352
 
        >>> b.add('foo')
353
 
        >>> b.commit('one')
354
 
        >>> b.remove('foo')
355
 
        >>> b.commit('two')
356
 
        >>> b.inventory.has_filename('foo') 
357
 
        False
358
 
        >>> b.basis_tree().has_filename('foo') 
359
 
        False
360
 
        >>> b.working_tree().has_filename('foo') 
361
 
        True
362
 
 
363
418
        TODO: Do something useful with directories.
364
419
 
365
420
        TODO: Should this remove the text or not?  Tough call; not
369
424
        """
370
425
        ## TODO: Normalize names
371
426
        ## TODO: Remove nested loops; better scalability
372
 
 
373
427
        if isinstance(files, types.StringTypes):
374
428
            files = [files]
375
 
        
 
429
 
376
430
        tree = self.working_tree()
377
431
        inv = tree.inventory
378
432
 
380
434
        for f in files:
381
435
            fid = inv.path2id(f)
382
436
            if not fid:
383
 
                bailout("cannot remove unversioned file %s" % quotefn(f))
 
437
                raise BzrError("cannot remove unversioned file %s" % quotefn(f))
384
438
            mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
385
439
            if verbose:
386
440
                # having remove it, it must be either ignored or unknown
394
448
        self._write_inventory(inv)
395
449
 
396
450
 
 
451
    def set_inventory(self, new_inventory_list):
 
452
        inv = Inventory()
 
453
        for path, file_id, parent, kind in new_inventory_list:
 
454
            name = os.path.basename(path)
 
455
            if name == "":
 
456
                continue
 
457
            inv.add(InventoryEntry(file_id, name, kind, parent))
 
458
        self._write_inventory(inv)
 
459
 
 
460
 
397
461
    def unknowns(self):
398
462
        """Return all unknown files.
399
463
 
413
477
        return self.working_tree().unknowns()
414
478
 
415
479
 
416
 
    def commit(self, message, timestamp=None, timezone=None,
417
 
               committer=None,
418
 
               verbose=False):
419
 
        """Commit working copy as a new revision.
420
 
        
421
 
        The basic approach is to add all the file texts into the
422
 
        store, then the inventory, then make a new revision pointing
423
 
        to that inventory and store that.
424
 
        
425
 
        This is not quite safe if the working copy changes during the
426
 
        commit; for the moment that is simply not allowed.  A better
427
 
        approach is to make a temporary copy of the files before
428
 
        computing their hashes, and then add those hashes in turn to
429
 
        the inventory.  This should mean at least that there are no
430
 
        broken hash pointers.  There is no way we can get a snapshot
431
 
        of the whole directory at an instant.  This would also have to
432
 
        be robust against files disappearing, moving, etc.  So the
433
 
        whole thing is a bit hard.
434
 
 
435
 
        timestamp -- if not None, seconds-since-epoch for a
436
 
             postdated/predated commit.
437
 
        """
438
 
 
439
 
        ## TODO: Show branch names
440
 
 
441
 
        # TODO: Don't commit if there are no changes, unless forced?
442
 
 
443
 
        # First walk over the working inventory; and both update that
444
 
        # and also build a new revision inventory.  The revision
445
 
        # inventory needs to hold the text-id, sha1 and size of the
446
 
        # actual file versions committed in the revision.  (These are
447
 
        # not present in the working inventory.)  We also need to
448
 
        # detect missing/deleted files, and remove them from the
449
 
        # working inventory.
450
 
 
451
 
        work_inv = self.read_working_inventory()
452
 
        inv = Inventory()
453
 
        basis = self.basis_tree()
454
 
        basis_inv = basis.inventory
455
 
        missing_ids = []
456
 
        for path, entry in work_inv.iter_entries():
457
 
            ## TODO: Cope with files that have gone missing.
458
 
 
459
 
            ## TODO: Check that the file kind has not changed from the previous
460
 
            ## revision of this file (if any).
461
 
 
462
 
            entry = entry.copy()
463
 
 
464
 
            p = self.abspath(path)
465
 
            file_id = entry.file_id
466
 
            mutter('commit prep file %s, id %r ' % (p, file_id))
467
 
 
468
 
            if not os.path.exists(p):
469
 
                mutter("    file is missing, removing from inventory")
470
 
                if verbose:
471
 
                    show_status('D', entry.kind, quotefn(path))
472
 
                missing_ids.append(file_id)
473
 
                continue
474
 
 
475
 
            # TODO: Handle files that have been deleted
476
 
 
477
 
            # TODO: Maybe a special case for empty files?  Seems a
478
 
            # waste to store them many times.
479
 
 
480
 
            inv.add(entry)
481
 
 
482
 
            if basis_inv.has_id(file_id):
483
 
                old_kind = basis_inv[file_id].kind
484
 
                if old_kind != entry.kind:
485
 
                    bailout("entry %r changed kind from %r to %r"
486
 
                            % (file_id, old_kind, entry.kind))
487
 
 
488
 
            if entry.kind == 'directory':
489
 
                if not isdir(p):
490
 
                    bailout("%s is entered as directory but not a directory" % quotefn(p))
491
 
            elif entry.kind == 'file':
492
 
                if not isfile(p):
493
 
                    bailout("%s is entered as file but is not a file" % quotefn(p))
494
 
 
495
 
                content = file(p, 'rb').read()
496
 
 
497
 
                entry.text_sha1 = sha_string(content)
498
 
                entry.text_size = len(content)
499
 
 
500
 
                old_ie = basis_inv.has_id(file_id) and basis_inv[file_id]
501
 
                if (old_ie
502
 
                    and (old_ie.text_size == entry.text_size)
503
 
                    and (old_ie.text_sha1 == entry.text_sha1)):
504
 
                    ## assert content == basis.get_file(file_id).read()
505
 
                    entry.text_id = basis_inv[file_id].text_id
506
 
                    mutter('    unchanged from previous text_id {%s}' %
507
 
                           entry.text_id)
508
 
                    
509
 
                else:
510
 
                    entry.text_id = gen_file_id(entry.name)
511
 
                    self.text_store.add(content, entry.text_id)
512
 
                    mutter('    stored with text_id {%s}' % entry.text_id)
513
 
                    if verbose:
514
 
                        if not old_ie:
515
 
                            state = 'A'
516
 
                        elif (old_ie.name == entry.name
517
 
                              and old_ie.parent_id == entry.parent_id):
518
 
                            state = 'M'
519
 
                        else:
520
 
                            state = 'R'
521
 
 
522
 
                        show_status(state, entry.kind, quotefn(path))
523
 
 
524
 
        for file_id in missing_ids:
525
 
            # have to do this later so we don't mess up the iterator.
526
 
            # since parents may be removed before their children we
527
 
            # have to test.
528
 
 
529
 
            # FIXME: There's probably a better way to do this; perhaps
530
 
            # the workingtree should know how to filter itself.
531
 
            if work_inv.has_id(file_id):
532
 
                del work_inv[file_id]
533
 
 
534
 
 
535
 
        inv_id = rev_id = _gen_revision_id(time.time())
536
 
        
537
 
        inv_tmp = tempfile.TemporaryFile()
538
 
        inv.write_xml(inv_tmp)
539
 
        inv_tmp.seek(0)
540
 
        self.inventory_store.add(inv_tmp, inv_id)
541
 
        mutter('new inventory_id is {%s}' % inv_id)
542
 
 
543
 
        self._write_inventory(work_inv)
544
 
 
545
 
        if timestamp == None:
546
 
            timestamp = time.time()
547
 
 
548
 
        if committer == None:
549
 
            committer = username()
550
 
 
551
 
        if timezone == None:
552
 
            timezone = local_time_offset()
553
 
 
554
 
        mutter("building commit log message")
555
 
        rev = Revision(timestamp=timestamp,
556
 
                       timezone=timezone,
557
 
                       committer=committer,
558
 
                       precursor = self.last_patch(),
559
 
                       message = message,
560
 
                       inventory_id=inv_id,
561
 
                       revision_id=rev_id)
562
 
 
563
 
        rev_tmp = tempfile.TemporaryFile()
564
 
        rev.write_xml(rev_tmp)
565
 
        rev_tmp.seek(0)
566
 
        self.revision_store.add(rev_tmp, rev_id)
567
 
        mutter("new revision_id is {%s}" % rev_id)
568
 
        
569
 
        ## XXX: Everything up to here can simply be orphaned if we abort
570
 
        ## the commit; it will leave junk files behind but that doesn't
571
 
        ## matter.
572
 
 
573
 
        ## TODO: Read back the just-generated changeset, and make sure it
574
 
        ## applies and recreates the right state.
575
 
 
576
 
        ## TODO: Also calculate and store the inventory SHA1
577
 
        mutter("committing patch r%d" % (self.revno() + 1))
578
 
 
579
 
 
580
 
        self.append_revision(rev_id)
581
 
        
582
 
        if verbose:
583
 
            note("commited r%d" % self.revno())
584
 
 
585
 
 
586
480
    def append_revision(self, revision_id):
587
481
        mutter("add {%s} to revision-history" % revision_id)
588
482
        rev_history = self.revision_history()
627
521
            return self.get_inventory(self.get_revision(revision_id).inventory_id)
628
522
 
629
523
 
 
524
    @with_readlock
630
525
    def revision_history(self):
631
526
        """Return sequence of revision hashes on to this branch.
632
527
 
633
528
        >>> ScratchBranch().revision_history()
634
529
        []
635
530
        """
636
 
        return [chomp(l) for l in self.controlfile('revision-history', 'r').readlines()]
 
531
        return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()]
 
532
 
 
533
 
 
534
    def enum_history(self, direction):
 
535
        """Return (revno, revision_id) for history of branch.
 
536
 
 
537
        direction
 
538
            'forward' is from earliest to latest
 
539
            'reverse' is from latest to earliest
 
540
        """
 
541
        rh = self.revision_history()
 
542
        if direction == 'forward':
 
543
            i = 1
 
544
            for rid in rh:
 
545
                yield i, rid
 
546
                i += 1
 
547
        elif direction == 'reverse':
 
548
            i = len(rh)
 
549
            while i > 0:
 
550
                yield i, rh[i-1]
 
551
                i -= 1
 
552
        else:
 
553
            raise ValueError('invalid history direction', direction)
637
554
 
638
555
 
639
556
    def revno(self):
641
558
 
642
559
        That is equivalent to the number of revisions committed to
643
560
        this branch.
644
 
 
645
 
        >>> b = ScratchBranch()
646
 
        >>> b.revno()
647
 
        0
648
 
        >>> b.commit('no foo')
649
 
        >>> b.revno()
650
 
        1
651
561
        """
652
562
        return len(self.revision_history())
653
563
 
654
564
 
655
565
    def last_patch(self):
656
566
        """Return last patch hash, or None if no history.
657
 
 
658
 
        >>> ScratchBranch().last_patch() == None
659
 
        True
660
567
        """
661
568
        ph = self.revision_history()
662
569
        if ph:
663
570
            return ph[-1]
664
571
        else:
665
572
            return None
 
573
 
 
574
 
 
575
    def commit(self, *args, **kw):
 
576
        """Deprecated"""
 
577
        from bzrlib.commit import commit
 
578
        commit(self, *args, **kw)
666
579
        
667
580
 
668
581
    def lookup_revision(self, revno):
682
595
 
683
596
        `revision_id` may be None for the null revision, in which case
684
597
        an `EmptyTree` is returned."""
685
 
 
 
598
        # TODO: refactor this to use an existing revision object
 
599
        # so we don't need to read it in twice.
686
600
        if revision_id == None:
687
601
            return EmptyTree()
688
602
        else:
692
606
 
693
607
    def working_tree(self):
694
608
        """Return a `Tree` for the working copy."""
 
609
        from workingtree import WorkingTree
695
610
        return WorkingTree(self.base, self.read_working_inventory())
696
611
 
697
612
 
699
614
        """Return `Tree` object for last revision.
700
615
 
701
616
        If there are no revisions yet, return an `EmptyTree`.
702
 
 
703
 
        >>> b = ScratchBranch(files=['foo'])
704
 
        >>> b.basis_tree().has_filename('foo')
705
 
        False
706
 
        >>> b.working_tree().has_filename('foo')
707
 
        True
708
 
        >>> b.add('foo')
709
 
        >>> b.commit('add foo')
710
 
        >>> b.basis_tree().has_filename('foo')
711
 
        True
712
617
        """
713
618
        r = self.last_patch()
714
619
        if r == None:
718
623
 
719
624
 
720
625
 
721
 
    def write_log(self, show_timezone='original', verbose=False):
722
 
        """Write out human-readable log of commits to this branch
723
 
 
724
 
        utc -- If true, show dates in universal time, not local time."""
725
 
        ## TODO: Option to choose either original, utc or local timezone
726
 
        revno = 1
727
 
        precursor = None
728
 
        for p in self.revision_history():
729
 
            print '-' * 40
730
 
            print 'revno:', revno
731
 
            ## TODO: Show hash if --id is given.
732
 
            ##print 'revision-hash:', p
733
 
            rev = self.get_revision(p)
734
 
            print 'committer:', rev.committer
735
 
            print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0,
736
 
                                                 show_timezone))
737
 
 
738
 
            ## opportunistic consistency check, same as check_patch_chaining
739
 
            if rev.precursor != precursor:
740
 
                bailout("mismatched precursor!")
741
 
 
742
 
            print 'message:'
743
 
            if not rev.message:
744
 
                print '  (no message)'
745
 
            else:
746
 
                for l in rev.message.split('\n'):
747
 
                    print '  ' + l
748
 
 
749
 
            if verbose == True and precursor != None:
750
 
                print 'changed files:'
751
 
                tree = self.revision_tree(p)
752
 
                prevtree = self.revision_tree(precursor)
753
 
                
754
 
                for file_state, fid, old_name, new_name, kind in \
755
 
                                        diff_trees(prevtree, tree, ):
756
 
                    if file_state == 'A' or file_state == 'M':
757
 
                        show_status(file_state, kind, new_name)
758
 
                    elif file_state == 'D':
759
 
                        show_status(file_state, kind, old_name)
760
 
                    elif file_state == 'R':
761
 
                        show_status(file_state, kind,
762
 
                            old_name + ' => ' + new_name)
763
 
                
764
 
            revno += 1
765
 
            precursor = p
766
 
 
767
 
 
 
626
    @with_writelock
768
627
    def rename_one(self, from_rel, to_rel):
 
628
        """Rename one file.
 
629
 
 
630
        This can change the directory or the filename or both.
 
631
        """
769
632
        tree = self.working_tree()
770
633
        inv = tree.inventory
771
634
        if not tree.has_filename(from_rel):
772
 
            bailout("can't rename: old working file %r does not exist" % from_rel)
 
635
            raise BzrError("can't rename: old working file %r does not exist" % from_rel)
773
636
        if tree.has_filename(to_rel):
774
 
            bailout("can't rename: new working file %r already exists" % to_rel)
775
 
            
 
637
            raise BzrError("can't rename: new working file %r already exists" % to_rel)
 
638
 
776
639
        file_id = inv.path2id(from_rel)
777
640
        if file_id == None:
778
 
            bailout("can't rename: old name %r is not versioned" % from_rel)
 
641
            raise BzrError("can't rename: old name %r is not versioned" % from_rel)
779
642
 
780
643
        if inv.path2id(to_rel):
781
 
            bailout("can't rename: new name %r is already versioned" % to_rel)
 
644
            raise BzrError("can't rename: new name %r is already versioned" % to_rel)
782
645
 
783
646
        to_dir, to_tail = os.path.split(to_rel)
784
647
        to_dir_id = inv.path2id(to_dir)
785
648
        if to_dir_id == None and to_dir != '':
786
 
            bailout("can't determine destination directory id for %r" % to_dir)
 
649
            raise BzrError("can't determine destination directory id for %r" % to_dir)
787
650
 
788
651
        mutter("rename_one:")
789
652
        mutter("  file_id    {%s}" % file_id)
791
654
        mutter("  to_rel     %r" % to_rel)
792
655
        mutter("  to_dir     %r" % to_dir)
793
656
        mutter("  to_dir_id  {%s}" % to_dir_id)
794
 
            
 
657
 
795
658
        inv.rename(file_id, to_dir_id, to_tail)
796
659
 
797
660
        print "%s => %s" % (from_rel, to_rel)
798
 
        
 
661
 
799
662
        from_abs = self.abspath(from_rel)
800
663
        to_abs = self.abspath(to_rel)
801
664
        try:
802
665
            os.rename(from_abs, to_abs)
803
666
        except OSError, e:
804
 
            bailout("failed to rename %r to %r: %s"
 
667
            raise BzrError("failed to rename %r to %r: %s"
805
668
                    % (from_abs, to_abs, e[1]),
806
669
                    ["rename rolled back"])
807
670
 
808
671
        self._write_inventory(inv)
809
 
            
810
 
 
811
 
 
 
672
 
 
673
 
 
674
 
 
675
    @with_writelock
812
676
    def move(self, from_paths, to_name):
813
677
        """Rename files.
814
678
 
826
690
        inv = tree.inventory
827
691
        to_abs = self.abspath(to_name)
828
692
        if not isdir(to_abs):
829
 
            bailout("destination %r is not a directory" % to_abs)
 
693
            raise BzrError("destination %r is not a directory" % to_abs)
830
694
        if not tree.has_filename(to_name):
831
 
            bailout("destination %r not in working directory" % to_abs)
 
695
            raise BzrError("destination %r not in working directory" % to_abs)
832
696
        to_dir_id = inv.path2id(to_name)
833
697
        if to_dir_id == None and to_name != '':
834
 
            bailout("destination %r is not a versioned directory" % to_name)
 
698
            raise BzrError("destination %r is not a versioned directory" % to_name)
835
699
        to_dir_ie = inv[to_dir_id]
836
700
        if to_dir_ie.kind not in ('directory', 'root_directory'):
837
 
            bailout("destination %r is not a directory" % to_abs)
 
701
            raise BzrError("destination %r is not a directory" % to_abs)
838
702
 
839
 
        to_idpath = Set(inv.get_idpath(to_dir_id))
 
703
        to_idpath = inv.get_idpath(to_dir_id)
840
704
 
841
705
        for f in from_paths:
842
706
            if not tree.has_filename(f):
843
 
                bailout("%r does not exist in working tree" % f)
 
707
                raise BzrError("%r does not exist in working tree" % f)
844
708
            f_id = inv.path2id(f)
845
709
            if f_id == None:
846
 
                bailout("%r is not versioned" % f)
 
710
                raise BzrError("%r is not versioned" % f)
847
711
            name_tail = splitpath(f)[-1]
848
712
            dest_path = appendpath(to_name, name_tail)
849
713
            if tree.has_filename(dest_path):
850
 
                bailout("destination %r already exists" % dest_path)
 
714
                raise BzrError("destination %r already exists" % dest_path)
851
715
            if f_id in to_idpath:
852
 
                bailout("can't move %r to a subdirectory of itself" % f)
 
716
                raise BzrError("can't move %r to a subdirectory of itself" % f)
853
717
 
854
718
        # OK, so there's a race here, it's possible that someone will
855
719
        # create a file in this interval and then the rename might be
863
727
            try:
864
728
                os.rename(self.abspath(f), self.abspath(dest_path))
865
729
            except OSError, e:
866
 
                bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
 
730
                raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
867
731
                        ["rename rolled back"])
868
732
 
869
733
        self._write_inventory(inv)
870
734
 
871
735
 
872
736
 
873
 
    def show_status(self, show_all=False):
874
 
        """Display single-line status for non-ignored working files.
875
 
 
876
 
        The list is show sorted in order by file name.
877
 
 
878
 
        >>> b = ScratchBranch(files=['foo', 'foo~'])
879
 
        >>> b.show_status()
880
 
        ?       foo
881
 
        >>> b.add('foo')
882
 
        >>> b.show_status()
883
 
        A       foo
884
 
        >>> b.commit("add foo")
885
 
        >>> b.show_status()
886
 
        >>> os.unlink(b.abspath('foo'))
887
 
        >>> b.show_status()
888
 
        D       foo
889
 
        
890
 
 
891
 
        TODO: Get state for single files.
892
 
 
893
 
        TODO: Perhaps show a slash at the end of directory names.        
894
 
 
895
 
        """
896
 
 
897
 
        # We have to build everything into a list first so that it can
898
 
        # sorted by name, incorporating all the different sources.
899
 
 
900
 
        # FIXME: Rather than getting things in random order and then sorting,
901
 
        # just step through in order.
902
 
 
903
 
        # Interesting case: the old ID for a file has been removed,
904
 
        # but a new file has been created under that name.
905
 
 
906
 
        old = self.basis_tree()
907
 
        new = self.working_tree()
908
 
 
909
 
        for fs, fid, oldname, newname, kind in diff_trees(old, new):
910
 
            if fs == 'R':
911
 
                show_status(fs, kind,
912
 
                            oldname + ' => ' + newname)
913
 
            elif fs == 'A' or fs == 'M':
914
 
                show_status(fs, kind, newname)
915
 
            elif fs == 'D':
916
 
                show_status(fs, kind, oldname)
917
 
            elif fs == '.':
918
 
                if show_all:
919
 
                    show_status(fs, kind, newname)
920
 
            elif fs == 'I':
921
 
                if show_all:
922
 
                    show_status(fs, kind, newname)
923
 
            elif fs == '?':
924
 
                show_status(fs, kind, newname)
925
 
            else:
926
 
                bailout("weird file state %r" % ((fs, fid),))
927
 
                
928
 
 
929
737
 
930
738
class ScratchBranch(Branch):
931
739
    """Special test class: a branch that cleans up after itself.
934
742
    >>> isdir(b.base)
935
743
    True
936
744
    >>> bd = b.base
937
 
    >>> del b
 
745
    >>> b.destroy()
938
746
    >>> isdir(bd)
939
747
    False
940
748
    """
954
762
 
955
763
 
956
764
    def __del__(self):
 
765
        self.destroy()
 
766
 
 
767
    def destroy(self):
957
768
        """Destroy the test branch, removing the scratch directory."""
958
769
        try:
 
770
            mutter("delete ScratchBranch %s" % self.base)
959
771
            shutil.rmtree(self.base)
960
 
        except OSError:
 
772
        except OSError, e:
961
773
            # Work around for shutil.rmtree failing on Windows when
962
774
            # readonly files are encountered
 
775
            mutter("hit exception in destroying ScratchBranch: %s" % e)
963
776
            for root, dirs, files in os.walk(self.base, topdown=False):
964
777
                for name in files:
965
778
                    os.chmod(os.path.join(root, name), 0700)
966
779
            shutil.rmtree(self.base)
 
780
        self.base = None
967
781
 
968
782
    
969
783
 
986
800
 
987
801
 
988
802
 
989
 
def _gen_revision_id(when):
990
 
    """Return new revision-id."""
991
 
    s = '%s-%s-' % (user_email(), compact_date(when))
992
 
    s += hexlify(rand_bytes(8))
993
 
    return s
994
 
 
995
 
 
996
803
def gen_file_id(name):
997
804
    """Return new file id.
998
805
 
999
806
    This should probably generate proper UUIDs, but for the moment we
1000
807
    cope with just randomness because running uuidgen every time is
1001
808
    slow."""
 
809
    import re
 
810
 
 
811
    # get last component
1002
812
    idx = name.rfind('/')
1003
813
    if idx != -1:
1004
814
        name = name[idx+1 : ]
 
815
    idx = name.rfind('\\')
 
816
    if idx != -1:
 
817
        name = name[idx+1 : ]
1005
818
 
 
819
    # make it not a hidden file
1006
820
    name = name.lstrip('.')
1007
821
 
 
822
    # remove any wierd characters; we don't escape them but rather
 
823
    # just pull them out
 
824
    name = re.sub(r'[^\w.]', '', name)
 
825
 
1008
826
    s = hexlify(rand_bytes(8))
1009
827
    return '-'.join((name, compact_date(time.time()), s))