/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: Robert Collins
  • Date: 2005-10-03 15:19:25 UTC
  • mto: (1393.1.30)
  • mto: This revision was merged to the branch mainline in revision 1400.
  • Revision ID: robertc@robertcollins.net-20051003151925-19df6a9a5e9dc42a
remove kind from the InventoryEntry constructor - only child classes should be created now

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