37
37
UnlistableBranch, NoSuchFile, NotVersionedError,
39
39
from bzrlib.textui import show_status
40
from bzrlib.revision import (Revision, is_ancestor, get_intervening_revisions,
40
from bzrlib.revision import (Revision, is_ancestor, get_intervening_revisions)
43
42
from bzrlib.delta import compare_trees
44
43
from bzrlib.tree import EmptyTree, RevisionTree
45
44
from bzrlib.inventory import Inventory
46
45
from bzrlib.store import copy_all
47
from bzrlib.store.compressed_text import CompressedTextStore
48
from bzrlib.store.text import TextStore
49
from bzrlib.store.weave import WeaveStore
50
from bzrlib.testament import Testament
51
46
import bzrlib.transactions as transactions
52
47
from bzrlib.transport import Transport, get_transport
55
50
from config import TreeConfig
51
from control_files import ControlFiles
52
from rev_storage import RevisionStorage
58
55
BZR_BRANCH_FORMAT_4 = "Bazaar-NG branch, format 0.0.4\n"
167
164
nick = property(_get_nick, _set_nick)
171
class _Branch(Branch):
167
class _Branch(Branch, ControlFiles):
172
168
"""A branch stored in the actual filesystem.
174
170
Note that it's "local" in the context of the filesystem; it doesn't
175
171
really matter if it's on an nfs/smb/afs/coda/... share, as long as
176
172
it's writable, and can be accessed via the normal filesystem API.
182
If _lock_mode is true, a positive count of the number of times the
186
Lock object from bzrlib.lock.
188
175
# We actually expect this class to be somewhat short-lived; part of its
189
176
# purpose is to try to isolate what bits of the branch logic are tied to
190
177
# filesystem access, so that in a later step, we can extricate them to
191
178
# a separarte ("storage") class.
195
179
_inventory_weave = None
197
181
# Map some sort of prefix into a namespace
240
224
assert isinstance(transport, Transport), \
241
225
"%r is not a Transport" % transport
242
self._transport = transport
226
ControlFiles.__init__(self, transport, 'branch-lock')
244
228
self._make_control()
245
229
self._check_format(relax_version_check)
247
def get_store(name, compressed=True, prefixed=False):
248
# FIXME: This approach of assuming stores are all entirely compressed
249
# or entirely uncompressed is tidy, but breaks upgrade from
250
# some existing branches where there's a mixture; we probably
251
# still want the option to look for both.
252
relpath = self._rel_controlfilename(name)
254
store = CompressedTextStore(self._transport.clone(relpath),
257
store = TextStore(self._transport.clone(relpath),
259
#if self._transport.should_cache():
260
# cache_path = os.path.join(self.cache_root, name)
261
# os.mkdir(cache_path)
262
# store = bzrlib.store.CachedStore(store, cache_path)
264
def get_weave(name, prefixed=False):
265
relpath = self._rel_controlfilename(name)
266
ws = WeaveStore(self._transport.clone(relpath), prefixed=prefixed)
267
if self._transport.should_cache():
268
ws.enable_cache = True
271
if self._branch_format == 4:
272
self.inventory_store = get_store('inventory-store')
273
self.text_store = get_store('text-store')
274
self.revision_store = get_store('revision-store')
275
elif self._branch_format == 5:
276
self.control_weaves = get_weave('')
277
self.weave_store = get_weave('weaves')
278
self.revision_store = get_store('revision-store', compressed=False)
279
elif self._branch_format == 6:
280
self.control_weaves = get_weave('')
281
self.weave_store = get_weave('weaves', prefixed=True)
282
self.revision_store = get_store('revision-store', compressed=False,
284
self.revision_store.register_suffix('sig')
285
self._transaction = None
230
self.storage = RevisionStorage(transport, self._branch_format)
287
232
def __str__(self):
288
233
return '%s(%r)' % (self.__class__.__name__, self._transport.base)
317
256
base = property(_get_base, doc="The URL for the root of this branch.")
319
def _finish_transaction(self):
320
"""Exit the current transaction."""
321
if self._transaction is None:
322
raise errors.LockError('Branch %s is not in a transaction' %
324
transaction = self._transaction
325
self._transaction = None
328
def get_transaction(self):
329
"""Return the current active transaction.
331
If no transaction is active, this returns a passthrough object
332
for which all data is immediately flushed and no caching happens.
334
if self._transaction is None:
335
return transactions.PassThroughTransaction()
337
return self._transaction
339
def _set_transaction(self, new_transaction):
340
"""Set a new active transaction."""
341
if self._transaction is not None:
342
raise errors.LockError('Branch %s is in a transaction already.' %
344
self._transaction = new_transaction
346
def lock_write(self):
347
mutter("lock write: %s (%s)", self, self._lock_count)
348
# TODO: Upgrade locking to support using a Transport,
349
# and potentially a remote locking protocol
351
if self._lock_mode != 'w':
352
raise LockError("can't upgrade to a write lock from %r" %
354
self._lock_count += 1
356
self._lock = self._transport.lock_write(
357
self._rel_controlfilename('branch-lock'))
358
self._lock_mode = 'w'
360
self._set_transaction(transactions.PassThroughTransaction())
363
mutter("lock read: %s (%s)", self, self._lock_count)
365
assert self._lock_mode in ('r', 'w'), \
366
"invalid lock mode %r" % self._lock_mode
367
self._lock_count += 1
369
self._lock = self._transport.lock_read(
370
self._rel_controlfilename('branch-lock'))
371
self._lock_mode = 'r'
373
self._set_transaction(transactions.ReadOnlyTransaction())
374
# 5K may be excessive, but hey, its a knob.
375
self.get_transaction().set_cache_size(5000)
378
mutter("unlock: %s (%s)", self, self._lock_count)
379
if not self._lock_mode:
380
raise LockError('branch %r is not locked' % (self))
382
if self._lock_count > 1:
383
self._lock_count -= 1
385
self._finish_transaction()
388
self._lock_mode = self._lock_count = None
390
258
def abspath(self, name):
391
259
"""Return absolute filename for something in the branch
396
264
return self._transport.abspath(name)
398
def _rel_controlfilename(self, file_or_path):
399
if not isinstance(file_or_path, basestring):
400
file_or_path = '/'.join(file_or_path)
401
if file_or_path == '':
403
return bzrlib.transport.urlescape(bzrlib.BZRDIR + '/' + file_or_path)
405
def controlfilename(self, file_or_path):
406
"""Return location relative to branch."""
407
return self._transport.abspath(self._rel_controlfilename(file_or_path))
409
def controlfile(self, file_or_path, mode='r'):
410
"""Open a control file for this branch.
412
There are two classes of file in the control directory: text
413
and binary. binary files are untranslated byte streams. Text
414
control files are stored with Unix newlines and in UTF-8, even
415
if the platform or locale defaults are different.
417
Controlfiles should almost never be opened in write mode but
418
rather should be atomically copied and replaced using atomicfile.
422
relpath = self._rel_controlfilename(file_or_path)
423
#TODO: codecs.open() buffers linewise, so it was overloaded with
424
# a much larger buffer, do we need to do the same for getreader/getwriter?
426
return self._transport.get(relpath)
428
raise BzrError("Branch.controlfile(mode='wb') is not supported, use put_controlfiles")
430
# XXX: Do we really want errors='replace'? Perhaps it should be
431
# an error, or at least reported, if there's incorrectly-encoded
432
# data inside a file.
433
# <https://launchpad.net/products/bzr/+bug/3823>
434
return codecs.getreader('utf-8')(self._transport.get(relpath), errors='replace')
436
raise BzrError("Branch.controlfile(mode='w') is not supported, use put_controlfiles")
438
raise BzrError("invalid controlfile mode %r" % mode)
440
def put_controlfile(self, path, f, encode=True):
441
"""Write an entry as a controlfile.
443
:param path: The path to put the file, relative to the .bzr control
445
:param f: A file-like or string object whose contents should be copied.
446
:param encode: If true, encode the contents as utf-8
448
self.put_controlfiles([(path, f)], encode=encode)
450
def put_controlfiles(self, files, encode=True):
451
"""Write several entries as controlfiles.
453
:param files: A list of [(path, file)] pairs, where the path is the directory
454
underneath the bzr control directory
455
:param encode: If true, encode the contents as utf-8
459
for path, f in files:
461
if isinstance(f, basestring):
462
f = f.encode('utf-8', 'replace')
464
f = codecs.getwriter('utf-8')(f, errors='replace')
465
path = self._rel_controlfilename(path)
466
ctrl_files.append((path, f))
467
self._transport.put_multi(ctrl_files)
469
266
def _make_control(self):
470
267
from bzrlib.inventory import Inventory
471
268
from bzrlib.weavefile import write_weave_v5
532
329
def get_root_id(self):
533
330
"""Return the id of this branches root"""
534
inv = self.get_inventory(self.last_revision())
331
inv = self.storage.get_inventory(self.last_revision())
535
332
return inv.root.file_id
334
def write_lock(self):
335
ControlFiles.write_lock(self)
336
self.storage.write_lock()
339
ControlFiles.read_lock(self)
340
self.storage.read_lock()
344
self.storage.unlock()
347
ControlFiles.unlock(self)
537
349
@needs_write_lock
538
350
def set_root_id(self, file_id):
539
351
inv = self.working_tree().read_working_inventory()
670
482
def set_revision_history(self, rev_history):
671
483
self.put_controlfile('revision-history', '\n'.join(rev_history))
673
def has_revision(self, revision_id):
674
"""True if this branch has a copy of the revision.
676
This does not necessarily imply the revision is merge
677
or on the mainline."""
678
return (revision_id is None
679
or self.revision_store.has_id(revision_id))
682
def get_revision_xml_file(self, revision_id):
683
"""Return XML file object for revision object."""
684
if not revision_id or not isinstance(revision_id, basestring):
685
raise InvalidRevisionId(revision_id=revision_id, branch=self)
687
return self.revision_store.get(revision_id)
688
except (IndexError, KeyError):
689
raise bzrlib.errors.NoSuchRevision(self, revision_id)
692
get_revision_xml = get_revision_xml_file
694
def get_revision_xml(self, revision_id):
695
return self.get_revision_xml_file(revision_id).read()
698
def get_revision(self, revision_id):
699
"""Return the Revision object for a named revision"""
700
xml_file = self.get_revision_xml_file(revision_id)
703
r = bzrlib.xml5.serializer_v5.read_revision(xml_file)
704
except SyntaxError, e:
705
raise bzrlib.errors.BzrError('failed to unpack revision_xml',
709
assert r.revision_id == revision_id
712
485
def get_revision_delta(self, revno):
713
486
"""Return the delta for one revision.
723
496
# revno is 1-based; list is 0-based
725
new_tree = self.revision_tree(rh[revno-1])
498
new_tree = self.storage.revision_tree(rh[revno-1])
727
500
old_tree = EmptyTree()
729
old_tree = self.revision_tree(rh[revno-2])
502
old_tree = self.storage.revision_tree(rh[revno-2])
731
504
return compare_trees(old_tree, new_tree)
733
def get_revision_sha1(self, revision_id):
734
"""Hash the stored value of a revision, and return it."""
735
# In the future, revision entries will be signed. At that
736
# point, it is probably best *not* to include the signature
737
# in the revision hash. Because that lets you re-sign
738
# the revision, (add signatures/remove signatures) and still
739
# have all hash pointers stay consistent.
740
# But for now, just hash the contents.
741
return bzrlib.osutils.sha_file(self.get_revision_xml_file(revision_id))
743
506
def get_ancestry(self, revision_id):
744
507
"""Return a list of revision-ids integrated by a revision.
749
512
if revision_id is None:
751
w = self.get_inventory_weave()
514
w = self.storage.get_inventory_weave()
752
515
return [None] + map(w.idx_to_name,
753
516
w.inclusions([w.lookup(revision_id)]))
755
def get_inventory_weave(self):
756
return self.control_weaves.get_weave('inventory',
757
self.get_transaction())
759
def get_inventory(self, revision_id):
760
"""Get Inventory object by hash."""
761
xml = self.get_inventory_xml(revision_id)
762
return bzrlib.xml5.serializer_v5.read_inventory_from_string(xml)
764
def get_inventory_xml(self, revision_id):
765
"""Get inventory XML as a file object."""
767
assert isinstance(revision_id, basestring), type(revision_id)
768
iw = self.get_inventory_weave()
769
return iw.get_text(iw.lookup(revision_id))
771
raise bzrlib.errors.HistoryMissing(self, 'inventory', revision_id)
773
def get_inventory_sha1(self, revision_id):
774
"""Return the sha1 hash of the inventory entry
776
return self.get_revision(revision_id).inventory_sha1
778
def get_revision_inventory(self, revision_id):
779
"""Return inventory of a past revision."""
780
# TODO: Unify this with get_inventory()
781
# bzr 0.0.6 and later imposes the constraint that the inventory_id
782
# must be the same as its revision, so this is trivial.
783
if revision_id == None:
784
# This does not make sense: if there is no revision,
785
# then it is the current tree inventory surely ?!
786
# and thus get_root_id() is something that looks at the last
787
# commit on the branch, and the get_root_id is an inventory check.
788
raise NotImplementedError
789
# return Inventory(self.get_root_id())
791
return self.get_inventory(revision_id)
794
519
def revision_history(self):
795
520
"""Return sequence of revision hashes on to this branch."""
923
649
raise bzrlib.errors.NoSuchRevision(self, revno)
924
650
return history[revno - 1]
926
def revision_tree(self, revision_id):
927
"""Return Tree for a revision on this branch.
929
`revision_id` may be None for the null revision, in which case
930
an `EmptyTree` is returned."""
931
# TODO: refactor this to use an existing revision object
932
# so we don't need to read it in twice.
933
if revision_id == None or revision_id == NULL_REVISION:
936
inv = self.get_revision_inventory(revision_id)
937
return RevisionTree(self.weave_store, inv, revision_id)
939
652
def working_tree(self):
940
653
"""Return a `Tree` for the working copy."""
941
654
from bzrlib.workingtree import WorkingTree
1212
925
if revno < 1 or revno > self.revno():
1213
926
raise InvalidRevisionNumber(revno)
1215
def sign_revision(self, revision_id, gpg_strategy):
1216
plaintext = Testament.from_revision(self, revision_id).as_short_text()
1217
self.store_revision_signature(gpg_strategy, plaintext, revision_id)
1220
def store_revision_signature(self, gpg_strategy, plaintext, revision_id):
1221
self.revision_store.add(StringIO(gpg_strategy.sign(plaintext)),
1226
930
class ScratchBranch(_Branch):