21
from warnings import warn
22
25
from bzrlib.trace import mutter, note
23
from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, \
25
sha_file, appendpath, file_kind
26
from bzrlib.errors import BzrError, InvalidRevisionNumber, InvalidRevisionId
26
from bzrlib.osutils import (isdir, quotefn, compact_date, rand_bytes,
27
rename, splitpath, sha_file, appendpath,
29
from bzrlib.errors import (BzrError, InvalidRevisionNumber, InvalidRevisionId,
30
NoSuchRevision, HistoryMissing, NotBranchError,
31
DivergedBranches, LockError, UnlistableStore,
28
33
from bzrlib.textui import show_status
29
from bzrlib.revision import Revision
30
from bzrlib.xml import unpack_xml
34
from bzrlib.revision import Revision, validate_revision_id, is_ancestor
31
35
from bzrlib.delta import compare_trees
32
36
from bzrlib.tree import EmptyTree, RevisionTree
34
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
37
from bzrlib.inventory import Inventory
38
from bzrlib.weavestore import WeaveStore
39
from bzrlib.store import copy_all, ImmutableStore
44
BZR_BRANCH_FORMAT_4 = "Bazaar-NG branch, format 0.0.4\n"
45
BZR_BRANCH_FORMAT_5 = "Bazaar-NG branch, format 5\n"
35
46
## TODO: Maybe include checks for common corruption of newlines, etc?
38
49
# TODO: Some operations like log might retrieve the same revisions
39
50
# repeatedly to calculate deltas. We could perhaps have a weakref
40
# cache in memory to make this faster.
43
def find_branch(f, **args):
44
if f and (f.startswith('http://') or f.startswith('https://')):
46
return remotebranch.RemoteBranch(f, **args)
48
return Branch(f, **args)
51
def find_cached_branch(f, cache_root, **args):
52
from remotebranch import RemoteBranch
53
br = find_branch(f, **args)
54
def cacheify(br, store_name):
55
from meta_store import CachedStore
56
cache_path = os.path.join(cache_root, store_name)
58
new_store = CachedStore(getattr(br, store_name), cache_path)
59
setattr(br, store_name, new_store)
61
if isinstance(br, RemoteBranch):
62
cacheify(br, 'inventory_store')
63
cacheify(br, 'text_store')
64
cacheify(br, 'revision_store')
51
# cache in memory to make this faster. In general anything can be
52
# cached in memory between lock and unlock operations.
54
def find_branch(*ignored, **ignored_too):
55
# XXX: leave this here for about one release, then remove it
56
raise NotImplementedError('find_branch() is not supported anymore, '
57
'please use one of the new branch constructors')
68
59
def _relpath(base, path):
69
60
"""Return path relative to base, or raise exception.
137
124
"""Branch holding a history of revisions.
140
Base directory of the branch.
127
Base directory/url of the branch.
131
def __init__(self, *ignored, **ignored_too):
132
raise NotImplementedError('The Branch class is abstract')
135
def open_downlevel(base):
136
"""Open a branch which may be of an old format.
138
Only local branches are supported."""
139
return LocalBranch(base, find_root=False, relax_version_check=True)
143
"""Open an existing branch, rooted at 'base' (url)"""
144
if base and (base.startswith('http://') or base.startswith('https://')):
145
from bzrlib.remotebranch import RemoteBranch
146
return RemoteBranch(base, find_root=False)
148
return LocalBranch(base, find_root=False)
151
def open_containing(url):
152
"""Open an existing branch which contains url.
154
This probes for a branch at url, and searches upwards from there.
156
if url and (url.startswith('http://') or url.startswith('https://')):
157
from bzrlib.remotebranch import RemoteBranch
158
return RemoteBranch(url)
160
return LocalBranch(url)
163
def initialize(base):
164
"""Create a new branch, rooted at 'base' (url)"""
165
if base and (base.startswith('http://') or base.startswith('https://')):
166
from bzrlib.remotebranch import RemoteBranch
167
return RemoteBranch(base, init=True)
169
return LocalBranch(base, init=True)
171
def setup_caching(self, cache_root):
172
"""Subclasses that care about caching should override this, and set
173
up cached stores located under cache_root.
177
class LocalBranch(Branch):
178
"""A branch stored in the actual filesystem.
180
Note that it's "local" in the context of the filesystem; it doesn't
181
really matter if it's on an nfs/smb/afs/coda/... share, as long as
182
it's writable, and can be accessed via the normal filesystem API.
143
185
None, or 'r' or 'w'
150
192
Lock object from bzrlib.lock.
194
# We actually expect this class to be somewhat short-lived; part of its
195
# purpose is to try to isolate what bits of the branch logic are tied to
196
# filesystem access, so that in a later step, we can extricate them to
197
# a separarte ("storage") class.
153
198
_lock_mode = None
154
199
_lock_count = None
201
_inventory_weave = None
157
203
# Map some sort of prefix into a namespace
158
204
# stuff like "revno:10", "revid:", etc.
159
205
# This should match a prefix with a function which accepts
160
206
REVISION_NAMESPACES = {}
162
def __init__(self, base, init=False, find_root=True):
208
def push_stores(self, branch_to):
209
"""Copy the content of this branches store to branch_to."""
210
if (self._branch_format != branch_to._branch_format
211
or self._branch_format != 4):
212
from bzrlib.fetch import greedy_fetch
213
mutter("falling back to fetch logic to push between %s(%s) and %s(%s)",
214
self, self._branch_format, branch_to, branch_to._branch_format)
215
greedy_fetch(to_branch=branch_to, from_branch=self,
216
revision=self.last_revision())
219
store_pairs = ((self.text_store, branch_to.text_store),
220
(self.inventory_store, branch_to.inventory_store),
221
(self.revision_store, branch_to.revision_store))
223
for from_store, to_store in store_pairs:
224
copy_all(from_store, to_store)
225
except UnlistableStore:
226
raise UnlistableBranch(from_store)
228
def __init__(self, base, init=False, find_root=True,
229
relax_version_check=False):
163
230
"""Create new branch object at a particular location.
165
base -- Base directory for the branch.
232
base -- Base directory for the branch. May be a file:// url.
167
234
init -- If True, create new control files in a previously
168
235
unversioned directory. If False, the branch must already
171
238
find_root -- If true and init is false, find the root of the
172
239
existing branch containing base.
241
relax_version_check -- If true, the usual check for the branch
242
version is not applied. This is intended only for
243
upgrade/recovery type use; it's not guaranteed that
244
all operations will work on old format branches.
174
246
In the test suite, creation of new trees is tested using the
175
247
`ScratchBranch` class.
177
from bzrlib.store import ImmutableStore
179
250
self.base = os.path.realpath(base)
180
251
self._make_control()
182
253
self.base = find_branch_root(base)
255
if base.startswith("file://"):
184
257
self.base = os.path.realpath(base)
185
258
if not isdir(self.controlfilename('.')):
186
from errors import NotBranchError
187
raise NotBranchError("not a bzr branch: %s" % quotefn(base),
188
['use "bzr init" to initialize a new working tree',
189
'current bzr can only operate from top-of-tree'])
192
self.text_store = ImmutableStore(self.controlfilename('text-store'))
193
self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
194
self.inventory_store = ImmutableStore(self.controlfilename('inventory-store'))
259
raise NotBranchError('not a bzr branch: %s' % quotefn(base),
260
['use "bzr init" to initialize a '
262
self._check_format(relax_version_check)
263
cfn = self.controlfilename
264
if self._branch_format == 4:
265
self.inventory_store = ImmutableStore(cfn('inventory-store'))
266
self.text_store = ImmutableStore(cfn('text-store'))
267
elif self._branch_format == 5:
268
self.control_weaves = WeaveStore(cfn([]))
269
self.weave_store = WeaveStore(cfn('weaves'))
271
# FIXME: Unify with make_control_files
272
self.control_weaves.put_empty_weave('inventory')
273
self.control_weaves.put_empty_weave('ancestry')
274
self.revision_store = ImmutableStore(cfn('revision-store'))
197
277
def __str__(self):
297
368
raise BzrError("invalid controlfile mode %r" % mode)
301
370
def _make_control(self):
302
from bzrlib.inventory import Inventory
303
from bzrlib.xml import pack_xml
305
371
os.mkdir(self.controlfilename([]))
306
372
self.controlfile('README', 'w').write(
307
373
"This is a Bazaar-NG control directory.\n"
308
374
"Do not change any files in this directory.\n")
309
self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT)
310
for d in ('text-store', 'inventory-store', 'revision-store'):
375
self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT_5)
376
for d in ('text-store', 'revision-store',
311
378
os.mkdir(self.controlfilename(d))
312
for f in ('revision-history', 'merged-patches',
313
'pending-merged-patches', 'branch-name',
379
for f in ('revision-history',
315
382
'pending-merges'):
316
383
self.controlfile(f, 'w').write('')
317
384
mutter('created control directory in ' + self.base)
319
pack_xml(Inventory(gen_root_id()), self.controlfile('inventory','w'))
322
def _check_format(self):
386
# if we want per-tree root ids then this is the place to set
387
# them; they're not needed for now and so ommitted for
389
f = self.controlfile('inventory','w')
390
bzrlib.xml5.serializer_v5.write_inventory(Inventory(), f)
393
def _check_format(self, relax_version_check):
323
394
"""Check this branch format is supported.
325
The current tool only supports the current unstable format.
396
The format level is stored, as an integer, in
397
self._branch_format for code that needs to check it later.
327
399
In the future, we might need different in-memory Branch
328
400
classes to support downlevel branches. But not yet.
330
# This ignores newlines so that we can open branches created
331
# on Windows from Linux and so on. I think it might be better
332
# to always make all internal files in unix format.
333
fmt = self.controlfile('branch-format', 'r').read()
334
fmt.replace('\r\n', '')
335
if fmt != BZR_BRANCH_FORMAT:
403
fmt = self.controlfile('branch-format', 'r').read()
405
if e.errno == errno.ENOENT:
406
raise NotBranchError(self.base)
410
if fmt == BZR_BRANCH_FORMAT_5:
411
self._branch_format = 5
412
elif fmt == BZR_BRANCH_FORMAT_4:
413
self._branch_format = 4
415
if (not relax_version_check
416
and self._branch_format != 5):
336
417
raise BzrError('sorry, branch format %r not supported' % fmt,
337
418
['use a different bzr version',
338
419
'or remove the .bzr directory and "bzr init" again'])
636
715
return compare_trees(old_tree, new_tree)
640
718
def get_revision_sha1(self, revision_id):
641
719
"""Hash the stored value of a revision, and return it."""
642
# In the future, revision entries will be signed. At that
643
# point, it is probably best *not* to include the signature
644
# in the revision hash. Because that lets you re-sign
645
# the revision, (add signatures/remove signatures) and still
646
# have all hash pointers stay consistent.
647
# But for now, just hash the contents.
648
return bzrlib.osutils.sha_file(self.get_revision_xml(revision_id))
651
def get_inventory(self, inventory_id):
652
"""Get Inventory object by hash.
654
TODO: Perhaps for this and similar methods, take a revision
655
parameter which can be either an integer revno or a
657
from bzrlib.inventory import Inventory
658
from bzrlib.xml import unpack_xml
660
return unpack_xml(Inventory, self.inventory_store[inventory_id])
663
def get_inventory_sha1(self, inventory_id):
720
return bzrlib.osutils.sha_file(self.get_revision_xml_file(revision_id))
723
def _get_ancestry_weave(self):
724
return self.control_weaves.get_weave('ancestry')
727
def get_ancestry(self, revision_id):
728
"""Return a list of revision-ids integrated by a revision.
731
if revision_id is None:
733
w = self._get_ancestry_weave()
734
return [None] + [l[:-1] for l in w.get_iter(w.lookup(revision_id))]
737
def get_inventory_weave(self):
738
return self.control_weaves.get_weave('inventory')
741
def get_inventory(self, revision_id):
742
"""Get Inventory object by hash."""
743
xml = self.get_inventory_xml(revision_id)
744
return bzrlib.xml5.serializer_v5.read_inventory_from_string(xml)
747
def get_inventory_xml(self, revision_id):
748
"""Get inventory XML as a file object."""
750
assert isinstance(revision_id, basestring), type(revision_id)
751
iw = self.get_inventory_weave()
752
return iw.get_text(iw.lookup(revision_id))
754
raise bzrlib.errors.HistoryMissing(self, 'inventory', revision_id)
757
def get_inventory_sha1(self, revision_id):
664
758
"""Return the sha1 hash of the inventory entry
666
return sha_file(self.inventory_store[inventory_id])
760
return self.get_revision(revision_id).inventory_sha1
669
763
def get_revision_inventory(self, revision_id):
670
764
"""Return inventory of a past revision."""
671
# bzr 0.0.6 imposes the constraint that the inventory_id
765
# TODO: Unify this with get_inventory()
766
# bzr 0.0.6 and later imposes the constraint that the inventory_id
672
767
# must be the same as its revision, so this is trivial.
673
768
if revision_id == None:
674
from bzrlib.inventory import Inventory
675
769
return Inventory(self.get_root_id())
677
771
return self.get_inventory(revision_id)
680
774
def revision_history(self):
681
"""Return sequence of revision hashes on to this branch.
683
>>> ScratchBranch().revision_history()
775
"""Return sequence of revision hashes on to this branch."""
688
778
return [l.rstrip('\r\n') for l in
794
888
if stop_revision is None:
795
889
stop_revision = other_len
796
elif stop_revision > other_len:
797
raise NoSuchRevision(self, stop_revision)
891
assert isinstance(stop_revision, int)
892
if stop_revision > other_len:
893
raise bzrlib.errors.NoSuchRevision(self, stop_revision)
799
894
return other_history[self_len:stop_revision]
802
896
def update_revisions(self, other, stop_revision=None):
803
"""Pull in all new revisions from other branch.
805
>>> from bzrlib.commit import commit
806
>>> bzrlib.trace.silent = True
807
>>> br1 = ScratchBranch(files=['foo', 'bar'])
810
>>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False)
811
>>> br2 = ScratchBranch()
812
>>> br2.update_revisions(br1)
816
>>> br2.revision_history()
818
>>> br2.update_revisions(br1)
822
>>> br1.text_store.total_size() == br2.text_store.total_size()
825
from bzrlib.progress import ProgressBar
829
pb.update('comparing histories')
830
revision_ids = self.missing_revisions(other, stop_revision)
832
if hasattr(other.revision_store, "prefetch"):
833
other.revision_store.prefetch(revision_ids)
834
if hasattr(other.inventory_store, "prefetch"):
835
inventory_ids = [other.get_revision(r).inventory_id
836
for r in revision_ids]
837
other.inventory_store.prefetch(inventory_ids)
842
for rev_id in revision_ids:
844
pb.update('fetching revision', i, len(revision_ids))
845
rev = other.get_revision(rev_id)
846
revisions.append(rev)
847
inv = other.get_inventory(str(rev.inventory_id))
848
for key, entry in inv.iter_entries():
849
if entry.text_id is None:
851
if entry.text_id not in self.text_store:
852
needed_texts.add(entry.text_id)
856
count = self.text_store.copy_multi(other.text_store, needed_texts)
857
print "Added %d texts." % count
858
inventory_ids = [ f.inventory_id for f in revisions ]
859
count = self.inventory_store.copy_multi(other.inventory_store,
861
print "Added %d inventories." % count
862
revision_ids = [ f.revision_id for f in revisions]
863
count = self.revision_store.copy_multi(other.revision_store,
865
for revision_id in revision_ids:
866
self.append_revision(revision_id)
867
print "Added %d revisions." % count
897
"""Pull in new perfect-fit revisions."""
898
from bzrlib.fetch import greedy_fetch
899
from bzrlib.revision import get_intervening_revisions
900
if stop_revision is None:
901
stop_revision = other.last_revision()
902
greedy_fetch(to_branch=self, from_branch=other,
903
revision=stop_revision)
904
pullable_revs = self.missing_revisions(
905
other, other.revision_id_to_revno(stop_revision))
907
greedy_fetch(to_branch=self,
909
revision=pullable_revs[-1])
910
self.append_revision(*pullable_revs)
870
913
def commit(self, *args, **kw):
871
from bzrlib.commit import commit
872
commit(self, *args, **kw)
875
def lookup_revision(self, revision):
876
"""Return the revision identifier for a given revision information."""
877
revno, info = self.get_revision_info(revision)
880
def get_revision_info(self, revision):
881
"""Return (revno, revision id) for revision identifier.
883
revision can be an integer, in which case it is assumed to be revno (though
884
this will translate negative values into positive ones)
885
revision can also be a string, in which case it is parsed for something like
886
'date:' or 'revid:' etc.
891
try:# Convert to int if possible
892
revision = int(revision)
895
revs = self.revision_history()
896
if isinstance(revision, int):
899
# Mabye we should do this first, but we don't need it if revision == 0
901
revno = len(revs) + revision + 1
904
elif isinstance(revision, basestring):
905
for prefix, func in Branch.REVISION_NAMESPACES.iteritems():
906
if revision.startswith(prefix):
907
revno = func(self, revs, revision)
910
raise BzrError('No namespace registered for string: %r' % revision)
912
if revno is None or revno <= 0 or revno > len(revs):
913
raise BzrError("no such revision %s" % revision)
914
return revno, revs[revno-1]
916
def _namespace_revno(self, revs, revision):
917
"""Lookup a revision by revision number"""
918
assert revision.startswith('revno:')
920
return int(revision[6:])
923
REVISION_NAMESPACES['revno:'] = _namespace_revno
925
def _namespace_revid(self, revs, revision):
926
assert revision.startswith('revid:')
928
return revs.index(revision[6:]) + 1
931
REVISION_NAMESPACES['revid:'] = _namespace_revid
933
def _namespace_last(self, revs, revision):
934
assert revision.startswith('last:')
936
offset = int(revision[5:])
941
raise BzrError('You must supply a positive value for --revision last:XXX')
942
return len(revs) - offset + 1
943
REVISION_NAMESPACES['last:'] = _namespace_last
945
def _namespace_tag(self, revs, revision):
946
assert revision.startswith('tag:')
947
raise BzrError('tag: namespace registered, but not implemented.')
948
REVISION_NAMESPACES['tag:'] = _namespace_tag
950
def _namespace_date(self, revs, revision):
951
assert revision.startswith('date:')
953
# Spec for date revisions:
955
# value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
956
# it can also start with a '+/-/='. '+' says match the first
957
# entry after the given date. '-' is match the first entry before the date
958
# '=' is match the first entry after, but still on the given date.
960
# +2005-05-12 says find the first matching entry after May 12th, 2005 at 0:00
961
# -2005-05-12 says find the first matching entry before May 12th, 2005 at 0:00
962
# =2005-05-12 says find the first match after May 12th, 2005 at 0:00 but before
963
# May 13th, 2005 at 0:00
965
# So the proper way of saying 'give me all entries for today' is:
966
# -r {date:+today}:{date:-tomorrow}
967
# The default is '=' when not supplied
970
if val[:1] in ('+', '-', '='):
971
match_style = val[:1]
974
today = datetime.datetime.today().replace(hour=0,minute=0,second=0,microsecond=0)
975
if val.lower() == 'yesterday':
976
dt = today - datetime.timedelta(days=1)
977
elif val.lower() == 'today':
979
elif val.lower() == 'tomorrow':
980
dt = today + datetime.timedelta(days=1)
983
# This should be done outside the function to avoid recompiling it.
984
_date_re = re.compile(
985
r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
987
r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
989
m = _date_re.match(val)
990
if not m or (not m.group('date') and not m.group('time')):
991
raise BzrError('Invalid revision date %r' % revision)
994
year, month, day = int(m.group('year')), int(m.group('month')), int(m.group('day'))
996
year, month, day = today.year, today.month, today.day
998
hour = int(m.group('hour'))
999
minute = int(m.group('minute'))
1000
if m.group('second'):
1001
second = int(m.group('second'))
1005
hour, minute, second = 0,0,0
1007
dt = datetime.datetime(year=year, month=month, day=day,
1008
hour=hour, minute=minute, second=second)
1012
if match_style == '-':
1014
elif match_style == '=':
1015
last = dt + datetime.timedelta(days=1)
1018
for i in range(len(revs)-1, -1, -1):
1019
r = self.get_revision(revs[i])
1020
# TODO: Handle timezone.
1021
dt = datetime.datetime.fromtimestamp(r.timestamp)
1022
if first >= dt and (last is None or dt >= last):
1025
for i in range(len(revs)):
1026
r = self.get_revision(revs[i])
1027
# TODO: Handle timezone.
1028
dt = datetime.datetime.fromtimestamp(r.timestamp)
1029
if first <= dt and (last is None or dt <= last):
1031
REVISION_NAMESPACES['date:'] = _namespace_date
914
from bzrlib.commit import Commit
915
Commit().commit(self, *args, **kw)
917
def revision_id_to_revno(self, revision_id):
918
"""Given a revision id, return its revno"""
919
if revision_id is None:
921
history = self.revision_history()
923
return history.index(revision_id) + 1
925
raise bzrlib.errors.NoSuchRevision(self, revision_id)
927
def get_rev_id(self, revno, history=None):
928
"""Find the revision id of the specified revno."""
932
history = self.revision_history()
933
elif revno <= 0 or revno > len(history):
934
raise bzrlib.errors.NoSuchRevision(self, revno)
935
return history[revno - 1]
1033
937
def revision_tree(self, revision_id):
1034
938
"""Return Tree for a revision on this branch.