15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21
from warnings import warn
22
from cStringIO import StringIO
26
from bzrlib.inventory import InventoryEntry
27
import bzrlib.inventory as inventory
28
21
from bzrlib.trace import mutter, note
29
from bzrlib.osutils import (isdir, quotefn, compact_date, rand_bytes,
30
rename, splitpath, sha_file, appendpath,
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)
37
from bzrlib.textui import show_status
38
from bzrlib.revision import Revision, is_ancestor, get_intervening_revisions
40
from bzrlib.delta import compare_trees
41
from bzrlib.tree import EmptyTree, RevisionTree
42
from bzrlib.inventory import Inventory
43
from bzrlib.store import copy_all
44
from bzrlib.store.compressed_text import CompressedTextStore
45
from bzrlib.store.text import TextStore
46
from bzrlib.store.weave import WeaveStore
47
from bzrlib.testament import Testament
48
import bzrlib.transactions as transactions
49
from bzrlib.transport import Transport, get_transport
54
BZR_BRANCH_FORMAT_4 = "Bazaar-NG branch, format 0.0.4\n"
55
BZR_BRANCH_FORMAT_5 = "Bazaar-NG branch, format 5\n"
56
BZR_BRANCH_FORMAT_6 = "Bazaar-NG branch, format 6\n"
22
from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, splitpath, \
23
sha_file, appendpath, file_kind
24
from bzrlib.errors import BzrError
26
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
57
27
## TODO: Maybe include checks for common corruption of newlines, etc?
60
# TODO: Some operations like log might retrieve the same revisions
61
# repeatedly to calculate deltas. We could perhaps have a weakref
62
# cache in memory to make this faster. In general anything can be
63
# cached in memory between lock and unlock operations.
65
def find_branch(*ignored, **ignored_too):
66
# XXX: leave this here for about one release, then remove it
67
raise NotImplementedError('find_branch() is not supported anymore, '
68
'please use one of the new branch constructors')
31
def find_branch(f, **args):
32
if f and (f.startswith('http://') or f.startswith('https://')):
34
return remotebranch.RemoteBranch(f, **args)
36
return Branch(f, **args)
39
def find_cached_branch(f, cache_root, **args):
40
from remotebranch import RemoteBranch
41
br = find_branch(f, **args)
42
def cacheify(br, store_name):
43
from meta_store import CachedStore
44
cache_path = os.path.join(cache_root, store_name)
46
new_store = CachedStore(getattr(br, store_name), cache_path)
47
setattr(br, store_name, new_store)
49
if isinstance(br, RemoteBranch):
50
cacheify(br, 'inventory_store')
51
cacheify(br, 'text_store')
52
cacheify(br, 'revision_store')
56
def _relpath(base, path):
57
"""Return path relative to base, or raise exception.
59
The path may be either an absolute path or a path relative to the
60
current working directory.
62
Lifted out of Branch.relpath for ease of testing.
64
os.path.commonprefix (python2.4) has a bad bug that it works just
65
on string prefixes, assuming that '/u' is a prefix of '/u2'. This
66
avoids that problem."""
67
rp = os.path.abspath(path)
71
while len(head) >= len(base):
74
head, tail = os.path.split(head)
78
from errors import NotBranchError
79
raise NotBranchError("path %r is not within branch %r" % (rp, base))
84
def find_branch_root(f=None):
85
"""Find the branch root enclosing f, or pwd.
87
f may be a filename or a URL.
89
It is not necessary that f exists.
91
Basically we keep looking up until we find the control directory or
95
elif hasattr(os.path, 'realpath'):
96
f = os.path.realpath(f)
98
f = os.path.abspath(f)
99
if not os.path.exists(f):
100
raise BzrError('%r does not exist' % f)
106
if os.path.exists(os.path.join(f, bzrlib.BZRDIR)):
108
head, tail = os.path.split(f)
110
# reached the root, whatever that may be
111
raise BzrError('%r is not in a branch' % orig_f)
114
class DivergedBranches(Exception):
115
def __init__(self, branch1, branch2):
116
self.branch1 = branch1
117
self.branch2 = branch2
118
Exception.__init__(self, "These branches have diverged.")
121
class NoSuchRevision(BzrError):
122
def __init__(self, branch, revision):
124
self.revision = revision
125
msg = "Branch %s has no revision %d" % (branch, revision)
126
BzrError.__init__(self, msg)
70
129
######################################################################
74
133
"""Branch holding a history of revisions.
77
Base directory/url of the branch.
81
def __init__(self, *ignored, **ignored_too):
82
raise NotImplementedError('The Branch class is abstract')
85
def open_downlevel(base):
86
"""Open a branch which may be of an old format.
88
Only local branches are supported."""
89
return _Branch(get_transport(base), relax_version_check=True)
93
"""Open an existing branch, rooted at 'base' (url)"""
94
t = get_transport(base)
95
mutter("trying to open %r with transport %r", base, t)
99
def open_containing(url):
100
"""Open an existing branch which contains url.
102
This probes for a branch at url, and searches upwards from there.
104
Basically we keep looking up until we find the control directory or
105
run into the root. If there isn't one, raises NotBranchError.
107
t = get_transport(url)
111
except NotBranchError:
113
new_t = t.clone('..')
114
if new_t.base == t.base:
115
# reached the root, whatever that may be
116
raise NotBranchError('%s is not in a branch' % url)
120
def initialize(base):
121
"""Create a new branch, rooted at 'base' (url)"""
122
t = get_transport(base)
123
return _Branch(t, init=True)
125
def setup_caching(self, cache_root):
126
"""Subclasses that care about caching should override this, and set
127
up cached stores located under cache_root.
129
self.cache_root = cache_root
132
class _Branch(Branch):
133
"""A branch stored in the actual filesystem.
135
Note that it's "local" in the context of the filesystem; it doesn't
136
really matter if it's on an nfs/smb/afs/coda/... share, as long as
137
it's writable, and can be accessed via the normal filesystem API.
136
Base directory of the branch.
140
139
None, or 'r' or 'w'
147
146
Lock object from bzrlib.lock.
149
# We actually expect this class to be somewhat short-lived; part of its
150
# purpose is to try to isolate what bits of the branch logic are tied to
151
# filesystem access, so that in a later step, we can extricate them to
152
# a separarte ("storage") class.
153
149
_lock_mode = None
154
150
_lock_count = None
156
_inventory_weave = None
158
# Map some sort of prefix into a namespace
159
# stuff like "revno:10", "revid:", etc.
160
# This should match a prefix with a function which accepts
161
REVISION_NAMESPACES = {}
163
def push_stores(self, branch_to):
164
"""Copy the content of this branches store to branch_to."""
165
if (self._branch_format != branch_to._branch_format
166
or self._branch_format != 4):
167
from bzrlib.fetch import greedy_fetch
168
mutter("falling back to fetch logic to push between %s(%s) and %s(%s)",
169
self, self._branch_format, branch_to, branch_to._branch_format)
170
greedy_fetch(to_branch=branch_to, from_branch=self,
171
revision=self.last_revision())
174
store_pairs = ((self.text_store, branch_to.text_store),
175
(self.inventory_store, branch_to.inventory_store),
176
(self.revision_store, branch_to.revision_store))
178
for from_store, to_store in store_pairs:
179
copy_all(from_store, to_store)
180
except UnlistableStore:
181
raise UnlistableBranch(from_store)
183
def __init__(self, transport, init=False,
184
relax_version_check=False):
153
def __init__(self, base, init=False, find_root=True):
185
154
"""Create new branch object at a particular location.
187
transport -- A Transport object, defining how to access files.
188
(If a string, transport.transport() will be used to
189
create a Transport object)
156
base -- Base directory for the branch.
191
158
init -- If True, create new control files in a previously
192
159
unversioned directory. If False, the branch must already
195
relax_version_check -- If true, the usual check for the branch
196
version is not applied. This is intended only for
197
upgrade/recovery type use; it's not guaranteed that
198
all operations will work on old format branches.
162
find_root -- If true and init is false, find the root of the
163
existing branch containing base.
200
165
In the test suite, creation of new trees is tested using the
201
166
`ScratchBranch` class.
203
assert isinstance(transport, Transport), \
204
"%r is not a Transport" % transport
205
self._transport = transport
168
from bzrlib.store import ImmutableStore
170
self.base = os.path.realpath(base)
207
171
self._make_control()
208
self._check_format(relax_version_check)
210
def get_store(name, compressed=True, prefixed=False):
211
# FIXME: This approach of assuming stores are all entirely compressed
212
# or entirely uncompressed is tidy, but breaks upgrade from
213
# some existing branches where there's a mixture; we probably
214
# still want the option to look for both.
215
relpath = self._rel_controlfilename(name)
217
store = CompressedTextStore(self._transport.clone(relpath),
220
store = TextStore(self._transport.clone(relpath),
222
#if self._transport.should_cache():
223
# cache_path = os.path.join(self.cache_root, name)
224
# os.mkdir(cache_path)
225
# store = bzrlib.store.CachedStore(store, cache_path)
227
def get_weave(name, prefixed=False):
228
relpath = self._rel_controlfilename(name)
229
ws = WeaveStore(self._transport.clone(relpath), prefixed=prefixed)
230
if self._transport.should_cache():
231
ws.enable_cache = True
234
if self._branch_format == 4:
235
self.inventory_store = get_store('inventory-store')
236
self.text_store = get_store('text-store')
237
self.revision_store = get_store('revision-store')
238
elif self._branch_format == 5:
239
self.control_weaves = get_weave([])
240
self.weave_store = get_weave('weaves')
241
self.revision_store = get_store('revision-store', compressed=False)
242
elif self._branch_format == 6:
243
self.control_weaves = get_weave([])
244
self.weave_store = get_weave('weaves', prefixed=True)
245
self.revision_store = get_store('revision-store', compressed=False,
247
self.revision_store.register_suffix('sig')
248
self._transaction = None
173
self.base = find_branch_root(base)
175
self.base = os.path.realpath(base)
176
if not isdir(self.controlfilename('.')):
177
from errors import NotBranchError
178
raise NotBranchError("not a bzr branch: %s" % quotefn(base),
179
['use "bzr init" to initialize a new working tree',
180
'current bzr can only operate from top-of-tree'])
183
self.text_store = ImmutableStore(self.controlfilename('text-store'))
184
self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
185
self.inventory_store = ImmutableStore(self.controlfilename('inventory-store'))
250
188
def __str__(self):
251
return '%s(%r)' % (self.__class__.__name__, self._transport.base)
189
return '%s(%r)' % (self.__class__.__name__, self.base)
254
192
__repr__ = __str__
257
195
def __del__(self):
258
196
if self._lock_mode or self._lock:
259
# XXX: This should show something every time, and be suitable for
260
# headless operation and embedding
197
from warnings import warn
261
198
warn("branch %r was not explicitly unlocked" % self)
262
199
self._lock.unlock()
264
# TODO: It might be best to do this somewhere else,
265
# but it is nice for a Branch object to automatically
266
# cache it's information.
267
# Alternatively, we could have the Transport objects cache requests
268
# See the earlier discussion about how major objects (like Branch)
269
# should never expect their __del__ function to run.
270
if hasattr(self, 'cache_root') and self.cache_root is not None:
273
shutil.rmtree(self.cache_root)
276
self.cache_root = None
280
return self._transport.base
283
base = property(_get_base, doc="The URL for the root of this branch.")
285
def _finish_transaction(self):
286
"""Exit the current transaction."""
287
if self._transaction is None:
288
raise errors.LockError('Branch %s is not in a transaction' %
290
transaction = self._transaction
291
self._transaction = None
294
def get_transaction(self):
295
"""Return the current active transaction.
297
If no transaction is active, this returns a passthrough object
298
for which all data is immedaitely flushed and no caching happens.
300
if self._transaction is None:
301
return transactions.PassThroughTransaction()
303
return self._transaction
305
def _set_transaction(self, new_transaction):
306
"""Set a new active transaction."""
307
if self._transaction is not None:
308
raise errors.LockError('Branch %s is in a transaction already.' %
310
self._transaction = new_transaction
312
203
def lock_write(self):
313
mutter("lock write: %s (%s)", self, self._lock_count)
314
# TODO: Upgrade locking to support using a Transport,
315
# and potentially a remote locking protocol
316
204
if self._lock_mode:
317
205
if self._lock_mode != 'w':
206
from errors import LockError
318
207
raise LockError("can't upgrade to a write lock from %r" %
320
209
self._lock_count += 1
322
self._lock = self._transport.lock_write(
323
self._rel_controlfilename('branch-lock'))
211
from bzrlib.lock import WriteLock
213
self._lock = WriteLock(self.controlfilename('branch-lock'))
324
214
self._lock_mode = 'w'
325
215
self._lock_count = 1
326
self._set_transaction(transactions.PassThroughTransaction())
328
219
def lock_read(self):
329
mutter("lock read: %s (%s)", self, self._lock_count)
330
220
if self._lock_mode:
331
221
assert self._lock_mode in ('r', 'w'), \
332
222
"invalid lock mode %r" % self._lock_mode
333
223
self._lock_count += 1
335
self._lock = self._transport.lock_read(
336
self._rel_controlfilename('branch-lock'))
225
from bzrlib.lock import ReadLock
227
self._lock = ReadLock(self.controlfilename('branch-lock'))
337
228
self._lock_mode = 'r'
338
229
self._lock_count = 1
339
self._set_transaction(transactions.ReadOnlyTransaction())
340
# 5K may be excessive, but hey, its a knob.
341
self.get_transaction().set_cache_size(5000)
343
233
def unlock(self):
344
mutter("unlock: %s (%s)", self, self._lock_count)
345
234
if not self._lock_mode:
235
from errors import LockError
346
236
raise LockError('branch %r is not locked' % (self))
348
238
if self._lock_count > 1:
349
239
self._lock_count -= 1
351
self._finish_transaction()
352
241
self._lock.unlock()
353
242
self._lock = None
354
243
self._lock_mode = self._lock_count = None
356
246
def abspath(self, name):
357
"""Return absolute filename for something in the branch
359
XXX: Robert Collins 20051017 what is this used for? why is it a branch
360
method and not a tree method.
362
return self._transport.abspath(name)
364
def _rel_controlfilename(self, file_or_path):
247
"""Return absolute filename for something in the branch"""
248
return os.path.join(self.base, name)
251
def relpath(self, path):
252
"""Return path relative to this branch of something inside it.
254
Raises an error if path is not in this branch."""
255
return _relpath(self.base, path)
258
def controlfilename(self, file_or_path):
259
"""Return location relative to branch."""
365
260
if isinstance(file_or_path, basestring):
366
261
file_or_path = [file_or_path]
367
return [bzrlib.BZRDIR] + file_or_path
369
def controlfilename(self, file_or_path):
370
"""Return location relative to branch."""
371
return self._transport.abspath(self._rel_controlfilename(file_or_path))
262
return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
374
265
def controlfile(self, file_or_path, mode='r'):
382
273
Controlfiles should almost never be opened in write mode but
383
274
rather should be atomically copied and replaced using atomicfile.
387
relpath = self._rel_controlfilename(file_or_path)
388
#TODO: codecs.open() buffers linewise, so it was overloaded with
389
# a much larger buffer, do we need to do the same for getreader/getwriter?
391
return self._transport.get(relpath)
393
raise BzrError("Branch.controlfile(mode='wb') is not supported, use put_controlfiles")
395
return codecs.getreader('utf-8')(self._transport.get(relpath), errors='replace')
397
raise BzrError("Branch.controlfile(mode='w') is not supported, use put_controlfiles")
277
fn = self.controlfilename(file_or_path)
279
if mode == 'rb' or mode == 'wb':
280
return file(fn, mode)
281
elif mode == 'r' or mode == 'w':
282
# open in binary mode anyhow so there's no newline translation;
283
# codecs uses line buffering by default; don't want that.
285
return codecs.open(fn, mode + 'b', 'utf-8',
399
288
raise BzrError("invalid controlfile mode %r" % mode)
401
def put_controlfile(self, path, f, encode=True):
402
"""Write an entry as a controlfile.
404
:param path: The path to put the file, relative to the .bzr control
406
:param f: A file-like or string object whose contents should be copied.
407
:param encode: If true, encode the contents as utf-8
409
self.put_controlfiles([(path, f)], encode=encode)
411
def put_controlfiles(self, files, encode=True):
412
"""Write several entries as controlfiles.
414
:param files: A list of [(path, file)] pairs, where the path is the directory
415
underneath the bzr control directory
416
:param encode: If true, encode the contents as utf-8
420
for path, f in files:
422
if isinstance(f, basestring):
423
f = f.encode('utf-8', 'replace')
425
f = codecs.getwriter('utf-8')(f, errors='replace')
426
path = self._rel_controlfilename(path)
427
ctrl_files.append((path, f))
428
self._transport.put_multi(ctrl_files)
430
292
def _make_control(self):
431
293
from bzrlib.inventory import Inventory
432
from bzrlib.weavefile import write_weave_v5
433
from bzrlib.weave import Weave
294
from bzrlib.xml import pack_xml
435
# Create an empty inventory
437
# if we want per-tree root ids then this is the place to set
438
# them; they're not needed for now and so ommitted for
440
bzrlib.xml5.serializer_v5.write_inventory(Inventory(), sio)
441
empty_inv = sio.getvalue()
443
bzrlib.weavefile.write_weave_v5(Weave(), sio)
444
empty_weave = sio.getvalue()
446
dirs = [[], 'revision-store', 'weaves']
296
os.mkdir(self.controlfilename([]))
297
self.controlfile('README', 'w').write(
448
298
"This is a Bazaar-NG control directory.\n"
449
"Do not change any files in this directory.\n"),
450
('branch-format', BZR_BRANCH_FORMAT_6),
451
('revision-history', ''),
454
('pending-merges', ''),
455
('inventory', empty_inv),
456
('inventory.weave', empty_weave),
457
('ancestry.weave', empty_weave)
459
cfn = self._rel_controlfilename
460
self._transport.mkdir_multi([cfn(d) for d in dirs])
461
self.put_controlfiles(files)
462
mutter('created control directory in ' + self._transport.base)
464
def _check_format(self, relax_version_check):
299
"Do not change any files in this directory.\n")
300
self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT)
301
for d in ('text-store', 'inventory-store', 'revision-store'):
302
os.mkdir(self.controlfilename(d))
303
for f in ('revision-history', 'merged-patches',
304
'pending-merged-patches', 'branch-name',
307
self.controlfile(f, 'w').write('')
308
mutter('created control directory in ' + self.base)
310
pack_xml(Inventory(), self.controlfile('inventory','w'))
313
def _check_format(self):
465
314
"""Check this branch format is supported.
467
The format level is stored, as an integer, in
468
self._branch_format for code that needs to check it later.
316
The current tool only supports the current unstable format.
470
318
In the future, we might need different in-memory Branch
471
319
classes to support downlevel branches. But not yet.
474
fmt = self.controlfile('branch-format', 'r').read()
476
raise NotBranchError(self.base)
477
mutter("got branch format %r", fmt)
478
if fmt == BZR_BRANCH_FORMAT_6:
479
self._branch_format = 6
480
elif fmt == BZR_BRANCH_FORMAT_5:
481
self._branch_format = 5
482
elif fmt == BZR_BRANCH_FORMAT_4:
483
self._branch_format = 4
485
if (not relax_version_check
486
and self._branch_format not in (5, 6)):
487
raise errors.UnsupportedFormatError(
488
'sorry, branch format %r not supported' % fmt,
321
# This ignores newlines so that we can open branches created
322
# on Windows from Linux and so on. I think it might be better
323
# to always make all internal files in unix format.
324
fmt = self.controlfile('branch-format', 'r').read()
325
fmt.replace('\r\n', '')
326
if fmt != BZR_BRANCH_FORMAT:
327
raise BzrError('sorry, branch format %r not supported' % fmt,
489
328
['use a different bzr version',
490
'or remove the .bzr directory'
491
' and "bzr init" again'])
493
def get_root_id(self):
494
"""Return the id of this branches root"""
495
inv = self.read_working_inventory()
496
return inv.root.file_id
498
def set_root_id(self, file_id):
499
inv = self.read_working_inventory()
500
orig_root_id = inv.root.file_id
501
del inv._byid[inv.root.file_id]
502
inv.root.file_id = file_id
503
inv._byid[inv.root.file_id] = inv.root
506
if entry.parent_id in (None, orig_root_id):
507
entry.parent_id = inv.root.file_id
508
self._write_inventory(inv)
329
'or remove the .bzr directory and "bzr init" again'])
510
333
def read_working_inventory(self):
511
334
"""Read the working inventory."""
335
from bzrlib.inventory import Inventory
336
from bzrlib.xml import unpack_xml
337
from time import time
514
341
# ElementTree does its own conversion from UTF-8, so open in
516
f = self.controlfile('inventory', 'rb')
517
return bzrlib.xml5.serializer_v5.read_inventory(f)
343
inv = unpack_xml(Inventory,
344
self.controlfile('inventory', 'rb'))
345
mutter("loaded inventory of %d items in %f"
346
% (len(inv), time() - before))
707
544
return self.working_tree().unknowns()
710
def append_revision(self, *revision_ids):
711
for revision_id in revision_ids:
712
mutter("add {%s} to revision-history" % revision_id)
715
rev_history = self.revision_history()
716
rev_history.extend(revision_ids)
717
self.put_controlfile('revision-history', '\n'.join(rev_history))
721
def has_revision(self, revision_id):
722
"""True if this branch has a copy of the revision.
724
This does not necessarily imply the revision is merge
725
or on the mainline."""
726
return (revision_id is None
727
or self.revision_store.has_id(revision_id))
729
def get_revision_xml_file(self, revision_id):
730
"""Return XML file object for revision object."""
731
if not revision_id or not isinstance(revision_id, basestring):
732
raise InvalidRevisionId(revision_id)
737
return self.revision_store.get(revision_id)
738
except (IndexError, KeyError):
739
raise bzrlib.errors.NoSuchRevision(self, revision_id)
744
get_revision_xml = get_revision_xml_file
746
def get_revision_xml(self, revision_id):
747
return self.get_revision_xml_file(revision_id).read()
547
def append_revision(self, revision_id):
548
from bzrlib.atomicfile import AtomicFile
550
mutter("add {%s} to revision-history" % revision_id)
551
rev_history = self.revision_history() + [revision_id]
553
f = AtomicFile(self.controlfilename('revision-history'))
555
for rev_id in rev_history:
750
562
def get_revision(self, revision_id):
751
563
"""Return the Revision object for a named revision"""
752
xml_file = self.get_revision_xml_file(revision_id)
564
from bzrlib.revision import Revision
565
from bzrlib.xml import unpack_xml
755
r = bzrlib.xml5.serializer_v5.read_revision(xml_file)
756
except SyntaxError, e:
757
raise bzrlib.errors.BzrError('failed to unpack revision_xml',
569
if not revision_id or not isinstance(revision_id, basestring):
570
raise ValueError('invalid revision-id: %r' % revision_id)
571
r = unpack_xml(Revision, self.revision_store[revision_id])
761
575
assert r.revision_id == revision_id
764
def get_revision_delta(self, revno):
765
"""Return the delta for one revision.
767
The delta is relative to its mainline predecessor, or the
768
empty tree for revision 1.
770
assert isinstance(revno, int)
771
rh = self.revision_history()
772
if not (1 <= revno <= len(rh)):
773
raise InvalidRevisionNumber(revno)
775
# revno is 1-based; list is 0-based
777
new_tree = self.revision_tree(rh[revno-1])
779
old_tree = EmptyTree()
781
old_tree = self.revision_tree(rh[revno-2])
783
return compare_trees(old_tree, new_tree)
785
579
def get_revision_sha1(self, revision_id):
786
580
"""Hash the stored value of a revision, and return it."""
790
584
# the revision, (add signatures/remove signatures) and still
791
585
# have all hash pointers stay consistent.
792
586
# But for now, just hash the contents.
793
return bzrlib.osutils.sha_file(self.get_revision_xml_file(revision_id))
795
def get_ancestry(self, revision_id):
796
"""Return a list of revision-ids integrated by a revision.
798
This currently returns a list, but the ordering is not guaranteed:
801
if revision_id is None:
803
w = self.get_inventory_weave()
804
return [None] + map(w.idx_to_name,
805
w.inclusions([w.lookup(revision_id)]))
807
def get_inventory_weave(self):
808
return self.control_weaves.get_weave('inventory',
809
self.get_transaction())
811
def get_inventory(self, revision_id):
812
"""Get Inventory object by hash."""
813
xml = self.get_inventory_xml(revision_id)
814
return bzrlib.xml5.serializer_v5.read_inventory_from_string(xml)
816
def get_inventory_xml(self, revision_id):
817
"""Get inventory XML as a file object."""
819
assert isinstance(revision_id, basestring), type(revision_id)
820
iw = self.get_inventory_weave()
821
return iw.get_text(iw.lookup(revision_id))
823
raise bzrlib.errors.HistoryMissing(self, 'inventory', revision_id)
825
def get_inventory_sha1(self, revision_id):
587
return sha_file(self.revision_store[revision_id])
590
def get_inventory(self, inventory_id):
591
"""Get Inventory object by hash.
593
TODO: Perhaps for this and similar methods, take a revision
594
parameter which can be either an integer revno or a
596
from bzrlib.inventory import Inventory
597
from bzrlib.xml import unpack_xml
599
return unpack_xml(Inventory, self.inventory_store[inventory_id])
602
def get_inventory_sha1(self, inventory_id):
826
603
"""Return the sha1 hash of the inventory entry
828
return self.get_revision(revision_id).inventory_sha1
605
return sha_file(self.inventory_store[inventory_id])
830
608
def get_revision_inventory(self, revision_id):
831
609
"""Return inventory of a past revision."""
832
# TODO: Unify this with get_inventory()
833
# bzr 0.0.6 and later imposes the constraint that the inventory_id
610
# bzr 0.0.6 imposes the constraint that the inventory_id
834
611
# must be the same as its revision, so this is trivial.
835
612
if revision_id == None:
836
return Inventory(self.get_root_id())
613
from bzrlib.inventory import Inventory
838
616
return self.get_inventory(revision_id)
840
619
def revision_history(self):
841
"""Return sequence of revision hashes on to this branch."""
620
"""Return sequence of revision hashes on to this branch.
622
>>> ScratchBranch().revision_history()
844
transaction = self.get_transaction()
845
history = transaction.map.find_revision_history()
846
if history is not None:
847
mutter("cache hit for revision-history in %s", self)
849
history = [l.rstrip('\r\n') for l in
627
return [l.rstrip('\r\n') for l in
850
628
self.controlfile('revision-history', 'r').readlines()]
851
transaction.map.add_revision_history(history)
852
# this call is disabled because revision_history is
853
# not really an object yet, and the transaction is for objects.
854
# transaction.register_clean(history, precious=True)
633
def common_ancestor(self, other, self_revno=None, other_revno=None):
636
>>> sb = ScratchBranch(files=['foo', 'foo~'])
637
>>> sb.common_ancestor(sb) == (None, None)
639
>>> commit.commit(sb, "Committing first revision", verbose=False)
640
>>> sb.common_ancestor(sb)[0]
642
>>> clone = sb.clone()
643
>>> commit.commit(sb, "Committing second revision", verbose=False)
644
>>> sb.common_ancestor(sb)[0]
646
>>> sb.common_ancestor(clone)[0]
648
>>> commit.commit(clone, "Committing divergent second revision",
650
>>> sb.common_ancestor(clone)[0]
652
>>> sb.common_ancestor(clone) == clone.common_ancestor(sb)
654
>>> sb.common_ancestor(sb) != clone.common_ancestor(clone)
656
>>> clone2 = sb.clone()
657
>>> sb.common_ancestor(clone2)[0]
659
>>> sb.common_ancestor(clone2, self_revno=1)[0]
661
>>> sb.common_ancestor(clone2, other_revno=1)[0]
664
my_history = self.revision_history()
665
other_history = other.revision_history()
666
if self_revno is None:
667
self_revno = len(my_history)
668
if other_revno is None:
669
other_revno = len(other_history)
670
indices = range(min((self_revno, other_revno)))
673
if my_history[r] == other_history[r]:
674
return r+1, my_history[r]
677
def enum_history(self, direction):
678
"""Return (revno, revision_id) for history of branch.
681
'forward' is from earliest to latest
682
'reverse' is from latest to earliest
684
rh = self.revision_history()
685
if direction == 'forward':
690
elif direction == 'reverse':
696
raise ValueError('invalid history direction', direction)
860
700
"""Return current revision number for this branch.
915
754
if stop_revision is None:
916
755
stop_revision = other_len
918
assert isinstance(stop_revision, int)
919
if stop_revision > other_len:
920
raise bzrlib.errors.NoSuchRevision(self, stop_revision)
756
elif stop_revision > other_len:
757
raise NoSuchRevision(self, stop_revision)
921
759
return other_history[self_len:stop_revision]
923
762
def update_revisions(self, other, stop_revision=None):
924
"""Pull in new perfect-fit revisions."""
925
# FIXME: If the branches have diverged, but the latest
926
# revision in this branch is completely merged into the other,
927
# then we should still be able to pull.
928
from bzrlib.fetch import greedy_fetch
929
if stop_revision is None:
930
stop_revision = other.last_revision()
931
### Should this be checking is_ancestor instead of revision_history?
932
if (stop_revision is not None and
933
stop_revision in self.revision_history()):
935
greedy_fetch(to_branch=self, from_branch=other,
936
revision=stop_revision)
937
pullable_revs = self.pullable_revisions(other, stop_revision)
938
if len(pullable_revs) > 0:
939
self.append_revision(*pullable_revs)
941
def pullable_revisions(self, other, stop_revision):
942
other_revno = other.revision_id_to_revno(stop_revision)
763
"""Pull in all new revisions from other branch.
765
>>> from bzrlib.commit import commit
766
>>> bzrlib.trace.silent = True
767
>>> br1 = ScratchBranch(files=['foo', 'bar'])
770
>>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False)
771
>>> br2 = ScratchBranch()
772
>>> br2.update_revisions(br1)
776
>>> br2.revision_history()
778
>>> br2.update_revisions(br1)
782
>>> br1.text_store.total_size() == br2.text_store.total_size()
785
from bzrlib.progress import ProgressBar
944
return self.missing_revisions(other, other_revno)
945
except DivergedBranches, e:
947
pullable_revs = get_intervening_revisions(self.last_revision(),
949
assert self.last_revision() not in pullable_revs
951
except bzrlib.errors.NotAncestor:
952
if is_ancestor(self.last_revision(), stop_revision, self):
789
from sets import Set as set
793
pb.update('comparing histories')
794
revision_ids = self.missing_revisions(other, stop_revision)
796
if hasattr(other.revision_store, "prefetch"):
797
other.revision_store.prefetch(revision_ids)
798
if hasattr(other.inventory_store, "prefetch"):
799
inventory_ids = [other.get_revision(r).inventory_id
800
for r in revision_ids]
801
other.inventory_store.prefetch(inventory_ids)
806
for rev_id in revision_ids:
808
pb.update('fetching revision', i, len(revision_ids))
809
rev = other.get_revision(rev_id)
810
revisions.append(rev)
811
inv = other.get_inventory(str(rev.inventory_id))
812
for key, entry in inv.iter_entries():
813
if entry.text_id is None:
815
if entry.text_id not in self.text_store:
816
needed_texts.add(entry.text_id)
820
count = self.text_store.copy_multi(other.text_store, needed_texts)
821
print "Added %d texts." % count
822
inventory_ids = [ f.inventory_id for f in revisions ]
823
count = self.inventory_store.copy_multi(other.inventory_store,
825
print "Added %d inventories." % count
826
revision_ids = [ f.revision_id for f in revisions]
827
count = self.revision_store.copy_multi(other.revision_store,
829
for revision_id in revision_ids:
830
self.append_revision(revision_id)
831
print "Added %d revisions." % count
957
834
def commit(self, *args, **kw):
958
from bzrlib.commit import Commit
959
Commit().commit(self, *args, **kw)
961
def revision_id_to_revno(self, revision_id):
962
"""Given a revision id, return its revno"""
963
if revision_id is None:
965
history = self.revision_history()
967
return history.index(revision_id) + 1
969
raise bzrlib.errors.NoSuchRevision(self, revision_id)
835
from bzrlib.commit import commit
836
commit(self, *args, **kw)
971
def get_rev_id(self, revno, history=None):
972
"""Find the revision id of the specified revno."""
839
def lookup_revision(self, revno):
840
"""Return revision hash for revision number."""
976
history = self.revision_history()
977
elif revno <= 0 or revno > len(history):
978
raise bzrlib.errors.NoSuchRevision(self, revno)
979
return history[revno - 1]
845
# list is 0-based; revisions are 1-based
846
return self.revision_history()[revno-1]
848
raise BzrError("no such revision %s" % revno)
981
851
def revision_tree(self, revision_id):
982
852
"""Return Tree for a revision on this branch.
984
854
`revision_id` may be None for the null revision, in which case
985
855
an `EmptyTree` is returned."""
856
from bzrlib.tree import EmptyTree, RevisionTree
986
857
# TODO: refactor this to use an existing revision object
987
858
# so we don't need to read it in twice.
988
859
if revision_id == None:
989
860
return EmptyTree()
991
862
inv = self.get_revision_inventory(revision_id)
992
return RevisionTree(self.weave_store, inv, revision_id)
863
return RevisionTree(self.text_store, inv)
994
866
def working_tree(self):
995
867
"""Return a `Tree` for the working copy."""
996
from bzrlib.workingtree import WorkingTree
997
# TODO: In the future, WorkingTree should utilize Transport
998
# RobertCollins 20051003 - I don't think it should - working trees are
999
# much more complex to keep consistent than our careful .bzr subset.
1000
# instead, we should say that working trees are local only, and optimise
1002
return WorkingTree(self.base, branch=self)
868
from workingtree import WorkingTree
869
return WorkingTree(self.base, self.read_working_inventory())
1005
872
def basis_tree(self):
1191
def add_pending_merge(self, *revision_ids):
1192
# TODO: Perhaps should check at this point that the
1193
# history of the revision is actually present?
1060
def add_pending_merge(self, revision_id):
1061
from bzrlib.revision import validate_revision_id
1063
validate_revision_id(revision_id)
1194
1065
p = self.pending_merges()
1196
for rev_id in revision_ids:
1202
self.set_pending_merges(p)
1066
if revision_id in p:
1068
p.append(revision_id)
1069
self.set_pending_merges(p)
1204
1072
def set_pending_merges(self, rev_list):
1207
self.put_controlfile('pending-merges', '\n'.join(rev_list))
1212
def get_parent(self):
1213
"""Return the parent location of the branch.
1215
This is the default location for push/pull/missing. The usual
1216
pattern is that the user can override it by specifying a
1220
_locs = ['parent', 'pull', 'x-pull']
1223
return self.controlfile(l, 'r').read().strip('\n')
1225
if e.errno != errno.ENOENT:
1230
def set_parent(self, url):
1231
# TODO: Maybe delete old location files?
1232
1073
from bzrlib.atomicfile import AtomicFile
1233
1074
self.lock_write()
1235
f = AtomicFile(self.controlfilename('parent'))
1076
f = AtomicFile(self.controlfilename('pending-merges'))
1244
def check_revno(self, revno):
1246
Check whether a revno corresponds to any revision.
1247
Zero (the NULL revision) is considered valid.
1250
self.check_real_revno(revno)
1252
def check_real_revno(self, revno):
1254
Check whether a revno corresponds to a real revision.
1255
Zero (the NULL revision) is considered invalid
1257
if revno < 1 or revno > self.revno():
1258
raise InvalidRevisionNumber(revno)
1260
def sign_revision(self, revision_id, gpg_strategy):
1263
plaintext = Testament.from_revision(self, revision_id).as_short_text()
1264
self.revision_store.add(StringIO(gpg_strategy.sign(plaintext)),
1270
class ScratchBranch(_Branch):
1088
class ScratchBranch(Branch):
1271
1089
"""Special test class: a branch that cleans up after itself.
1273
1091
>>> b = ScratchBranch()
1274
1092
>>> isdir(b.base)
1276
1094
>>> bd = b.base
1277
>>> b._transport.__del__()
1282
def __init__(self, files=[], dirs=[], transport=None):
1099
def __init__(self, files=[], dirs=[], base=None):
1283
1100
"""Make a test branch.
1285
1102
This creates a temporary directory and runs init-tree in it.
1287
1104
If any files are listed, they are created in the working copy.
1289
if transport is None:
1290
transport = bzrlib.transport.local.ScratchTransport()
1291
super(ScratchBranch, self).__init__(transport, init=True)
1293
super(ScratchBranch, self).__init__(transport)
1106
from tempfile import mkdtemp
1111
Branch.__init__(self, base, init=init)
1296
self._transport.mkdir(d)
1113
os.mkdir(self.abspath(d))
1298
1115
for f in files:
1299
self._transport.put(f, 'content of %s' % f)
1116
file(os.path.join(self.base, f), 'w').write('content of %s' % f)
1302
1119
def clone(self):
1304
1121
>>> orig = ScratchBranch(files=["file1", "file2"])
1305
1122
>>> clone = orig.clone()
1306
>>> if os.name != 'nt':
1307
... os.path.samefile(orig.base, clone.base)
1309
... orig.base == clone.base
1123
>>> os.path.samefile(orig.base, clone.base)
1312
1125
>>> os.path.isfile(os.path.join(clone.base, "file1"))