~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Martin Pool
  • Date: 2005-04-15 01:31:21 UTC
  • Revision ID: mbp@sourcefrog.net-20050415013121-b18f1be12a735066
- Doc cleanups from Magnus Therning

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