21
from warnings import warn
25
22
from bzrlib.trace import mutter, note
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,
23
from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, \
25
sha_file, appendpath, file_kind
27
from bzrlib.errors import BzrError, InvalidRevisionNumber, InvalidRevisionId
33
29
from bzrlib.textui import show_status
34
from bzrlib.revision import Revision, validate_revision_id, is_ancestor
30
from bzrlib.revision import Revision
31
from bzrlib.xml import unpack_xml
35
32
from bzrlib.delta import compare_trees
36
33
from bzrlib.tree import EmptyTree, RevisionTree
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"
38
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
46
39
## TODO: Maybe include checks for common corruption of newlines, etc?
49
42
# TODO: Some operations like log might retrieve the same revisions
50
43
# repeatedly to calculate deltas. We could perhaps have a weakref
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')
44
# cache in memory to make this faster.
46
# TODO: please move the revision-string syntax stuff out of the branch
47
# object; it's clutter
50
def find_branch(f, **args):
51
if f and (f.startswith('http://') or f.startswith('https://')):
53
return remotebranch.RemoteBranch(f, **args)
55
return Branch(f, **args)
58
def find_cached_branch(f, cache_root, **args):
59
from remotebranch import RemoteBranch
60
br = find_branch(f, **args)
61
def cacheify(br, store_name):
62
from meta_store import CachedStore
63
cache_path = os.path.join(cache_root, store_name)
65
new_store = CachedStore(getattr(br, store_name), cache_path)
66
setattr(br, store_name, new_store)
68
if isinstance(br, RemoteBranch):
69
cacheify(br, 'inventory_store')
70
cacheify(br, 'text_store')
71
cacheify(br, 'revision_store')
59
75
def _relpath(base, path):
60
76
"""Return path relative to base, or raise exception.
124
148
"""Branch holding a history of revisions.
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.
151
Base directory of the branch.
185
154
None, or 'r' or 'w'
192
161
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.
198
164
_lock_mode = None
199
165
_lock_count = None
201
_inventory_weave = None
203
168
# Map some sort of prefix into a namespace
204
169
# stuff like "revno:10", "revid:", etc.
205
170
# This should match a prefix with a function which accepts
206
171
REVISION_NAMESPACES = {}
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):
173
def __init__(self, base, init=False, find_root=True):
230
174
"""Create new branch object at a particular location.
232
base -- Base directory for the branch. May be a file:// url.
176
base -- Base directory for the branch.
234
178
init -- If True, create new control files in a previously
235
179
unversioned directory. If False, the branch must already
238
182
find_root -- If true and init is false, find the root of the
239
183
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.
246
185
In the test suite, creation of new trees is tested using the
247
186
`ScratchBranch` class.
188
from bzrlib.store import ImmutableStore
250
190
self.base = os.path.realpath(base)
251
191
self._make_control()
253
193
self.base = find_branch_root(base)
255
if base.startswith("file://"):
257
195
self.base = os.path.realpath(base)
258
196
if not isdir(self.controlfilename('.')):
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
from errors import NotBranchError
198
raise NotBranchError("not a bzr branch: %s" % quotefn(base),
199
['use "bzr init" to initialize a new working tree',
200
'current bzr can only operate from top-of-tree'])
203
self.text_store = ImmutableStore(self.controlfilename('text-store'))
204
self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
205
self.inventory_store = ImmutableStore(self.controlfilename('inventory-store'))
277
208
def __str__(self):
715
633
return compare_trees(old_tree, new_tree)
718
637
def get_revision_sha1(self, revision_id):
719
638
"""Hash the stored value of a revision, and return it."""
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):
639
# In the future, revision entries will be signed. At that
640
# point, it is probably best *not* to include the signature
641
# in the revision hash. Because that lets you re-sign
642
# the revision, (add signatures/remove signatures) and still
643
# have all hash pointers stay consistent.
644
# But for now, just hash the contents.
645
return bzrlib.osutils.sha_file(self.get_revision_xml(revision_id))
648
def get_inventory(self, inventory_id):
649
"""Get Inventory object by hash.
651
TODO: Perhaps for this and similar methods, take a revision
652
parameter which can be either an integer revno or a
654
from bzrlib.inventory import Inventory
655
from bzrlib.xml import unpack_xml
657
return unpack_xml(Inventory, self.get_inventory_xml(inventory_id))
660
def get_inventory_xml(self, inventory_id):
748
661
"""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):
662
return self.inventory_store[inventory_id]
665
def get_inventory_sha1(self, inventory_id):
758
666
"""Return the sha1 hash of the inventory entry
760
return self.get_revision(revision_id).inventory_sha1
668
return sha_file(self.get_inventory_xml(inventory_id))
763
671
def get_revision_inventory(self, revision_id):
764
672
"""Return inventory of a past revision."""
765
# TODO: Unify this with get_inventory()
766
# bzr 0.0.6 and later imposes the constraint that the inventory_id
673
# bzr 0.0.6 imposes the constraint that the inventory_id
767
674
# must be the same as its revision, so this is trivial.
768
675
if revision_id == None:
676
from bzrlib.inventory import Inventory
769
677
return Inventory(self.get_root_id())
771
679
return self.get_inventory(revision_id)
774
682
def revision_history(self):
775
"""Return sequence of revision hashes on to this branch."""
683
"""Return sequence of revision hashes on to this branch.
685
>>> ScratchBranch().revision_history()
778
690
return [l.rstrip('\r\n') for l in
888
796
if stop_revision is None:
889
797
stop_revision = other_len
891
assert isinstance(stop_revision, int)
892
if stop_revision > other_len:
893
raise bzrlib.errors.NoSuchRevision(self, stop_revision)
798
elif stop_revision > other_len:
799
raise bzrlib.errors.NoSuchRevision(self, stop_revision)
894
801
return other_history[self_len:stop_revision]
896
804
def update_revisions(self, other, stop_revision=None):
897
"""Pull in new perfect-fit revisions."""
805
"""Pull in all new revisions from other branch.
898
807
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)
809
pb = bzrlib.ui.ui_factory.progress_bar()
810
pb.update('comparing histories')
812
revision_ids = self.missing_revisions(other, stop_revision)
814
if len(revision_ids) > 0:
815
count = greedy_fetch(self, other, revision_ids[-1], pb)[0]
818
self.append_revision(*revision_ids)
819
## note("Added %d revisions." % count)
822
def install_revisions(self, other, revision_ids, pb):
823
if hasattr(other.revision_store, "prefetch"):
824
other.revision_store.prefetch(revision_ids)
825
if hasattr(other.inventory_store, "prefetch"):
826
inventory_ids = [other.get_revision(r).inventory_id
827
for r in revision_ids]
828
other.inventory_store.prefetch(inventory_ids)
831
pb = bzrlib.ui.ui_factory.progress_bar()
838
for i, rev_id in enumerate(revision_ids):
839
pb.update('fetching revision', i+1, len(revision_ids))
841
rev = other.get_revision(rev_id)
842
except bzrlib.errors.NoSuchRevision:
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, cp_fail = self.text_store.copy_multi(other.text_store,
858
#print "Added %d texts." % count
859
inventory_ids = [ f.inventory_id for f in revisions ]
860
count, cp_fail = self.inventory_store.copy_multi(other.inventory_store,
862
#print "Added %d inventories." % count
863
revision_ids = [ f.revision_id for f in revisions]
865
count, cp_fail = self.revision_store.copy_multi(other.revision_store,
868
assert len(cp_fail) == 0
869
return count, failures
913
872
def commit(self, *args, **kw):
914
from bzrlib.commit import Commit
915
Commit().commit(self, *args, **kw)
873
from bzrlib.commit import commit
874
commit(self, *args, **kw)
877
def lookup_revision(self, revision):
878
"""Return the revision identifier for a given revision information."""
879
revno, info = self.get_revision_info(revision)
917
883
def revision_id_to_revno(self, revision_id):
918
884
"""Given a revision id, return its revno"""
919
if revision_id is None:
921
885
history = self.revision_history()
923
887
return history.index(revision_id) + 1
924
888
except ValueError:
925
889
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]
892
def get_revision_info(self, revision):
893
"""Return (revno, revision id) for revision identifier.
895
revision can be an integer, in which case it is assumed to be revno (though
896
this will translate negative values into positive ones)
897
revision can also be a string, in which case it is parsed for something like
898
'date:' or 'revid:' etc.
903
try:# Convert to int if possible
904
revision = int(revision)
907
revs = self.revision_history()
908
if isinstance(revision, int):
911
# Mabye we should do this first, but we don't need it if revision == 0
913
revno = len(revs) + revision + 1
916
elif isinstance(revision, basestring):
917
for prefix, func in Branch.REVISION_NAMESPACES.iteritems():
918
if revision.startswith(prefix):
919
revno = func(self, revs, revision)
922
raise BzrError('No namespace registered for string: %r' % revision)
924
if revno is None or revno <= 0 or revno > len(revs):
925
raise BzrError("no such revision %s" % revision)
926
return revno, revs[revno-1]
928
def _namespace_revno(self, revs, revision):
929
"""Lookup a revision by revision number"""
930
assert revision.startswith('revno:')
932
return int(revision[6:])
935
REVISION_NAMESPACES['revno:'] = _namespace_revno
937
def _namespace_revid(self, revs, revision):
938
assert revision.startswith('revid:')
940
return revs.index(revision[6:]) + 1
943
REVISION_NAMESPACES['revid:'] = _namespace_revid
945
def _namespace_last(self, revs, revision):
946
assert revision.startswith('last:')
948
offset = int(revision[5:])
953
raise BzrError('You must supply a positive value for --revision last:XXX')
954
return len(revs) - offset + 1
955
REVISION_NAMESPACES['last:'] = _namespace_last
957
def _namespace_tag(self, revs, revision):
958
assert revision.startswith('tag:')
959
raise BzrError('tag: namespace registered, but not implemented.')
960
REVISION_NAMESPACES['tag:'] = _namespace_tag
962
def _namespace_date(self, revs, revision):
963
assert revision.startswith('date:')
965
# Spec for date revisions:
967
# value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
968
# it can also start with a '+/-/='. '+' says match the first
969
# entry after the given date. '-' is match the first entry before the date
970
# '=' is match the first entry after, but still on the given date.
972
# +2005-05-12 says find the first matching entry after May 12th, 2005 at 0:00
973
# -2005-05-12 says find the first matching entry before May 12th, 2005 at 0:00
974
# =2005-05-12 says find the first match after May 12th, 2005 at 0:00 but before
975
# May 13th, 2005 at 0:00
977
# So the proper way of saying 'give me all entries for today' is:
978
# -r {date:+today}:{date:-tomorrow}
979
# The default is '=' when not supplied
982
if val[:1] in ('+', '-', '='):
983
match_style = val[:1]
986
today = datetime.datetime.today().replace(hour=0,minute=0,second=0,microsecond=0)
987
if val.lower() == 'yesterday':
988
dt = today - datetime.timedelta(days=1)
989
elif val.lower() == 'today':
991
elif val.lower() == 'tomorrow':
992
dt = today + datetime.timedelta(days=1)
995
# This should be done outside the function to avoid recompiling it.
996
_date_re = re.compile(
997
r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
999
r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
1001
m = _date_re.match(val)
1002
if not m or (not m.group('date') and not m.group('time')):
1003
raise BzrError('Invalid revision date %r' % revision)
1006
year, month, day = int(m.group('year')), int(m.group('month')), int(m.group('day'))
1008
year, month, day = today.year, today.month, today.day
1010
hour = int(m.group('hour'))
1011
minute = int(m.group('minute'))
1012
if m.group('second'):
1013
second = int(m.group('second'))
1017
hour, minute, second = 0,0,0
1019
dt = datetime.datetime(year=year, month=month, day=day,
1020
hour=hour, minute=minute, second=second)
1024
if match_style == '-':
1026
elif match_style == '=':
1027
last = dt + datetime.timedelta(days=1)
1030
for i in range(len(revs)-1, -1, -1):
1031
r = self.get_revision(revs[i])
1032
# TODO: Handle timezone.
1033
dt = datetime.datetime.fromtimestamp(r.timestamp)
1034
if first >= dt and (last is None or dt >= last):
1037
for i in range(len(revs)):
1038
r = self.get_revision(revs[i])
1039
# TODO: Handle timezone.
1040
dt = datetime.datetime.fromtimestamp(r.timestamp)
1041
if first <= dt and (last is None or dt <= last):
1043
REVISION_NAMESPACES['date:'] = _namespace_date
937
1045
def revision_tree(self, revision_id):
938
1046
"""Return Tree for a revision on this branch.