/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Lalo Martins
  • Date: 2005-09-08 00:40:15 UTC
  • mto: (1185.1.22)
  • mto: This revision was merged to the branch mainline in revision 1390.
  • Revision ID: lalo@exoweb.net-20050908004014-bb63b3378ac8ff58
turned get_revision_info into a RevisionSpec class

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005 Canonical Ltd
 
2
 
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
 
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
 
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
 
 
18
import sys
 
19
import os
 
20
 
 
21
import bzrlib
 
22
from bzrlib.trace import mutter, note
 
23
from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, \
 
24
     splitpath, \
 
25
     sha_file, appendpath, file_kind
 
26
 
 
27
from bzrlib.errors import BzrError, InvalidRevisionNumber, InvalidRevisionId, \
 
28
     DivergedBranches, NotBranchError
 
29
from bzrlib.textui import show_status
 
30
from bzrlib.revision import Revision
 
31
from bzrlib.delta import compare_trees
 
32
from bzrlib.tree import EmptyTree, RevisionTree
 
33
import bzrlib.xml
 
34
import bzrlib.ui
 
35
 
 
36
 
 
37
 
 
38
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
 
39
## TODO: Maybe include checks for common corruption of newlines, etc?
 
40
 
 
41
 
 
42
# TODO: Some operations like log might retrieve the same revisions
 
43
# repeatedly to calculate deltas.  We could perhaps have a weakref
 
44
# cache in memory to make this faster.
 
45
 
 
46
# TODO: please move the revision-string syntax stuff out of the branch
 
47
# object; it's clutter
 
48
 
 
49
 
 
50
def find_branch(f, **args):
 
51
    if f and (f.startswith('http://') or f.startswith('https://')):
 
52
        from bzrlib.remotebranch import RemoteBranch
 
53
        return RemoteBranch(f, **args)
 
54
    else:
 
55
        return LocalBranch(f, **args)
 
56
 
 
57
 
 
58
def find_cached_branch(f, cache_root, **args):
 
59
    from bzrlib.remotebranch import RemoteBranch
 
60
    br = find_branch(f, **args)
 
61
    def cacheify(br, store_name):
 
62
        from bzrlib.meta_store import CachedStore
 
63
        cache_path = os.path.join(cache_root, store_name)
 
64
        os.mkdir(cache_path)
 
65
        new_store = CachedStore(getattr(br, store_name), cache_path)
 
66
        setattr(br, store_name, new_store)
 
67
 
 
68
    if isinstance(br, RemoteBranch):
 
69
        cacheify(br, 'inventory_store')
 
70
        cacheify(br, 'text_store')
 
71
        cacheify(br, 'revision_store')
 
72
    return br
 
73
 
 
74
 
 
75
def _relpath(base, path):
 
76
    """Return path relative to base, or raise exception.
 
77
 
 
78
    The path may be either an absolute path or a path relative to the
 
79
    current working directory.
 
80
 
 
81
    Lifted out of Branch.relpath for ease of testing.
 
82
 
 
83
    os.path.commonprefix (python2.4) has a bad bug that it works just
 
84
    on string prefixes, assuming that '/u' is a prefix of '/u2'.  This
 
85
    avoids that problem."""
 
86
    rp = os.path.abspath(path)
 
87
 
 
88
    s = []
 
89
    head = rp
 
90
    while len(head) >= len(base):
 
91
        if head == base:
 
92
            break
 
93
        head, tail = os.path.split(head)
 
94
        if tail:
 
95
            s.insert(0, tail)
 
96
    else:
 
97
        raise NotBranchError("path %r is not within branch %r" % (rp, base))
 
98
 
 
99
    return os.sep.join(s)
 
100
        
 
101
 
 
102
def find_branch_root(f=None):
 
103
    """Find the branch root enclosing f, or pwd.
 
104
 
 
105
    f may be a filename or a URL.
 
106
 
 
107
    It is not necessary that f exists.
 
108
 
 
109
    Basically we keep looking up until we find the control directory or
 
110
    run into the root.  If there isn't one, raises NotBranchError.
 
111
    """
 
112
    if f == None:
 
113
        f = os.getcwd()
 
114
    elif hasattr(os.path, 'realpath'):
 
115
        f = os.path.realpath(f)
 
116
    else:
 
117
        f = os.path.abspath(f)
 
118
    if not os.path.exists(f):
 
119
        raise BzrError('%r does not exist' % f)
 
120
        
 
121
 
 
122
    orig_f = f
 
123
 
 
124
    while True:
 
125
        if os.path.exists(os.path.join(f, bzrlib.BZRDIR)):
 
126
            return f
 
127
        head, tail = os.path.split(f)
 
128
        if head == f:
 
129
            # reached the root, whatever that may be
 
130
            raise NotBranchError('%s is not in a branch' % orig_f)
 
131
        f = head
 
132
 
 
133
 
 
134
 
 
135
 
 
136
######################################################################
 
137
# branch objects
 
138
 
 
139
class Branch(object):
 
140
    """Branch holding a history of revisions.
 
141
 
 
142
    base
 
143
        Base directory/url of the branch.
 
144
    """
 
145
    base = None
 
146
 
 
147
    def __new__(cls, *a, **kw):
 
148
        """this is temporary, till we get rid of all code that does
 
149
        b = Branch()
 
150
        """
 
151
        # XXX: AAARGH!  MY EYES!  UUUUGLY!!!
 
152
        if cls == Branch:
 
153
            cls = LocalBranch
 
154
        b = object.__new__(cls)
 
155
        return b
 
156
 
 
157
 
 
158
class LocalBranch(Branch):
 
159
    """A branch stored in the actual filesystem.
 
160
 
 
161
    Note that it's "local" in the context of the filesystem; it doesn't
 
162
    really matter if it's on an nfs/smb/afs/coda/... share, as long as
 
163
    it's writable, and can be accessed via the normal filesystem API.
 
164
 
 
165
    _lock_mode
 
166
        None, or 'r' or 'w'
 
167
 
 
168
    _lock_count
 
169
        If _lock_mode is true, a positive count of the number of times the
 
170
        lock has been taken.
 
171
 
 
172
    _lock
 
173
        Lock object from bzrlib.lock.
 
174
    """
 
175
    # We actually expect this class to be somewhat short-lived; part of its
 
176
    # purpose is to try to isolate what bits of the branch logic are tied to
 
177
    # filesystem access, so that in a later step, we can extricate them to
 
178
    # a separarte ("storage") class.
 
179
    _lock_mode = None
 
180
    _lock_count = None
 
181
    _lock = None
 
182
 
 
183
    def __init__(self, base, init=False, find_root=True):
 
184
        """Create new branch object at a particular location.
 
185
 
 
186
        base -- Base directory for the branch.
 
187
        
 
188
        init -- If True, create new control files in a previously
 
189
             unversioned directory.  If False, the branch must already
 
190
             be versioned.
 
191
 
 
192
        find_root -- If true and init is false, find the root of the
 
193
             existing branch containing base.
 
194
 
 
195
        In the test suite, creation of new trees is tested using the
 
196
        `ScratchBranch` class.
 
197
        """
 
198
        from bzrlib.store import ImmutableStore
 
199
        if init:
 
200
            self.base = os.path.realpath(base)
 
201
            self._make_control()
 
202
        elif find_root:
 
203
            self.base = find_branch_root(base)
 
204
        else:
 
205
            self.base = os.path.realpath(base)
 
206
            if not isdir(self.controlfilename('.')):
 
207
                raise NotBranchError("not a bzr branch: %s" % quotefn(base),
 
208
                                     ['use "bzr init" to initialize a new working tree',
 
209
                                      'current bzr can only operate from top-of-tree'])
 
210
        self._check_format()
 
211
 
 
212
        self.text_store = ImmutableStore(self.controlfilename('text-store'))
 
213
        self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
 
214
        self.inventory_store = ImmutableStore(self.controlfilename('inventory-store'))
 
215
 
 
216
 
 
217
    def __str__(self):
 
218
        return '%s(%r)' % (self.__class__.__name__, self.base)
 
219
 
 
220
 
 
221
    __repr__ = __str__
 
222
 
 
223
 
 
224
    def __del__(self):
 
225
        if self._lock_mode or self._lock:
 
226
            from bzrlib.warnings import warn
 
227
            warn("branch %r was not explicitly unlocked" % self)
 
228
            self._lock.unlock()
 
229
 
 
230
 
 
231
    def lock_write(self):
 
232
        if self._lock_mode:
 
233
            if self._lock_mode != 'w':
 
234
                from bzrlib.errors import LockError
 
235
                raise LockError("can't upgrade to a write lock from %r" %
 
236
                                self._lock_mode)
 
237
            self._lock_count += 1
 
238
        else:
 
239
            from bzrlib.lock import WriteLock
 
240
 
 
241
            self._lock = WriteLock(self.controlfilename('branch-lock'))
 
242
            self._lock_mode = 'w'
 
243
            self._lock_count = 1
 
244
 
 
245
 
 
246
    def lock_read(self):
 
247
        if self._lock_mode:
 
248
            assert self._lock_mode in ('r', 'w'), \
 
249
                   "invalid lock mode %r" % self._lock_mode
 
250
            self._lock_count += 1
 
251
        else:
 
252
            from bzrlib.lock import ReadLock
 
253
 
 
254
            self._lock = ReadLock(self.controlfilename('branch-lock'))
 
255
            self._lock_mode = 'r'
 
256
            self._lock_count = 1
 
257
                        
 
258
    def unlock(self):
 
259
        if not self._lock_mode:
 
260
            from bzrlib.errors import LockError
 
261
            raise LockError('branch %r is not locked' % (self))
 
262
 
 
263
        if self._lock_count > 1:
 
264
            self._lock_count -= 1
 
265
        else:
 
266
            self._lock.unlock()
 
267
            self._lock = None
 
268
            self._lock_mode = self._lock_count = None
 
269
 
 
270
    def abspath(self, name):
 
271
        """Return absolute filename for something in the branch"""
 
272
        return os.path.join(self.base, name)
 
273
 
 
274
    def relpath(self, path):
 
275
        """Return path relative to this branch of something inside it.
 
276
 
 
277
        Raises an error if path is not in this branch."""
 
278
        return _relpath(self.base, path)
 
279
 
 
280
    def controlfilename(self, file_or_path):
 
281
        """Return location relative to branch."""
 
282
        if isinstance(file_or_path, basestring):
 
283
            file_or_path = [file_or_path]
 
284
        return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
 
285
 
 
286
 
 
287
    def controlfile(self, file_or_path, mode='r'):
 
288
        """Open a control file for this branch.
 
289
 
 
290
        There are two classes of file in the control directory: text
 
291
        and binary.  binary files are untranslated byte streams.  Text
 
292
        control files are stored with Unix newlines and in UTF-8, even
 
293
        if the platform or locale defaults are different.
 
294
 
 
295
        Controlfiles should almost never be opened in write mode but
 
296
        rather should be atomically copied and replaced using atomicfile.
 
297
        """
 
298
 
 
299
        fn = self.controlfilename(file_or_path)
 
300
 
 
301
        if mode == 'rb' or mode == 'wb':
 
302
            return file(fn, mode)
 
303
        elif mode == 'r' or mode == 'w':
 
304
            # open in binary mode anyhow so there's no newline translation;
 
305
            # codecs uses line buffering by default; don't want that.
 
306
            import codecs
 
307
            return codecs.open(fn, mode + 'b', 'utf-8',
 
308
                               buffering=60000)
 
309
        else:
 
310
            raise BzrError("invalid controlfile mode %r" % mode)
 
311
 
 
312
    def _make_control(self):
 
313
        from bzrlib.inventory import Inventory
 
314
        
 
315
        os.mkdir(self.controlfilename([]))
 
316
        self.controlfile('README', 'w').write(
 
317
            "This is a Bazaar-NG control directory.\n"
 
318
            "Do not change any files in this directory.\n")
 
319
        self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT)
 
320
        for d in ('text-store', 'inventory-store', 'revision-store'):
 
321
            os.mkdir(self.controlfilename(d))
 
322
        for f in ('revision-history', 'merged-patches',
 
323
                  'pending-merged-patches', 'branch-name',
 
324
                  'branch-lock',
 
325
                  'pending-merges'):
 
326
            self.controlfile(f, 'w').write('')
 
327
        mutter('created control directory in ' + self.base)
 
328
 
 
329
        # if we want per-tree root ids then this is the place to set
 
330
        # them; they're not needed for now and so ommitted for
 
331
        # simplicity.
 
332
        f = self.controlfile('inventory','w')
 
333
        bzrlib.xml.serializer_v4.write_inventory(Inventory(), f)
 
334
 
 
335
 
 
336
    def _check_format(self):
 
337
        """Check this branch format is supported.
 
338
 
 
339
        The current tool only supports the current unstable format.
 
340
 
 
341
        In the future, we might need different in-memory Branch
 
342
        classes to support downlevel branches.  But not yet.
 
343
        """
 
344
        # This ignores newlines so that we can open branches created
 
345
        # on Windows from Linux and so on.  I think it might be better
 
346
        # to always make all internal files in unix format.
 
347
        fmt = self.controlfile('branch-format', 'r').read()
 
348
        fmt = fmt.replace('\r\n', '\n')
 
349
        if fmt != BZR_BRANCH_FORMAT:
 
350
            raise BzrError('sorry, branch format %r not supported' % fmt,
 
351
                           ['use a different bzr version',
 
352
                            'or remove the .bzr directory and "bzr init" again'])
 
353
 
 
354
    def get_root_id(self):
 
355
        """Return the id of this branches root"""
 
356
        inv = self.read_working_inventory()
 
357
        return inv.root.file_id
 
358
 
 
359
    def set_root_id(self, file_id):
 
360
        inv = self.read_working_inventory()
 
361
        orig_root_id = inv.root.file_id
 
362
        del inv._byid[inv.root.file_id]
 
363
        inv.root.file_id = file_id
 
364
        inv._byid[inv.root.file_id] = inv.root
 
365
        for fid in inv:
 
366
            entry = inv[fid]
 
367
            if entry.parent_id in (None, orig_root_id):
 
368
                entry.parent_id = inv.root.file_id
 
369
        self._write_inventory(inv)
 
370
 
 
371
    def read_working_inventory(self):
 
372
        """Read the working inventory."""
 
373
        from bzrlib.inventory import Inventory
 
374
        self.lock_read()
 
375
        try:
 
376
            # ElementTree does its own conversion from UTF-8, so open in
 
377
            # binary.
 
378
            f = self.controlfile('inventory', 'rb')
 
379
            return bzrlib.xml.serializer_v4.read_inventory(f)
 
380
        finally:
 
381
            self.unlock()
 
382
            
 
383
 
 
384
    def _write_inventory(self, inv):
 
385
        """Update the working inventory.
 
386
 
 
387
        That is to say, the inventory describing changes underway, that
 
388
        will be committed to the next revision.
 
389
        """
 
390
        from bzrlib.atomicfile import AtomicFile
 
391
        
 
392
        self.lock_write()
 
393
        try:
 
394
            f = AtomicFile(self.controlfilename('inventory'), 'wb')
 
395
            try:
 
396
                bzrlib.xml.serializer_v4.write_inventory(inv, f)
 
397
                f.commit()
 
398
            finally:
 
399
                f.close()
 
400
        finally:
 
401
            self.unlock()
 
402
        
 
403
        mutter('wrote working inventory')
 
404
            
 
405
 
 
406
    inventory = property(read_working_inventory, _write_inventory, None,
 
407
                         """Inventory for the working copy.""")
 
408
 
 
409
 
 
410
    def add(self, files, ids=None):
 
411
        """Make files versioned.
 
412
 
 
413
        Note that the command line normally calls smart_add instead,
 
414
        which can automatically recurse.
 
415
 
 
416
        This puts the files in the Added state, so that they will be
 
417
        recorded by the next commit.
 
418
 
 
419
        files
 
420
            List of paths to add, relative to the base of the tree.
 
421
 
 
422
        ids
 
423
            If set, use these instead of automatically generated ids.
 
424
            Must be the same length as the list of files, but may
 
425
            contain None for ids that are to be autogenerated.
 
426
 
 
427
        TODO: Perhaps have an option to add the ids even if the files do
 
428
              not (yet) exist.
 
429
 
 
430
        TODO: Perhaps yield the ids and paths as they're added.
 
431
        """
 
432
        # TODO: Re-adding a file that is removed in the working copy
 
433
        # should probably put it back with the previous ID.
 
434
        if isinstance(files, basestring):
 
435
            assert(ids is None or isinstance(ids, basestring))
 
436
            files = [files]
 
437
            if ids is not None:
 
438
                ids = [ids]
 
439
 
 
440
        if ids is None:
 
441
            ids = [None] * len(files)
 
442
        else:
 
443
            assert(len(ids) == len(files))
 
444
 
 
445
        self.lock_write()
 
446
        try:
 
447
            inv = self.read_working_inventory()
 
448
            for f,file_id in zip(files, ids):
 
449
                if is_control_file(f):
 
450
                    raise BzrError("cannot add control file %s" % quotefn(f))
 
451
 
 
452
                fp = splitpath(f)
 
453
 
 
454
                if len(fp) == 0:
 
455
                    raise BzrError("cannot add top-level %r" % f)
 
456
 
 
457
                fullpath = os.path.normpath(self.abspath(f))
 
458
 
 
459
                try:
 
460
                    kind = file_kind(fullpath)
 
461
                except OSError:
 
462
                    # maybe something better?
 
463
                    raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
 
464
 
 
465
                if kind != 'file' and kind != 'directory':
 
466
                    raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
 
467
 
 
468
                if file_id is None:
 
469
                    file_id = gen_file_id(f)
 
470
                inv.add_path(f, kind=kind, file_id=file_id)
 
471
 
 
472
                mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
 
473
 
 
474
            self._write_inventory(inv)
 
475
        finally:
 
476
            self.unlock()
 
477
            
 
478
 
 
479
    def print_file(self, file, revno):
 
480
        """Print `file` to stdout."""
 
481
        self.lock_read()
 
482
        try:
 
483
            tree = self.revision_tree(self.get_rev_id(revno))
 
484
            # use inventory as it was in that revision
 
485
            file_id = tree.inventory.path2id(file)
 
486
            if not file_id:
 
487
                raise BzrError("%r is not present in revision %s" % (file, revno))
 
488
            tree.print_file(file_id)
 
489
        finally:
 
490
            self.unlock()
 
491
 
 
492
 
 
493
    def remove(self, files, verbose=False):
 
494
        """Mark nominated files for removal from the inventory.
 
495
 
 
496
        This does not remove their text.  This does not run on 
 
497
 
 
498
        TODO: Refuse to remove modified files unless --force is given?
 
499
 
 
500
        TODO: Do something useful with directories.
 
501
 
 
502
        TODO: Should this remove the text or not?  Tough call; not
 
503
        removing may be useful and the user can just use use rm, and
 
504
        is the opposite of add.  Removing it is consistent with most
 
505
        other tools.  Maybe an option.
 
506
        """
 
507
        ## TODO: Normalize names
 
508
        ## TODO: Remove nested loops; better scalability
 
509
        if isinstance(files, basestring):
 
510
            files = [files]
 
511
 
 
512
        self.lock_write()
 
513
 
 
514
        try:
 
515
            tree = self.working_tree()
 
516
            inv = tree.inventory
 
517
 
 
518
            # do this before any modifications
 
519
            for f in files:
 
520
                fid = inv.path2id(f)
 
521
                if not fid:
 
522
                    raise BzrError("cannot remove unversioned file %s" % quotefn(f))
 
523
                mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
 
524
                if verbose:
 
525
                    # having remove it, it must be either ignored or unknown
 
526
                    if tree.is_ignored(f):
 
527
                        new_status = 'I'
 
528
                    else:
 
529
                        new_status = '?'
 
530
                    show_status(new_status, inv[fid].kind, quotefn(f))
 
531
                del inv[fid]
 
532
 
 
533
            self._write_inventory(inv)
 
534
        finally:
 
535
            self.unlock()
 
536
 
 
537
 
 
538
    # FIXME: this doesn't need to be a branch method
 
539
    def set_inventory(self, new_inventory_list):
 
540
        from bzrlib.inventory import Inventory, InventoryEntry
 
541
        inv = Inventory(self.get_root_id())
 
542
        for path, file_id, parent, kind in new_inventory_list:
 
543
            name = os.path.basename(path)
 
544
            if name == "":
 
545
                continue
 
546
            inv.add(InventoryEntry(file_id, name, kind, parent))
 
547
        self._write_inventory(inv)
 
548
 
 
549
 
 
550
    def unknowns(self):
 
551
        """Return all unknown files.
 
552
 
 
553
        These are files in the working directory that are not versioned or
 
554
        control files or ignored.
 
555
        
 
556
        >>> b = ScratchBranch(files=['foo', 'foo~'])
 
557
        >>> list(b.unknowns())
 
558
        ['foo']
 
559
        >>> b.add('foo')
 
560
        >>> list(b.unknowns())
 
561
        []
 
562
        >>> b.remove('foo')
 
563
        >>> list(b.unknowns())
 
564
        ['foo']
 
565
        """
 
566
        return self.working_tree().unknowns()
 
567
 
 
568
 
 
569
    def append_revision(self, *revision_ids):
 
570
        from bzrlib.atomicfile import AtomicFile
 
571
 
 
572
        for revision_id in revision_ids:
 
573
            mutter("add {%s} to revision-history" % revision_id)
 
574
 
 
575
        rev_history = self.revision_history()
 
576
        rev_history.extend(revision_ids)
 
577
 
 
578
        f = AtomicFile(self.controlfilename('revision-history'))
 
579
        try:
 
580
            for rev_id in rev_history:
 
581
                print >>f, rev_id
 
582
            f.commit()
 
583
        finally:
 
584
            f.close()
 
585
 
 
586
 
 
587
    def get_revision_xml_file(self, revision_id):
 
588
        """Return XML file object for revision object."""
 
589
        if not revision_id or not isinstance(revision_id, basestring):
 
590
            raise InvalidRevisionId(revision_id)
 
591
 
 
592
        self.lock_read()
 
593
        try:
 
594
            try:
 
595
                return self.revision_store[revision_id]
 
596
            except IndexError:
 
597
                raise bzrlib.errors.NoSuchRevision(self, revision_id)
 
598
        finally:
 
599
            self.unlock()
 
600
 
 
601
 
 
602
    #deprecated
 
603
    get_revision_xml = get_revision_xml_file
 
604
 
 
605
 
 
606
    def get_revision(self, revision_id):
 
607
        """Return the Revision object for a named revision"""
 
608
        xml_file = self.get_revision_xml_file(revision_id)
 
609
 
 
610
        try:
 
611
            r = bzrlib.xml.serializer_v4.read_revision(xml_file)
 
612
        except SyntaxError, e:
 
613
            raise bzrlib.errors.BzrError('failed to unpack revision_xml',
 
614
                                         [revision_id,
 
615
                                          str(e)])
 
616
            
 
617
        assert r.revision_id == revision_id
 
618
        return r
 
619
 
 
620
 
 
621
    def get_revision_delta(self, revno):
 
622
        """Return the delta for one revision.
 
623
 
 
624
        The delta is relative to its mainline predecessor, or the
 
625
        empty tree for revision 1.
 
626
        """
 
627
        assert isinstance(revno, int)
 
628
        rh = self.revision_history()
 
629
        if not (1 <= revno <= len(rh)):
 
630
            raise InvalidRevisionNumber(revno)
 
631
 
 
632
        # revno is 1-based; list is 0-based
 
633
 
 
634
        new_tree = self.revision_tree(rh[revno-1])
 
635
        if revno == 1:
 
636
            old_tree = EmptyTree()
 
637
        else:
 
638
            old_tree = self.revision_tree(rh[revno-2])
 
639
 
 
640
        return compare_trees(old_tree, new_tree)
 
641
 
 
642
        
 
643
 
 
644
    def get_revision_sha1(self, revision_id):
 
645
        """Hash the stored value of a revision, and return it."""
 
646
        # In the future, revision entries will be signed. At that
 
647
        # point, it is probably best *not* to include the signature
 
648
        # in the revision hash. Because that lets you re-sign
 
649
        # the revision, (add signatures/remove signatures) and still
 
650
        # have all hash pointers stay consistent.
 
651
        # But for now, just hash the contents.
 
652
        return bzrlib.osutils.sha_file(self.get_revision_xml(revision_id))
 
653
 
 
654
 
 
655
    def get_inventory(self, inventory_id):
 
656
        """Get Inventory object by hash.
 
657
 
 
658
        TODO: Perhaps for this and similar methods, take a revision
 
659
               parameter which can be either an integer revno or a
 
660
               string hash."""
 
661
        from bzrlib.inventory import Inventory
 
662
 
 
663
        f = self.get_inventory_xml_file(inventory_id)
 
664
        return bzrlib.xml.serializer_v4.read_inventory(f)
 
665
 
 
666
 
 
667
    def get_inventory_xml(self, inventory_id):
 
668
        """Get inventory XML as a file object."""
 
669
        return self.inventory_store[inventory_id]
 
670
 
 
671
    get_inventory_xml_file = get_inventory_xml
 
672
            
 
673
 
 
674
    def get_inventory_sha1(self, inventory_id):
 
675
        """Return the sha1 hash of the inventory entry
 
676
        """
 
677
        return sha_file(self.get_inventory_xml(inventory_id))
 
678
 
 
679
 
 
680
    def get_revision_inventory(self, revision_id):
 
681
        """Return inventory of a past revision."""
 
682
        # bzr 0.0.6 imposes the constraint that the inventory_id
 
683
        # must be the same as its revision, so this is trivial.
 
684
        if revision_id == None:
 
685
            from bzrlib.inventory import Inventory
 
686
            return Inventory(self.get_root_id())
 
687
        else:
 
688
            return self.get_inventory(revision_id)
 
689
 
 
690
 
 
691
    def revision_history(self):
 
692
        """Return sequence of revision hashes on to this branch.
 
693
 
 
694
        >>> ScratchBranch().revision_history()
 
695
        []
 
696
        """
 
697
        self.lock_read()
 
698
        try:
 
699
            return [l.rstrip('\r\n') for l in
 
700
                    self.controlfile('revision-history', 'r').readlines()]
 
701
        finally:
 
702
            self.unlock()
 
703
 
 
704
 
 
705
    def common_ancestor(self, other, self_revno=None, other_revno=None):
 
706
        """
 
707
        >>> from bzrlib.commit import commit
 
708
        >>> sb = ScratchBranch(files=['foo', 'foo~'])
 
709
        >>> sb.common_ancestor(sb) == (None, None)
 
710
        True
 
711
        >>> commit(sb, "Committing first revision", verbose=False)
 
712
        >>> sb.common_ancestor(sb)[0]
 
713
        1
 
714
        >>> clone = sb.clone()
 
715
        >>> commit(sb, "Committing second revision", verbose=False)
 
716
        >>> sb.common_ancestor(sb)[0]
 
717
        2
 
718
        >>> sb.common_ancestor(clone)[0]
 
719
        1
 
720
        >>> commit(clone, "Committing divergent second revision", 
 
721
        ...               verbose=False)
 
722
        >>> sb.common_ancestor(clone)[0]
 
723
        1
 
724
        >>> sb.common_ancestor(clone) == clone.common_ancestor(sb)
 
725
        True
 
726
        >>> sb.common_ancestor(sb) != clone.common_ancestor(clone)
 
727
        True
 
728
        >>> clone2 = sb.clone()
 
729
        >>> sb.common_ancestor(clone2)[0]
 
730
        2
 
731
        >>> sb.common_ancestor(clone2, self_revno=1)[0]
 
732
        1
 
733
        >>> sb.common_ancestor(clone2, other_revno=1)[0]
 
734
        1
 
735
        """
 
736
        my_history = self.revision_history()
 
737
        other_history = other.revision_history()
 
738
        if self_revno is None:
 
739
            self_revno = len(my_history)
 
740
        if other_revno is None:
 
741
            other_revno = len(other_history)
 
742
        indices = range(min((self_revno, other_revno)))
 
743
        indices.reverse()
 
744
        for r in indices:
 
745
            if my_history[r] == other_history[r]:
 
746
                return r+1, my_history[r]
 
747
        return None, None
 
748
 
 
749
 
 
750
    def revno(self):
 
751
        """Return current revision number for this branch.
 
752
 
 
753
        That is equivalent to the number of revisions committed to
 
754
        this branch.
 
755
        """
 
756
        return len(self.revision_history())
 
757
 
 
758
 
 
759
    def last_patch(self):
 
760
        """Return last patch hash, or None if no history.
 
761
        """
 
762
        ph = self.revision_history()
 
763
        if ph:
 
764
            return ph[-1]
 
765
        else:
 
766
            return None
 
767
 
 
768
 
 
769
    def missing_revisions(self, other, stop_revision=None, diverged_ok=False):
 
770
        """
 
771
        If self and other have not diverged, return a list of the revisions
 
772
        present in other, but missing from self.
 
773
 
 
774
        >>> from bzrlib.commit import commit
 
775
        >>> bzrlib.trace.silent = True
 
776
        >>> br1 = ScratchBranch()
 
777
        >>> br2 = ScratchBranch()
 
778
        >>> br1.missing_revisions(br2)
 
779
        []
 
780
        >>> commit(br2, "lala!", rev_id="REVISION-ID-1")
 
781
        >>> br1.missing_revisions(br2)
 
782
        [u'REVISION-ID-1']
 
783
        >>> br2.missing_revisions(br1)
 
784
        []
 
785
        >>> commit(br1, "lala!", rev_id="REVISION-ID-1")
 
786
        >>> br1.missing_revisions(br2)
 
787
        []
 
788
        >>> commit(br2, "lala!", rev_id="REVISION-ID-2A")
 
789
        >>> br1.missing_revisions(br2)
 
790
        [u'REVISION-ID-2A']
 
791
        >>> commit(br1, "lala!", rev_id="REVISION-ID-2B")
 
792
        >>> br1.missing_revisions(br2)
 
793
        Traceback (most recent call last):
 
794
        DivergedBranches: These branches have diverged.
 
795
        """
 
796
        self_history = self.revision_history()
 
797
        self_len = len(self_history)
 
798
        other_history = other.revision_history()
 
799
        other_len = len(other_history)
 
800
        common_index = min(self_len, other_len) -1
 
801
        if common_index >= 0 and \
 
802
            self_history[common_index] != other_history[common_index]:
 
803
            raise DivergedBranches(self, other)
 
804
 
 
805
        if stop_revision is None:
 
806
            stop_revision = other_len
 
807
        elif stop_revision > other_len:
 
808
            raise bzrlib.errors.NoSuchRevision(self, stop_revision)
 
809
        
 
810
        return other_history[self_len:stop_revision]
 
811
 
 
812
 
 
813
    def update_revisions(self, other, stop_revision=None):
 
814
        """Pull in all new revisions from other branch.
 
815
        """
 
816
        from bzrlib.fetch import greedy_fetch
 
817
 
 
818
        pb = bzrlib.ui.ui_factory.progress_bar()
 
819
        pb.update('comparing histories')
 
820
 
 
821
        revision_ids = self.missing_revisions(other, stop_revision)
 
822
 
 
823
        if len(revision_ids) > 0:
 
824
            count = greedy_fetch(self, other, revision_ids[-1], pb)[0]
 
825
        else:
 
826
            count = 0
 
827
        self.append_revision(*revision_ids)
 
828
        ## note("Added %d revisions." % count)
 
829
        pb.clear()
 
830
 
 
831
    def install_revisions(self, other, revision_ids, pb):
 
832
        if hasattr(other.revision_store, "prefetch"):
 
833
            other.revision_store.prefetch(revision_ids)
 
834
        if hasattr(other.inventory_store, "prefetch"):
 
835
            inventory_ids = [other.get_revision(r).inventory_id
 
836
                             for r in revision_ids]
 
837
            other.inventory_store.prefetch(inventory_ids)
 
838
 
 
839
        if pb is None:
 
840
            pb = bzrlib.ui.ui_factory.progress_bar()
 
841
                
 
842
        revisions = []
 
843
        needed_texts = set()
 
844
        i = 0
 
845
 
 
846
        failures = set()
 
847
        for i, rev_id in enumerate(revision_ids):
 
848
            pb.update('fetching revision', i+1, len(revision_ids))
 
849
            try:
 
850
                rev = other.get_revision(rev_id)
 
851
            except bzrlib.errors.NoSuchRevision:
 
852
                failures.add(rev_id)
 
853
                continue
 
854
 
 
855
            revisions.append(rev)
 
856
            inv = other.get_inventory(str(rev.inventory_id))
 
857
            for key, entry in inv.iter_entries():
 
858
                if entry.text_id is None:
 
859
                    continue
 
860
                if entry.text_id not in self.text_store:
 
861
                    needed_texts.add(entry.text_id)
 
862
 
 
863
        pb.clear()
 
864
                    
 
865
        count, cp_fail = self.text_store.copy_multi(other.text_store, 
 
866
                                                    needed_texts)
 
867
        #print "Added %d texts." % count 
 
868
        inventory_ids = [ f.inventory_id for f in revisions ]
 
869
        count, cp_fail = self.inventory_store.copy_multi(other.inventory_store, 
 
870
                                                         inventory_ids)
 
871
        #print "Added %d inventories." % count 
 
872
        revision_ids = [ f.revision_id for f in revisions]
 
873
 
 
874
        count, cp_fail = self.revision_store.copy_multi(other.revision_store, 
 
875
                                                          revision_ids,
 
876
                                                          permit_failure=True)
 
877
        assert len(cp_fail) == 0 
 
878
        return count, failures
 
879
       
 
880
 
 
881
    def commit(self, *args, **kw):
 
882
        from bzrlib.commit import commit
 
883
        commit(self, *args, **kw)
 
884
        
 
885
 
 
886
    def lookup_revision(self, revision):
 
887
        """Return the revision identifier for a given revision specifier."""
 
888
        # XXX: I'm not sure this method belongs here; I'd rather have the
 
889
        # revision spec stuff be an UI thing, and branch blissfully unaware
 
890
        # of it.
 
891
        # Also, I'm not entirely happy with this method returning None
 
892
        # when the revision doesn't exist.
 
893
        # But I'm keeping the contract I found, because this seems to be
 
894
        # used in a lot of places - and when I do change these, I'd rather
 
895
        # figure out case-by-case which ones actually want to care about
 
896
        # revision specs (eg, they are UI-level) and which ones should trust
 
897
        # that they have a revno/revid.
 
898
        #   -- lalo@exoweb.net, 2005-09-07
 
899
        from bzrlib.errors import NoSuchRevision
 
900
        from bzrlib.revisionspec import RevisionSpec
 
901
        try:
 
902
            spec = RevisionSpec(self, revision)
 
903
        except NoSuchRevision:
 
904
            return None
 
905
        return spec.rev_id
 
906
 
 
907
 
 
908
    def revision_id_to_revno(self, revision_id):
 
909
        """Given a revision id, return its revno"""
 
910
        history = self.revision_history()
 
911
        try:
 
912
            return history.index(revision_id) + 1
 
913
        except ValueError:
 
914
            raise bzrlib.errors.NoSuchRevision(self, revision_id)
 
915
 
 
916
 
 
917
    def get_rev_id(self, revno, history=None):
 
918
        """Find the revision id of the specified revno."""
 
919
        if revno == 0:
 
920
            return None
 
921
        if history is None:
 
922
            history = self.revision_history()
 
923
        elif revno <= 0 or revno > len(history):
 
924
            raise bzrlib.errors.NoSuchRevision(self, revno)
 
925
        return history[revno - 1]
 
926
 
 
927
    def revision_tree(self, revision_id):
 
928
        """Return Tree for a revision on this branch.
 
929
 
 
930
        `revision_id` may be None for the null revision, in which case
 
931
        an `EmptyTree` is returned."""
 
932
        # TODO: refactor this to use an existing revision object
 
933
        # so we don't need to read it in twice.
 
934
        if revision_id == None:
 
935
            return EmptyTree()
 
936
        else:
 
937
            inv = self.get_revision_inventory(revision_id)
 
938
            return RevisionTree(self.text_store, inv)
 
939
 
 
940
 
 
941
    def working_tree(self):
 
942
        """Return a `Tree` for the working copy."""
 
943
        from bzrlib.workingtree import WorkingTree
 
944
        return WorkingTree(self.base, self.read_working_inventory())
 
945
 
 
946
 
 
947
    def basis_tree(self):
 
948
        """Return `Tree` object for last revision.
 
949
 
 
950
        If there are no revisions yet, return an `EmptyTree`.
 
951
        """
 
952
        r = self.last_patch()
 
953
        if r == None:
 
954
            return EmptyTree()
 
955
        else:
 
956
            return RevisionTree(self.text_store, self.get_revision_inventory(r))
 
957
 
 
958
 
 
959
 
 
960
    def rename_one(self, from_rel, to_rel):
 
961
        """Rename one file.
 
962
 
 
963
        This can change the directory or the filename or both.
 
964
        """
 
965
        self.lock_write()
 
966
        try:
 
967
            tree = self.working_tree()
 
968
            inv = tree.inventory
 
969
            if not tree.has_filename(from_rel):
 
970
                raise BzrError("can't rename: old working file %r does not exist" % from_rel)
 
971
            if tree.has_filename(to_rel):
 
972
                raise BzrError("can't rename: new working file %r already exists" % to_rel)
 
973
 
 
974
            file_id = inv.path2id(from_rel)
 
975
            if file_id == None:
 
976
                raise BzrError("can't rename: old name %r is not versioned" % from_rel)
 
977
 
 
978
            if inv.path2id(to_rel):
 
979
                raise BzrError("can't rename: new name %r is already versioned" % to_rel)
 
980
 
 
981
            to_dir, to_tail = os.path.split(to_rel)
 
982
            to_dir_id = inv.path2id(to_dir)
 
983
            if to_dir_id == None and to_dir != '':
 
984
                raise BzrError("can't determine destination directory id for %r" % to_dir)
 
985
 
 
986
            mutter("rename_one:")
 
987
            mutter("  file_id    {%s}" % file_id)
 
988
            mutter("  from_rel   %r" % from_rel)
 
989
            mutter("  to_rel     %r" % to_rel)
 
990
            mutter("  to_dir     %r" % to_dir)
 
991
            mutter("  to_dir_id  {%s}" % to_dir_id)
 
992
 
 
993
            inv.rename(file_id, to_dir_id, to_tail)
 
994
 
 
995
            from_abs = self.abspath(from_rel)
 
996
            to_abs = self.abspath(to_rel)
 
997
            try:
 
998
                os.rename(from_abs, to_abs)
 
999
            except OSError, e:
 
1000
                raise BzrError("failed to rename %r to %r: %s"
 
1001
                        % (from_abs, to_abs, e[1]),
 
1002
                        ["rename rolled back"])
 
1003
 
 
1004
            self._write_inventory(inv)
 
1005
        finally:
 
1006
            self.unlock()
 
1007
 
 
1008
 
 
1009
    def move(self, from_paths, to_name):
 
1010
        """Rename files.
 
1011
 
 
1012
        to_name must exist as a versioned directory.
 
1013
 
 
1014
        If to_name exists and is a directory, the files are moved into
 
1015
        it, keeping their old names.  If it is a directory, 
 
1016
 
 
1017
        Note that to_name is only the last component of the new name;
 
1018
        this doesn't change the directory.
 
1019
 
 
1020
        This returns a list of (from_path, to_path) pairs for each
 
1021
        entry that is moved.
 
1022
        """
 
1023
        result = []
 
1024
        self.lock_write()
 
1025
        try:
 
1026
            ## TODO: Option to move IDs only
 
1027
            assert not isinstance(from_paths, basestring)
 
1028
            tree = self.working_tree()
 
1029
            inv = tree.inventory
 
1030
            to_abs = self.abspath(to_name)
 
1031
            if not isdir(to_abs):
 
1032
                raise BzrError("destination %r is not a directory" % to_abs)
 
1033
            if not tree.has_filename(to_name):
 
1034
                raise BzrError("destination %r not in working directory" % to_abs)
 
1035
            to_dir_id = inv.path2id(to_name)
 
1036
            if to_dir_id == None and to_name != '':
 
1037
                raise BzrError("destination %r is not a versioned directory" % to_name)
 
1038
            to_dir_ie = inv[to_dir_id]
 
1039
            if to_dir_ie.kind not in ('directory', 'root_directory'):
 
1040
                raise BzrError("destination %r is not a directory" % to_abs)
 
1041
 
 
1042
            to_idpath = inv.get_idpath(to_dir_id)
 
1043
 
 
1044
            for f in from_paths:
 
1045
                if not tree.has_filename(f):
 
1046
                    raise BzrError("%r does not exist in working tree" % f)
 
1047
                f_id = inv.path2id(f)
 
1048
                if f_id == None:
 
1049
                    raise BzrError("%r is not versioned" % f)
 
1050
                name_tail = splitpath(f)[-1]
 
1051
                dest_path = appendpath(to_name, name_tail)
 
1052
                if tree.has_filename(dest_path):
 
1053
                    raise BzrError("destination %r already exists" % dest_path)
 
1054
                if f_id in to_idpath:
 
1055
                    raise BzrError("can't move %r to a subdirectory of itself" % f)
 
1056
 
 
1057
            # OK, so there's a race here, it's possible that someone will
 
1058
            # create a file in this interval and then the rename might be
 
1059
            # left half-done.  But we should have caught most problems.
 
1060
 
 
1061
            for f in from_paths:
 
1062
                name_tail = splitpath(f)[-1]
 
1063
                dest_path = appendpath(to_name, name_tail)
 
1064
                result.append((f, dest_path))
 
1065
                inv.rename(inv.path2id(f), to_dir_id, name_tail)
 
1066
                try:
 
1067
                    os.rename(self.abspath(f), self.abspath(dest_path))
 
1068
                except OSError, e:
 
1069
                    raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
 
1070
                            ["rename rolled back"])
 
1071
 
 
1072
            self._write_inventory(inv)
 
1073
        finally:
 
1074
            self.unlock()
 
1075
 
 
1076
        return result
 
1077
 
 
1078
 
 
1079
    def revert(self, filenames, old_tree=None, backups=True):
 
1080
        """Restore selected files to the versions from a previous tree.
 
1081
 
 
1082
        backups
 
1083
            If true (default) backups are made of files before
 
1084
            they're renamed.
 
1085
        """
 
1086
        from bzrlib.errors import NotVersionedError, BzrError
 
1087
        from bzrlib.atomicfile import AtomicFile
 
1088
        from bzrlib.osutils import backup_file
 
1089
        
 
1090
        inv = self.read_working_inventory()
 
1091
        if old_tree is None:
 
1092
            old_tree = self.basis_tree()
 
1093
        old_inv = old_tree.inventory
 
1094
 
 
1095
        nids = []
 
1096
        for fn in filenames:
 
1097
            file_id = inv.path2id(fn)
 
1098
            if not file_id:
 
1099
                raise NotVersionedError("not a versioned file", fn)
 
1100
            if not old_inv.has_id(file_id):
 
1101
                raise BzrError("file not present in old tree", fn, file_id)
 
1102
            nids.append((fn, file_id))
 
1103
            
 
1104
        # TODO: Rename back if it was previously at a different location
 
1105
 
 
1106
        # TODO: If given a directory, restore the entire contents from
 
1107
        # the previous version.
 
1108
 
 
1109
        # TODO: Make a backup to a temporary file.
 
1110
 
 
1111
        # TODO: If the file previously didn't exist, delete it?
 
1112
        for fn, file_id in nids:
 
1113
            backup_file(fn)
 
1114
            
 
1115
            f = AtomicFile(fn, 'wb')
 
1116
            try:
 
1117
                f.write(old_tree.get_file(file_id).read())
 
1118
                f.commit()
 
1119
            finally:
 
1120
                f.close()
 
1121
 
 
1122
 
 
1123
    def pending_merges(self):
 
1124
        """Return a list of pending merges.
 
1125
 
 
1126
        These are revisions that have been merged into the working
 
1127
        directory but not yet committed.
 
1128
        """
 
1129
        cfn = self.controlfilename('pending-merges')
 
1130
        if not os.path.exists(cfn):
 
1131
            return []
 
1132
        p = []
 
1133
        for l in self.controlfile('pending-merges', 'r').readlines():
 
1134
            p.append(l.rstrip('\n'))
 
1135
        return p
 
1136
 
 
1137
 
 
1138
    def add_pending_merge(self, revision_id):
 
1139
        from bzrlib.revision import validate_revision_id
 
1140
 
 
1141
        validate_revision_id(revision_id)
 
1142
 
 
1143
        p = self.pending_merges()
 
1144
        if revision_id in p:
 
1145
            return
 
1146
        p.append(revision_id)
 
1147
        self.set_pending_merges(p)
 
1148
 
 
1149
 
 
1150
    def set_pending_merges(self, rev_list):
 
1151
        from bzrlib.atomicfile import AtomicFile
 
1152
        self.lock_write()
 
1153
        try:
 
1154
            f = AtomicFile(self.controlfilename('pending-merges'))
 
1155
            try:
 
1156
                for l in rev_list:
 
1157
                    print >>f, l
 
1158
                f.commit()
 
1159
            finally:
 
1160
                f.close()
 
1161
        finally:
 
1162
            self.unlock()
 
1163
 
 
1164
 
 
1165
    def get_parent(self):
 
1166
        """Return the parent location of the branch.
 
1167
 
 
1168
        This is the default location for push/pull/missing.  The usual
 
1169
        pattern is that the user can override it by specifying a
 
1170
        location.
 
1171
        """
 
1172
        import errno
 
1173
        _locs = ['parent', 'pull', 'x-pull']
 
1174
        for l in _locs:
 
1175
            try:
 
1176
                return self.controlfile(l, 'r').read().strip('\n')
 
1177
            except IOError, e:
 
1178
                if e.errno != errno.ENOENT:
 
1179
                    raise
 
1180
        return None
 
1181
 
 
1182
 
 
1183
    def set_parent(self, url):
 
1184
        # TODO: Maybe delete old location files?
 
1185
        from bzrlib.atomicfile import AtomicFile
 
1186
        self.lock_write()
 
1187
        try:
 
1188
            f = AtomicFile(self.controlfilename('parent'))
 
1189
            try:
 
1190
                f.write(url + '\n')
 
1191
                f.commit()
 
1192
            finally:
 
1193
                f.close()
 
1194
        finally:
 
1195
            self.unlock()
 
1196
 
 
1197
    def check_revno(self, revno):
 
1198
        """\
 
1199
        Check whether a revno corresponds to any revision.
 
1200
        Zero (the NULL revision) is considered valid.
 
1201
        """
 
1202
        if revno != 0:
 
1203
            self.check_real_revno(revno)
 
1204
            
 
1205
    def check_real_revno(self, revno):
 
1206
        """\
 
1207
        Check whether a revno corresponds to a real revision.
 
1208
        Zero (the NULL revision) is considered invalid
 
1209
        """
 
1210
        if revno < 1 or revno > self.revno():
 
1211
            raise InvalidRevisionNumber(revno)
 
1212
        
 
1213
        
 
1214
 
 
1215
 
 
1216
class ScratchBranch(LocalBranch):
 
1217
    """Special test class: a branch that cleans up after itself.
 
1218
 
 
1219
    >>> b = ScratchBranch()
 
1220
    >>> isdir(b.base)
 
1221
    True
 
1222
    >>> bd = b.base
 
1223
    >>> b.destroy()
 
1224
    >>> isdir(bd)
 
1225
    False
 
1226
    """
 
1227
    def __init__(self, files=[], dirs=[], base=None):
 
1228
        """Make a test branch.
 
1229
 
 
1230
        This creates a temporary directory and runs init-tree in it.
 
1231
 
 
1232
        If any files are listed, they are created in the working copy.
 
1233
        """
 
1234
        from tempfile import mkdtemp
 
1235
        init = False
 
1236
        if base is None:
 
1237
            base = mkdtemp()
 
1238
            init = True
 
1239
        LocalBranch.__init__(self, base, init=init)
 
1240
        for d in dirs:
 
1241
            os.mkdir(self.abspath(d))
 
1242
            
 
1243
        for f in files:
 
1244
            file(os.path.join(self.base, f), 'w').write('content of %s' % f)
 
1245
 
 
1246
 
 
1247
    def clone(self):
 
1248
        """
 
1249
        >>> orig = ScratchBranch(files=["file1", "file2"])
 
1250
        >>> clone = orig.clone()
 
1251
        >>> os.path.samefile(orig.base, clone.base)
 
1252
        False
 
1253
        >>> os.path.isfile(os.path.join(clone.base, "file1"))
 
1254
        True
 
1255
        """
 
1256
        from shutil import copytree
 
1257
        from tempfile import mkdtemp
 
1258
        base = mkdtemp()
 
1259
        os.rmdir(base)
 
1260
        copytree(self.base, base, symlinks=True)
 
1261
        return ScratchBranch(base=base)
 
1262
 
 
1263
 
 
1264
        
 
1265
    def __del__(self):
 
1266
        self.destroy()
 
1267
 
 
1268
    def destroy(self):
 
1269
        """Destroy the test branch, removing the scratch directory."""
 
1270
        from shutil import rmtree
 
1271
        try:
 
1272
            if self.base:
 
1273
                mutter("delete ScratchBranch %s" % self.base)
 
1274
                rmtree(self.base)
 
1275
        except OSError, e:
 
1276
            # Work around for shutil.rmtree failing on Windows when
 
1277
            # readonly files are encountered
 
1278
            mutter("hit exception in destroying ScratchBranch: %s" % e)
 
1279
            for root, dirs, files in os.walk(self.base, topdown=False):
 
1280
                for name in files:
 
1281
                    os.chmod(os.path.join(root, name), 0700)
 
1282
            rmtree(self.base)
 
1283
        self.base = None
 
1284
 
 
1285
    
 
1286
 
 
1287
######################################################################
 
1288
# predicates
 
1289
 
 
1290
 
 
1291
def is_control_file(filename):
 
1292
    ## FIXME: better check
 
1293
    filename = os.path.normpath(filename)
 
1294
    while filename != '':
 
1295
        head, tail = os.path.split(filename)
 
1296
        ## mutter('check %r for control file' % ((head, tail), ))
 
1297
        if tail == bzrlib.BZRDIR:
 
1298
            return True
 
1299
        if filename == head:
 
1300
            break
 
1301
        filename = head
 
1302
    return False
 
1303
 
 
1304
 
 
1305
 
 
1306
def gen_file_id(name):
 
1307
    """Return new file id.
 
1308
 
 
1309
    This should probably generate proper UUIDs, but for the moment we
 
1310
    cope with just randomness because running uuidgen every time is
 
1311
    slow."""
 
1312
    import re
 
1313
    from binascii import hexlify
 
1314
    from time import time
 
1315
 
 
1316
    # get last component
 
1317
    idx = name.rfind('/')
 
1318
    if idx != -1:
 
1319
        name = name[idx+1 : ]
 
1320
    idx = name.rfind('\\')
 
1321
    if idx != -1:
 
1322
        name = name[idx+1 : ]
 
1323
 
 
1324
    # make it not a hidden file
 
1325
    name = name.lstrip('.')
 
1326
 
 
1327
    # remove any wierd characters; we don't escape them but rather
 
1328
    # just pull them out
 
1329
    name = re.sub(r'[^\w.]', '', name)
 
1330
 
 
1331
    s = hexlify(rand_bytes(8))
 
1332
    return '-'.join((name, compact_date(time()), s))
 
1333
 
 
1334
 
 
1335
def gen_root_id():
 
1336
    """Return a new tree-root file id."""
 
1337
    return gen_file_id('TREE_ROOT')
 
1338
 
 
1339
 
 
1340
def copy_branch(branch_from, to_location, revision=None):
 
1341
    """Copy branch_from into the existing directory to_location.
 
1342
 
 
1343
    revision
 
1344
        If not None, only revisions up to this point will be copied.
 
1345
        The head of the new branch will be that revision.
 
1346
 
 
1347
    to_location
 
1348
        The name of a local directory that exists but is empty.
 
1349
    """
 
1350
    from bzrlib.merge import merge
 
1351
    from bzrlib.revisionspec import RevisionSpec
 
1352
 
 
1353
    assert isinstance(branch_from, Branch)
 
1354
    assert isinstance(to_location, basestring)
 
1355
    
 
1356
    br_to = Branch(to_location, init=True)
 
1357
    br_to.set_root_id(branch_from.get_root_id())
 
1358
    if revision is None:
 
1359
        revno = branch_from.revno()
 
1360
    else:
 
1361
        revno, rev_id = RevisionSpec(branch_from, revision)
 
1362
    br_to.update_revisions(branch_from, stop_revision=revno)
 
1363
    merge((to_location, -1), (to_location, 0), this_dir=to_location,
 
1364
          check_clean=False, ignore_zero=True)
 
1365
    
 
1366
    from_location = branch_from.base
 
1367
    br_to.set_parent(branch_from.base)
 
1368