50
50
def find_branch(f, **args):
51
if f and (f.startswith('http://') or f.startswith('https://')):
52
from bzrlib.remotebranch import RemoteBranch
53
return RemoteBranch(f, **args)
55
return Branch(f, **args)
58
def find_cached_branch(f, cache_root, **args):
59
from bzrlib.remotebranch import RemoteBranch
60
br = find_branch(f, **args)
61
def cacheify(br, store_name):
62
from bzrlib.meta_store import CachedStore
63
cache_path = os.path.join(cache_root, store_name)
65
new_store = CachedStore(getattr(br, store_name), cache_path)
66
setattr(br, store_name, new_store)
68
if isinstance(br, RemoteBranch):
69
cacheify(br, 'inventory_store')
70
cacheify(br, 'text_store')
71
cacheify(br, 'revision_store')
51
from bzrlib.transport import transport
52
from bzrlib.local_transport import LocalTransport
54
# FIXME: This is a hack around transport so that
55
# We can search the local directories for
57
if args.has_key('init') and args['init']:
58
# Don't search if we are init-ing
59
return Branch(t, **args)
60
if isinstance(t, LocalTransport):
61
root = find_branch_root(f)
64
return Branch(t, **args)
75
66
def _relpath(base, path):
76
67
"""Return path relative to base, or raise exception.
156
145
_lock_mode = None
157
146
_lock_count = None
160
150
# Map some sort of prefix into a namespace
161
151
# stuff like "revno:10", "revid:", etc.
162
152
# This should match a prefix with a function which accepts
163
153
REVISION_NAMESPACES = {}
165
def __init__(self, base, init=False, find_root=True):
155
def __init__(self, transport, init=False):
166
156
"""Create new branch object at a particular location.
168
base -- Base directory for the branch. May be a file:// url.
158
transport -- A Transport object, defining how to access files.
159
(If a string, transport.transport() will be used to
160
create a Transport object)
170
162
init -- If True, create new control files in a previously
171
163
unversioned directory. If False, the branch must already
174
find_root -- If true and init is false, find the root of the
175
existing branch containing base.
177
166
In the test suite, creation of new trees is tested using the
178
167
`ScratchBranch` class.
180
from bzrlib.store import ImmutableStore
169
if isinstance(transport, basestring):
170
from transport import transport as get_transport
171
transport = get_transport(transport)
173
self._transport = transport
182
self.base = os.path.realpath(base)
183
175
self._make_control()
185
self.base = find_branch_root(base)
187
if base.startswith("file://"):
189
self.base = os.path.realpath(base)
190
if not isdir(self.controlfilename('.')):
191
raise NotBranchError("not a bzr branch: %s" % quotefn(base),
192
['use "bzr init" to initialize a new working tree',
193
'current bzr can only operate from top-of-tree'])
194
176
self._check_format()
196
self.text_store = ImmutableStore(self.controlfilename('text-store'))
197
self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
198
self.inventory_store = ImmutableStore(self.controlfilename('inventory-store'))
201
179
def __str__(self):
202
return '%s(%r)' % (self.__class__.__name__, self.base)
180
return '%s(%r)' % (self.__class__.__name__, self._transport.base)
205
183
__repr__ = __str__
211
189
warn("branch %r was not explicitly unlocked" % self)
212
190
self._lock.unlock()
192
# TODO: It might be best to do this somewhere else,
193
# but it is nice for a Branch object to automatically
194
# cache it's information.
195
# Alternatively, we could have the Transport objects cache requests
196
# See the earlier discussion about how major objects (like Branch)
197
# should never expect their __del__ function to run.
198
if self.cache_root is not None:
199
#from warnings import warn
200
#warn("branch %r auto-cleanup of cache files" % self)
203
shutil.rmtree(self.cache_root)
206
self.cache_root = None
210
return self._transport.base
213
base = property(_get_base)
214
216
def lock_write(self):
217
# TODO: Upgrade locking to support using a Transport,
218
# and potentially a remote locking protocol
215
219
if self._lock_mode:
216
220
if self._lock_mode != 'w':
217
221
from bzrlib.errors import LockError
253
255
def abspath(self, name):
254
256
"""Return absolute filename for something in the branch"""
255
return os.path.join(self.base, name)
257
return self._transport.abspath(name)
257
259
def relpath(self, path):
258
260
"""Return path relative to this branch of something inside it.
260
262
Raises an error if path is not in this branch."""
261
return _relpath(self.base, path)
263
return self._transport.relpath(path)
266
def _rel_controlfilename(self, file_or_path):
267
if isinstance(file_or_path, basestring):
268
file_or_path = [file_or_path]
269
return [bzrlib.BZRDIR] + file_or_path
263
271
def controlfilename(self, file_or_path):
264
272
"""Return location relative to branch."""
265
if isinstance(file_or_path, basestring):
266
file_or_path = [file_or_path]
267
return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
273
return self._transport.abspath(self._rel_controlfilename(file_or_path))
270
276
def controlfile(self, file_or_path, mode='r'):
278
284
Controlfiles should almost never be opened in write mode but
279
285
rather should be atomically copied and replaced using atomicfile.
282
fn = self.controlfilename(file_or_path)
284
if mode == 'rb' or mode == 'wb':
285
return file(fn, mode)
286
elif mode == 'r' or mode == 'w':
287
# open in binary mode anyhow so there's no newline translation;
288
# codecs uses line buffering by default; don't want that.
290
return codecs.open(fn, mode + 'b', 'utf-8',
289
relpath = self._rel_controlfilename(file_or_path)
290
#TODO: codecs.open() buffers linewise, so it was overloaded with
291
# a much larger buffer, do we need to do the same for getreader/getwriter?
293
return self._transport.get(relpath)
295
raise BzrError("Branch.controlfile(mode='wb') is not supported, use put_controlfiles")
297
return codecs.getreader('utf-8')(self._transport.get(relpath), errors='replace')
299
raise BzrError("Branch.controlfile(mode='w') is not supported, use put_controlfiles")
293
301
raise BzrError("invalid controlfile mode %r" % mode)
303
def put_controlfile(self, path, f, encode=True):
304
"""Write an entry as a controlfile.
306
:param path: The path to put the file, relative to the .bzr control
308
:param f: A file-like or string object whose contents should be copied.
309
:param encode: If true, encode the contents as utf-8
311
self.put_controlfiles([(path, f)], encode=encode)
313
def put_controlfiles(self, files, encode=True):
314
"""Write several entries as controlfiles.
316
:param files: A list of [(path, file)] pairs, where the path is the directory
317
underneath the bzr control directory
318
:param encode: If true, encode the contents as utf-8
322
for path, f in files:
324
if isinstance(f, basestring):
325
f = f.encode('utf-8', 'replace')
327
f = codecs.getwriter('utf-8')(f, errors='replace')
328
path = self._rel_controlfilename(path)
329
ctrl_files.append((path, f))
330
self._transport.put_multi(ctrl_files)
295
332
def _make_control(self):
296
333
from bzrlib.inventory import Inventory
334
from cStringIO import StringIO
298
os.mkdir(self.controlfilename([]))
299
self.controlfile('README', 'w').write(
300
"This is a Bazaar-NG control directory.\n"
301
"Do not change any files in this directory.\n")
302
self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT)
303
for d in ('text-store', 'inventory-store', 'revision-store'):
304
os.mkdir(self.controlfilename(d))
305
for f in ('revision-history', 'merged-patches',
306
'pending-merged-patches', 'branch-name',
309
self.controlfile(f, 'w').write('')
310
mutter('created control directory in ' + self.base)
336
# Create an empty inventory
312
338
# if we want per-tree root ids then this is the place to set
313
339
# them; they're not needed for now and so ommitted for
315
f = self.controlfile('inventory','w')
316
bzrlib.xml.serializer_v4.write_inventory(Inventory(), f)
341
bzrlib.xml.serializer_v4.write_inventory(Inventory(), sio)
343
dirs = [[], 'text-store', 'inventory-store', 'revision-store']
345
"This is a Bazaar-NG control directory.\n"
346
"Do not change any files in this directory.\n"),
347
('branch-format', BZR_BRANCH_FORMAT),
348
('revision-history', ''),
349
('merged-patches', ''),
350
('pending-merged-patches', ''),
353
('pending-merges', ''),
354
('inventory', sio.getvalue())
356
self._transport.mkdir_multi([self._rel_controlfilename(d) for d in dirs])
357
self.put_controlfiles(files)
358
mutter('created control directory in ' + self._transport.base)
319
360
def _check_format(self):
320
361
"""Check this branch format is supported.
334
375
['use a different bzr version',
335
376
'or remove the .bzr directory and "bzr init" again'])
378
# We know that the format is the currently supported one.
379
# So create the rest of the entries.
380
from bzrlib.store.compressed_text import CompressedTextStore
382
if self._transport.should_cache():
384
self.cache_root = tempfile.mkdtemp(prefix='bzr-cache')
385
mutter('Branch %r using caching in %r' % (self, self.cache_root))
387
self.cache_root = None
390
relpath = self._rel_controlfilename(name)
391
store = CompressedTextStore(self._transport.clone(relpath))
392
if self._transport.should_cache():
393
from meta_store import CachedStore
394
cache_path = os.path.join(self.cache_root, name)
396
store = CachedStore(store, cache_path)
399
self.text_store = get_store('text-store')
400
self.revision_store = get_store('revision-store')
401
self.inventory_store = get_store('inventory-store')
337
403
def get_root_id(self):
338
404
"""Return the id of this branches root"""
339
405
inv = self.read_working_inventory()
623
683
return compare_trees(old_tree, new_tree)
686
def get_revisions(self, revision_ids, pb=None):
687
"""Return the Revision object for a set of named revisions"""
688
from bzrlib.revision import Revision
689
from bzrlib.xml import unpack_xml
691
# TODO: We need to decide what to do here
692
# we cannot use a generator with a try/finally, because
693
# you cannot guarantee that the caller will iterate through
695
# in the past, get_inventory wasn't even wrapped in a
696
# try/finally locking block.
697
# We could either lock without the try/finally, or just
698
# not lock at all. We are reading entries that should
700
# I prefer locking with no finally, so that if someone
701
# asks for a list of revisions, but doesn't consume them,
702
# that is their problem, and they will suffer the consequences
704
for xml_file in self.revision_store.get(revision_ids, pb=pb):
706
r = bzrlib.xml.serializer_v4.read_revision(xml_file)
707
except SyntaxError, e:
708
raise bzrlib.errors.BzrError('failed to unpack revision_xml',
627
714
def get_revision_sha1(self, revision_id):
628
715
"""Hash the stored value of a revision, and return it."""
629
716
# In the future, revision entries will be signed. At that
641
728
TODO: Perhaps for this and similar methods, take a revision
642
729
parameter which can be either an integer revno or a
644
from bzrlib.inventory import Inventory
646
732
f = self.get_inventory_xml_file(inventory_id)
647
733
return bzrlib.xml.serializer_v4.read_inventory(f)
650
736
def get_inventory_xml(self, inventory_id):
651
737
"""Get inventory XML as a file object."""
738
# Shouldn't this have a read-lock around it?
739
# As well as some sort of trap for missing ids?
652
740
return self.inventory_store[inventory_id]
654
742
get_inventory_xml_file = get_inventory_xml
744
def get_inventories(self, inventory_ids, pb=None, ignore_missing=False):
745
"""Get Inventory objects by id
747
from bzrlib.inventory import Inventory
749
# See the discussion in get_revisions for why
750
# we don't use a try/finally block here
752
for f in self.inventory_store.get(inventory_ids, pb=pb, ignore_missing=ignore_missing):
754
# TODO: Possibly put a try/except around this to handle
755
# read serialization errors
756
r = bzrlib.xml.serializer_v4.read_inventory(f)
761
raise bzrlib.errors.NoSuchRevision(self, revision_id)
657
764
def get_inventory_sha1(self, inventory_id):
658
765
"""Return the sha1 hash of the inventory entry
822
929
def install_revisions(self, other, revision_ids, pb):
930
# We are going to iterate this many times, so make sure
931
# that it is a list, and not a generator
932
revision_ids = list(revision_ids)
823
933
if hasattr(other.revision_store, "prefetch"):
824
934
other.revision_store.prefetch(revision_ids)
825
935
if hasattr(other.inventory_store, "prefetch"):
827
for rev_id in revision_ids:
829
revision = other.get_revision(rev_id).inventory_id
830
inventory_ids.append(revision)
831
except bzrlib.errors.NoSuchRevision:
833
936
other.inventory_store.prefetch(inventory_ids)
836
939
pb = bzrlib.ui.ui_factory.progress_bar()
941
# This entire next section is generally done
942
# with either generators, or bulk updates
943
inventories = other.get_inventories(revision_ids, ignore_missing=True)
839
944
needed_texts = set()
843
for i, rev_id in enumerate(revision_ids):
947
good_revisions = set()
948
for i, (inv, rev_id) in enumerate(zip(inventories, revision_ids)):
844
949
pb.update('fetching revision', i+1, len(revision_ids))
846
rev = other.get_revision(rev_id)
847
except bzrlib.errors.NoSuchRevision:
951
# We don't really need to get the revision here, because
952
# the only thing we needed was the inventory_id, which now
953
# is (by design) identical to the revision_id
955
# rev = other.get_revision(rev_id)
956
# except bzrlib.errors.NoSuchRevision:
957
# failures.add(rev_id)
848
961
failures.add(rev_id)
964
good_revisions.add(rev_id)
851
revisions.append(rev)
852
inv = other.get_inventory(str(rev.inventory_id))
853
967
for key, entry in inv.iter_entries():
854
968
if entry.text_id is None:
856
if entry.text_id not in self.text_store:
857
needed_texts.add(entry.text_id)
970
text_ids.append(entry.text_id)
972
has_ids = self.text_store.has(text_ids)
973
for has, text_id in zip(has_ids, text_ids):
975
needed_texts.add(text_id)
861
979
count, cp_fail = self.text_store.copy_multi(other.text_store,
863
981
#print "Added %d texts." % count
864
inventory_ids = [ f.inventory_id for f in revisions ]
865
count, cp_fail = self.inventory_store.copy_multi(other.inventory_store,
982
count, cp_fail = self.inventory_store.copy_multi(other.inventory_store,
867
984
#print "Added %d inventories." % count
868
revision_ids = [ f.revision_id for f in revisions]
870
985
count, cp_fail = self.revision_store.copy_multi(other.revision_store,
872
987
permit_failure=True)
873
988
assert len(cp_fail) == 0
874
989
return count, failures
1317
def add_pending_merge(self, revision_id):
1433
def add_pending_merge(self, *revision_ids):
1318
1434
from bzrlib.revision import validate_revision_id
1320
validate_revision_id(revision_id)
1436
for rev_id in revision_ids:
1437
validate_revision_id(rev_id)
1322
1439
p = self.pending_merges()
1323
if revision_id in p:
1325
p.append(revision_id)
1326
self.set_pending_merges(p)
1441
for rev_id in revision_ids:
1447
self.set_pending_merges(p)
1329
1449
def set_pending_merges(self, rev_list):
1330
from bzrlib.atomicfile import AtomicFile
1331
1450
self.lock_write()
1333
f = AtomicFile(self.controlfilename('pending-merges'))
1452
self.put_controlfile('pending-merges', '\n'.join(rev_list))