22
22
from bzrlib.trace import mutter, note
23
23
from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, \
25
sha_file, appendpath, file_kind
24
rename, splitpath, sha_file, appendpath, file_kind
27
from bzrlib.errors import BzrError, InvalidRevisionNumber, InvalidRevisionId
26
from bzrlib.errors import BzrError, InvalidRevisionNumber, InvalidRevisionId, \
27
DivergedBranches, NotBranchError
29
28
from bzrlib.textui import show_status
30
29
from bzrlib.revision import Revision
31
from bzrlib.xml import unpack_xml
32
30
from bzrlib.delta import compare_trees
33
31
from bzrlib.tree import EmptyTree, RevisionTree
43
42
# repeatedly to calculate deltas. We could perhaps have a weakref
44
43
# 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')
45
def find_branch(*ignored, **ignored_too):
46
# XXX: leave this here for about one release, then remove it
47
raise NotImplementedError('find_branch() is not supported anymore, '
48
'please use one of the new branch constructors')
75
50
def _relpath(base, path):
76
51
"""Return path relative to base, or raise exception.
148
115
"""Branch holding a history of revisions.
151
Base directory of the branch.
118
Base directory/url of the branch.
122
def __init__(self, *ignored, **ignored_too):
123
raise NotImplementedError('The Branch class is abstract')
127
"""Open an existing branch, rooted at 'base' (url)"""
128
if base and (base.startswith('http://') or base.startswith('https://')):
129
from bzrlib.remotebranch import RemoteBranch
130
return RemoteBranch(base, find_root=False)
132
return LocalBranch(base, find_root=False)
135
def open_containing(url):
136
"""Open an existing branch which contains url.
138
This probes for a branch at url, and searches upwards from there.
140
if url and (url.startswith('http://') or url.startswith('https://')):
141
from bzrlib.remotebranch import RemoteBranch
142
return RemoteBranch(url)
144
return LocalBranch(url)
147
def initialize(base):
148
"""Create a new branch, rooted at 'base' (url)"""
149
if base and (base.startswith('http://') or base.startswith('https://')):
150
from bzrlib.remotebranch import RemoteBranch
151
return RemoteBranch(base, init=True)
153
return LocalBranch(base, init=True)
155
def setup_caching(self, cache_root):
156
"""Subclasses that care about caching should override this, and set
157
up cached stores located under cache_root.
161
class LocalBranch(Branch):
162
"""A branch stored in the actual filesystem.
164
Note that it's "local" in the context of the filesystem; it doesn't
165
really matter if it's on an nfs/smb/afs/coda/... share, as long as
166
it's writable, and can be accessed via the normal filesystem API.
154
169
None, or 'r' or 'w'
696
709
def common_ancestor(self, other, self_revno=None, other_revno=None):
711
>>> from bzrlib.commit import commit
699
712
>>> sb = ScratchBranch(files=['foo', 'foo~'])
700
713
>>> sb.common_ancestor(sb) == (None, None)
702
>>> commit.commit(sb, "Committing first revision", verbose=False)
715
>>> commit(sb, "Committing first revision", verbose=False)
703
716
>>> sb.common_ancestor(sb)[0]
705
718
>>> clone = sb.clone()
706
>>> commit.commit(sb, "Committing second revision", verbose=False)
719
>>> commit(sb, "Committing second revision", verbose=False)
707
720
>>> sb.common_ancestor(sb)[0]
709
722
>>> sb.common_ancestor(clone)[0]
711
>>> commit.commit(clone, "Committing divergent second revision",
724
>>> commit(clone, "Committing divergent second revision",
712
725
... verbose=False)
713
726
>>> sb.common_ancestor(clone)[0]
805
818
"""Pull in all new revisions from other branch.
807
820
from bzrlib.fetch import greedy_fetch
821
from bzrlib.revision import get_intervening_revisions
809
823
pb = bzrlib.ui.ui_factory.progress_bar()
810
824
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]
825
if stop_revision is None:
826
other_revision = other.last_patch()
828
other_revision = other.get_rev_id(stop_revision)
829
count = greedy_fetch(self, other, other_revision, pb)[0]
831
revision_ids = self.missing_revisions(other, stop_revision)
832
except DivergedBranches, e:
834
revision_ids = get_intervening_revisions(self.last_patch(),
835
other_revision, self)
836
assert self.last_patch() not in revision_ids
837
except bzrlib.errors.NotAncestor:
818
840
self.append_revision(*revision_ids)
819
## note("Added %d revisions." % count)
822
843
def install_revisions(self, other, revision_ids, pb):
823
844
if hasattr(other.revision_store, "prefetch"):
824
845
other.revision_store.prefetch(revision_ids)
825
846
if hasattr(other.inventory_store, "prefetch"):
826
inventory_ids = [other.get_revision(r).inventory_id
827
for r in revision_ids]
848
for rev_id in revision_ids:
850
revision = other.get_revision(rev_id).inventory_id
851
inventory_ids.append(revision)
852
except bzrlib.errors.NoSuchRevision:
828
854
other.inventory_store.prefetch(inventory_ids)
888
907
except ValueError:
889
908
raise bzrlib.errors.NoSuchRevision(self, revision_id)
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
910
def get_rev_id(self, revno, history=None):
911
"""Find the revision id of the specified revno."""
915
history = self.revision_history()
916
elif revno <= 0 or revno > len(history):
917
raise bzrlib.errors.NoSuchRevision(self, revno)
918
return history[revno - 1]
1045
921
def revision_tree(self, revision_id):
1046
922
"""Return Tree for a revision on this branch.
1284
class ScratchBranch(Branch):
1159
def get_parent(self):
1160
"""Return the parent location of the branch.
1162
This is the default location for push/pull/missing. The usual
1163
pattern is that the user can override it by specifying a
1167
_locs = ['parent', 'pull', 'x-pull']
1170
return self.controlfile(l, 'r').read().strip('\n')
1172
if e.errno != errno.ENOENT:
1177
def set_parent(self, url):
1178
# TODO: Maybe delete old location files?
1179
from bzrlib.atomicfile import AtomicFile
1182
f = AtomicFile(self.controlfilename('parent'))
1191
def check_revno(self, revno):
1193
Check whether a revno corresponds to any revision.
1194
Zero (the NULL revision) is considered valid.
1197
self.check_real_revno(revno)
1199
def check_real_revno(self, revno):
1201
Check whether a revno corresponds to a real revision.
1202
Zero (the NULL revision) is considered invalid
1204
if revno < 1 or revno > self.revno():
1205
raise InvalidRevisionNumber(revno)
1211
class ScratchBranch(LocalBranch):
1285
1212
"""Special test class: a branch that cleans up after itself.
1287
1214
>>> b = ScratchBranch()
1403
1336
return gen_file_id('TREE_ROOT')
1406
def pull_loc(branch):
1407
# TODO: Should perhaps just make attribute be 'base' in
1408
# RemoteBranch and Branch?
1409
if hasattr(branch, "baseurl"):
1410
return branch.baseurl
1415
def copy_branch(branch_from, to_location, revision=None):
1339
def copy_branch(branch_from, to_location, revno=None):
1416
1340
"""Copy branch_from into the existing directory to_location.
1418
If revision is not None, the head of the new branch will be revision.
1343
If not None, only revisions up to this point will be copied.
1344
The head of the new branch will be that revision.
1347
The name of a local directory that exists but is empty.
1420
1349
from bzrlib.merge import merge
1421
from bzrlib.branch import Branch
1422
br_to = Branch(to_location, init=True)
1351
assert isinstance(branch_from, Branch)
1352
assert isinstance(to_location, basestring)
1354
br_to = Branch.initialize(to_location)
1423
1355
br_to.set_root_id(branch_from.get_root_id())
1424
if revision is None:
1425
1357
revno = branch_from.revno()
1427
revno, rev_id = branch_from.get_revision_info(revision)
1428
1358
br_to.update_revisions(branch_from, stop_revision=revno)
1429
1359
merge((to_location, -1), (to_location, 0), this_dir=to_location,
1430
1360
check_clean=False, ignore_zero=True)
1431
from_location = pull_loc(branch_from)
1432
br_to.controlfile("x-pull", "wb").write(from_location + "\n")
1361
br_to.set_parent(branch_from.base)