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
21
28
from bzrlib.trace import mutter, note
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"
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"
27
57
## TODO: Maybe include checks for common corruption of newlines, etc?
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)
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')
129
70
######################################################################
146
147
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.
149
153
_lock_mode = None
150
154
_lock_count = None
156
_inventory_weave = None
153
158
# Map some sort of prefix into a namespace
154
159
# stuff like "revno:10", "revid:", etc.
155
160
# This should match a prefix with a function which accepts
156
161
REVISION_NAMESPACES = {}
158
def __init__(self, base, init=False, find_root=True):
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):
159
185
"""Create new branch object at a particular location.
161
base -- Base directory for the branch.
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)
163
191
init -- If True, create new control files in a previously
164
192
unversioned directory. If False, the branch must already
167
find_root -- If true and init is false, find the root of the
168
existing branch containing base.
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.
170
200
In the test suite, creation of new trees is tested using the
171
201
`ScratchBranch` class.
173
from bzrlib.store import ImmutableStore
203
assert isinstance(transport, Transport), \
204
"%r is not a Transport" % transport
205
self._transport = transport
175
self.base = os.path.realpath(base)
176
207
self._make_control()
178
self.base = find_branch_root(base)
180
self.base = os.path.realpath(base)
181
if not isdir(self.controlfilename('.')):
182
from errors import NotBranchError
183
raise NotBranchError("not a bzr branch: %s" % quotefn(base),
184
['use "bzr init" to initialize a new working tree',
185
'current bzr can only operate from top-of-tree'])
188
self.text_store = ImmutableStore(self.controlfilename('text-store'))
189
self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
190
self.inventory_store = ImmutableStore(self.controlfilename('inventory-store'))
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
193
250
def __str__(self):
194
return '%s(%r)' % (self.__class__.__name__, self.base)
251
return '%s(%r)' % (self.__class__.__name__, self._transport.base)
197
254
__repr__ = __str__
200
257
def __del__(self):
201
258
if self._lock_mode or self._lock:
202
from warnings import warn
259
# XXX: This should show something every time, and be suitable for
260
# headless operation and embedding
203
261
warn("branch %r was not explicitly unlocked" % self)
204
262
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
208
312
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
209
316
if self._lock_mode:
210
317
if self._lock_mode != 'w':
211
from errors import LockError
212
318
raise LockError("can't upgrade to a write lock from %r" %
214
320
self._lock_count += 1
216
from bzrlib.lock import WriteLock
218
self._lock = WriteLock(self.controlfilename('branch-lock'))
322
self._lock = self._transport.lock_write(
323
self._rel_controlfilename('branch-lock'))
219
324
self._lock_mode = 'w'
220
325
self._lock_count = 1
326
self._set_transaction(transactions.PassThroughTransaction())
224
328
def lock_read(self):
329
mutter("lock read: %s (%s)", self, self._lock_count)
225
330
if self._lock_mode:
226
331
assert self._lock_mode in ('r', 'w'), \
227
332
"invalid lock mode %r" % self._lock_mode
228
333
self._lock_count += 1
230
from bzrlib.lock import ReadLock
232
self._lock = ReadLock(self.controlfilename('branch-lock'))
335
self._lock = self._transport.lock_read(
336
self._rel_controlfilename('branch-lock'))
233
337
self._lock_mode = 'r'
234
338
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)
238
343
def unlock(self):
344
mutter("unlock: %s (%s)", self, self._lock_count)
239
345
if not self._lock_mode:
240
from errors import LockError
241
346
raise LockError('branch %r is not locked' % (self))
243
348
if self._lock_count > 1:
244
349
self._lock_count -= 1
351
self._finish_transaction()
246
352
self._lock.unlock()
247
353
self._lock = None
248
354
self._lock_mode = self._lock_count = None
251
356
def abspath(self, name):
252
"""Return absolute filename for something in the branch"""
253
return os.path.join(self.base, name)
256
def relpath(self, path):
257
"""Return path relative to this branch of something inside it.
259
Raises an error if path is not in this branch."""
260
return _relpath(self.base, path)
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):
365
if isinstance(file_or_path, basestring):
366
file_or_path = [file_or_path]
367
return [bzrlib.BZRDIR] + file_or_path
263
369
def controlfilename(self, file_or_path):
264
370
"""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)
371
return self._transport.abspath(self._rel_controlfilename(file_or_path))
270
374
def controlfile(self, file_or_path, mode='r'):
278
382
Controlfiles should almost never be opened in write mode but
279
383
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',
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")
293
399
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)
297
430
def _make_control(self):
298
431
from bzrlib.inventory import Inventory
299
from bzrlib.xml import pack_xml
432
from bzrlib.weavefile import write_weave_v5
433
from bzrlib.weave import Weave
301
os.mkdir(self.controlfilename([]))
302
self.controlfile('README', 'w').write(
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']
303
448
"This is a Bazaar-NG control directory.\n"
304
"Do not change any files in this directory.\n")
305
self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT)
306
for d in ('text-store', 'inventory-store', 'revision-store'):
307
os.mkdir(self.controlfilename(d))
308
for f in ('revision-history', 'merged-patches',
309
'pending-merged-patches', 'branch-name',
312
self.controlfile(f, 'w').write('')
313
mutter('created control directory in ' + self.base)
315
pack_xml(Inventory(gen_root_id()), self.controlfile('inventory','w'))
318
def _check_format(self):
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):
319
465
"""Check this branch format is supported.
321
The current tool only supports the current unstable format.
467
The format level is stored, as an integer, in
468
self._branch_format for code that needs to check it later.
323
470
In the future, we might need different in-memory Branch
324
471
classes to support downlevel branches. But not yet.
326
# This ignores newlines so that we can open branches created
327
# on Windows from Linux and so on. I think it might be better
328
# to always make all internal files in unix format.
329
fmt = self.controlfile('branch-format', 'r').read()
330
fmt.replace('\r\n', '')
331
if fmt != BZR_BRANCH_FORMAT:
332
raise BzrError('sorry, branch format %r not supported' % fmt,
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,
333
489
['use a different bzr version',
334
'or remove the .bzr directory and "bzr init" again'])
490
'or remove the .bzr directory'
491
' and "bzr init" again'])
336
493
def get_root_id(self):
337
494
"""Return the id of this branches root"""
567
710
def append_revision(self, *revision_ids):
568
from bzrlib.atomicfile import AtomicFile
570
711
for revision_id in revision_ids:
571
712
mutter("add {%s} to revision-history" % revision_id)
573
rev_history = self.revision_history()
574
rev_history.extend(revision_ids)
576
f = AtomicFile(self.controlfilename('revision-history'))
578
for rev_id in rev_history:
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()
585
750
def get_revision(self, revision_id):
586
751
"""Return the Revision object for a named revision"""
587
from bzrlib.revision import Revision
588
from bzrlib.xml import unpack_xml
752
xml_file = self.get_revision_xml_file(revision_id)
592
if not revision_id or not isinstance(revision_id, basestring):
593
raise ValueError('invalid revision-id: %r' % revision_id)
594
r = unpack_xml(Revision, self.revision_store[revision_id])
755
r = bzrlib.xml5.serializer_v5.read_revision(xml_file)
756
except SyntaxError, e:
757
raise bzrlib.errors.BzrError('failed to unpack revision_xml',
598
761
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)
602
785
def get_revision_sha1(self, revision_id):
603
786
"""Hash the stored value of a revision, and return it."""
607
790
# the revision, (add signatures/remove signatures) and still
608
791
# have all hash pointers stay consistent.
609
792
# But for now, just hash the contents.
610
return sha_file(self.revision_store[revision_id])
613
def get_inventory(self, inventory_id):
614
"""Get Inventory object by hash.
616
TODO: Perhaps for this and similar methods, take a revision
617
parameter which can be either an integer revno or a
619
from bzrlib.inventory import Inventory
620
from bzrlib.xml import unpack_xml
622
return unpack_xml(Inventory, self.inventory_store[inventory_id])
625
def get_inventory_sha1(self, inventory_id):
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):
626
826
"""Return the sha1 hash of the inventory entry
628
return sha_file(self.inventory_store[inventory_id])
828
return self.get_revision(revision_id).inventory_sha1
631
830
def get_revision_inventory(self, revision_id):
632
831
"""Return inventory of a past revision."""
633
# bzr 0.0.6 imposes the constraint that the inventory_id
832
# TODO: Unify this with get_inventory()
833
# bzr 0.0.6 and later imposes the constraint that the inventory_id
634
834
# must be the same as its revision, so this is trivial.
635
835
if revision_id == None:
636
from bzrlib.inventory import Inventory
637
836
return Inventory(self.get_root_id())
639
838
return self.get_inventory(revision_id)
642
840
def revision_history(self):
643
"""Return sequence of revision hashes on to this branch.
645
>>> ScratchBranch().revision_history()
841
"""Return sequence of revision hashes on to this branch."""
650
return [l.rstrip('\r\n') for l in
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
651
850
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)
656
def common_ancestor(self, other, self_revno=None, other_revno=None):
659
>>> sb = ScratchBranch(files=['foo', 'foo~'])
660
>>> sb.common_ancestor(sb) == (None, None)
662
>>> commit.commit(sb, "Committing first revision", verbose=False)
663
>>> sb.common_ancestor(sb)[0]
665
>>> clone = sb.clone()
666
>>> commit.commit(sb, "Committing second revision", verbose=False)
667
>>> sb.common_ancestor(sb)[0]
669
>>> sb.common_ancestor(clone)[0]
671
>>> commit.commit(clone, "Committing divergent second revision",
673
>>> sb.common_ancestor(clone)[0]
675
>>> sb.common_ancestor(clone) == clone.common_ancestor(sb)
677
>>> sb.common_ancestor(sb) != clone.common_ancestor(clone)
679
>>> clone2 = sb.clone()
680
>>> sb.common_ancestor(clone2)[0]
682
>>> sb.common_ancestor(clone2, self_revno=1)[0]
684
>>> sb.common_ancestor(clone2, other_revno=1)[0]
687
my_history = self.revision_history()
688
other_history = other.revision_history()
689
if self_revno is None:
690
self_revno = len(my_history)
691
if other_revno is None:
692
other_revno = len(other_history)
693
indices = range(min((self_revno, other_revno)))
696
if my_history[r] == other_history[r]:
697
return r+1, my_history[r]
700
def enum_history(self, direction):
701
"""Return (revno, revision_id) for history of branch.
704
'forward' is from earliest to latest
705
'reverse' is from latest to earliest
707
rh = self.revision_history()
708
if direction == 'forward':
713
elif direction == 'reverse':
719
raise ValueError('invalid history direction', direction)
723
860
"""Return current revision number for this branch.
777
915
if stop_revision is None:
778
916
stop_revision = other_len
779
elif stop_revision > other_len:
780
raise NoSuchRevision(self, stop_revision)
918
assert isinstance(stop_revision, int)
919
if stop_revision > other_len:
920
raise bzrlib.errors.NoSuchRevision(self, stop_revision)
782
921
return other_history[self_len:stop_revision]
785
923
def update_revisions(self, other, stop_revision=None):
786
"""Pull in all new revisions from other branch.
788
>>> from bzrlib.commit import commit
789
>>> bzrlib.trace.silent = True
790
>>> br1 = ScratchBranch(files=['foo', 'bar'])
793
>>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False)
794
>>> br2 = ScratchBranch()
795
>>> br2.update_revisions(br1)
799
>>> br2.revision_history()
801
>>> br2.update_revisions(br1)
805
>>> br1.text_store.total_size() == br2.text_store.total_size()
808
from bzrlib.progress import ProgressBar
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)
812
from sets import Set as set
816
pb.update('comparing histories')
817
revision_ids = self.missing_revisions(other, stop_revision)
819
if hasattr(other.revision_store, "prefetch"):
820
other.revision_store.prefetch(revision_ids)
821
if hasattr(other.inventory_store, "prefetch"):
822
inventory_ids = [other.get_revision(r).inventory_id
823
for r in revision_ids]
824
other.inventory_store.prefetch(inventory_ids)
829
for rev_id in revision_ids:
831
pb.update('fetching revision', i, len(revision_ids))
832
rev = other.get_revision(rev_id)
833
revisions.append(rev)
834
inv = other.get_inventory(str(rev.inventory_id))
835
for key, entry in inv.iter_entries():
836
if entry.text_id is None:
838
if entry.text_id not in self.text_store:
839
needed_texts.add(entry.text_id)
843
count = self.text_store.copy_multi(other.text_store, needed_texts)
844
print "Added %d texts." % count
845
inventory_ids = [ f.inventory_id for f in revisions ]
846
count = self.inventory_store.copy_multi(other.inventory_store,
848
print "Added %d inventories." % count
849
revision_ids = [ f.revision_id for f in revisions]
850
count = self.revision_store.copy_multi(other.revision_store,
852
for revision_id in revision_ids:
853
self.append_revision(revision_id)
854
print "Added %d revisions." % count
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):
857
957
def commit(self, *args, **kw):
858
from bzrlib.commit import commit
859
commit(self, *args, **kw)
862
def lookup_revision(self, revision):
863
"""Return the revision identifier for a given revision information."""
864
revno, info = self.get_revision_info(revision)
867
def get_revision_info(self, revision):
868
"""Return (revno, revision id) for revision identifier.
870
revision can be an integer, in which case it is assumed to be revno (though
871
this will translate negative values into positive ones)
872
revision can also be a string, in which case it is parsed for something like
873
'date:' or 'revid:' etc.
878
try:# Convert to int if possible
879
revision = int(revision)
882
revs = self.revision_history()
883
if isinstance(revision, int):
886
# Mabye we should do this first, but we don't need it if revision == 0
888
revno = len(revs) + revision + 1
891
elif isinstance(revision, basestring):
892
for prefix, func in Branch.REVISION_NAMESPACES.iteritems():
893
if revision.startswith(prefix):
894
revno = func(self, revs, revision)
897
raise BzrError('No namespace registered for string: %r' % revision)
899
if revno is None or revno <= 0 or revno > len(revs):
900
raise BzrError("no such revision %s" % revision)
901
return revno, revs[revno-1]
903
def _namespace_revno(self, revs, revision):
904
"""Lookup a revision by revision number"""
905
assert revision.startswith('revno:')
907
return int(revision[6:])
910
REVISION_NAMESPACES['revno:'] = _namespace_revno
912
def _namespace_revid(self, revs, revision):
913
assert revision.startswith('revid:')
915
return revs.index(revision[6:]) + 1
918
REVISION_NAMESPACES['revid:'] = _namespace_revid
920
def _namespace_last(self, revs, revision):
921
assert revision.startswith('last:')
923
offset = int(revision[5:])
928
raise BzrError('You must supply a positive value for --revision last:XXX')
929
return len(revs) - offset + 1
930
REVISION_NAMESPACES['last:'] = _namespace_last
932
def _namespace_tag(self, revs, revision):
933
assert revision.startswith('tag:')
934
raise BzrError('tag: namespace registered, but not implemented.')
935
REVISION_NAMESPACES['tag:'] = _namespace_tag
937
def _namespace_date(self, revs, revision):
938
assert revision.startswith('date:')
940
# Spec for date revisions:
942
# value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
943
# it can also start with a '+/-/='. '+' says match the first
944
# entry after the given date. '-' is match the first entry before the date
945
# '=' is match the first entry after, but still on the given date.
947
# +2005-05-12 says find the first matching entry after May 12th, 2005 at 0:00
948
# -2005-05-12 says find the first matching entry before May 12th, 2005 at 0:00
949
# =2005-05-12 says find the first match after May 12th, 2005 at 0:00 but before
950
# May 13th, 2005 at 0:00
952
# So the proper way of saying 'give me all entries for today' is:
953
# -r {date:+today}:{date:-tomorrow}
954
# The default is '=' when not supplied
957
if val[:1] in ('+', '-', '='):
958
match_style = val[:1]
961
today = datetime.datetime.today().replace(hour=0,minute=0,second=0,microsecond=0)
962
if val.lower() == 'yesterday':
963
dt = today - datetime.timedelta(days=1)
964
elif val.lower() == 'today':
966
elif val.lower() == 'tomorrow':
967
dt = today + datetime.timedelta(days=1)
970
# This should be done outside the function to avoid recompiling it.
971
_date_re = re.compile(
972
r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
974
r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
976
m = _date_re.match(val)
977
if not m or (not m.group('date') and not m.group('time')):
978
raise BzrError('Invalid revision date %r' % revision)
981
year, month, day = int(m.group('year')), int(m.group('month')), int(m.group('day'))
983
year, month, day = today.year, today.month, today.day
985
hour = int(m.group('hour'))
986
minute = int(m.group('minute'))
987
if m.group('second'):
988
second = int(m.group('second'))
992
hour, minute, second = 0,0,0
994
dt = datetime.datetime(year=year, month=month, day=day,
995
hour=hour, minute=minute, second=second)
999
if match_style == '-':
1001
elif match_style == '=':
1002
last = dt + datetime.timedelta(days=1)
1005
for i in range(len(revs)-1, -1, -1):
1006
r = self.get_revision(revs[i])
1007
# TODO: Handle timezone.
1008
dt = datetime.datetime.fromtimestamp(r.timestamp)
1009
if first >= dt and (last is None or dt >= last):
1012
for i in range(len(revs)):
1013
r = self.get_revision(revs[i])
1014
# TODO: Handle timezone.
1015
dt = datetime.datetime.fromtimestamp(r.timestamp)
1016
if first <= dt and (last is None or dt <= last):
1018
REVISION_NAMESPACES['date:'] = _namespace_date
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)
971
def get_rev_id(self, revno, history=None):
972
"""Find the revision id of the specified revno."""
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]
1020
981
def revision_tree(self, revision_id):
1021
982
"""Return Tree for a revision on this branch.
1023
984
`revision_id` may be None for the null revision, in which case
1024
985
an `EmptyTree` is returned."""
1025
from bzrlib.tree import EmptyTree, RevisionTree
1026
986
# TODO: refactor this to use an existing revision object
1027
987
# so we don't need to read it in twice.
1028
988
if revision_id == None:
1029
return EmptyTree(self.get_root_id())
1031
991
inv = self.get_revision_inventory(revision_id)
1032
return RevisionTree(self.text_store, inv)
992
return RevisionTree(self.weave_store, inv, revision_id)
1035
994
def working_tree(self):
1036
995
"""Return a `Tree` for the working copy."""
1037
from workingtree import WorkingTree
1038
return WorkingTree(self.base, self.read_working_inventory())
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)
1041
1005
def basis_tree(self):
1229
def add_pending_merge(self, revision_id):
1230
from bzrlib.revision import validate_revision_id
1232
validate_revision_id(revision_id)
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?
1234
1194
p = self.pending_merges()
1235
if revision_id in p:
1237
p.append(revision_id)
1238
self.set_pending_merges(p)
1196
for rev_id in revision_ids:
1202
self.set_pending_merges(p)
1241
1204
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?
1242
1232
from bzrlib.atomicfile import AtomicFile
1243
1233
self.lock_write()
1245
f = AtomicFile(self.controlfilename('pending-merges'))
1235
f = AtomicFile(self.controlfilename('parent'))
1257
class ScratchBranch(Branch):
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):
1258
1271
"""Special test class: a branch that cleans up after itself.
1260
1273
>>> b = ScratchBranch()
1261
1274
>>> isdir(b.base)
1263
1276
>>> bd = b.base
1277
>>> b._transport.__del__()
1268
def __init__(self, files=[], dirs=[], base=None):
1282
def __init__(self, files=[], dirs=[], transport=None):
1269
1283
"""Make a test branch.
1271
1285
This creates a temporary directory and runs init-tree in it.
1273
1287
If any files are listed, they are created in the working copy.
1275
from tempfile import mkdtemp
1280
Branch.__init__(self, base, init=init)
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)
1282
os.mkdir(self.abspath(d))
1296
self._transport.mkdir(d)
1284
1298
for f in files:
1285
file(os.path.join(self.base, f), 'w').write('content of %s' % f)
1299
self._transport.put(f, 'content of %s' % f)
1288
1302
def clone(self):
1290
1304
>>> orig = ScratchBranch(files=["file1", "file2"])
1291
1305
>>> clone = orig.clone()
1292
>>> os.path.samefile(orig.base, clone.base)
1306
>>> if os.name != 'nt':
1307
... os.path.samefile(orig.base, clone.base)
1309
... orig.base == clone.base
1294
1312
>>> os.path.isfile(os.path.join(clone.base, "file1"))