~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Martin Pool
  • Date: 2005-07-07 10:22:02 UTC
  • Revision ID: mbp@sourcefrog.net-20050707102201-2d2a13a25098b101
- rearrange and clear up merged weave

Show diffs side-by-side

added added

removed removed

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