15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21
from warnings import warn
21
25
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"
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)
32
from bzrlib.textui import show_status
33
from bzrlib.revision import Revision, validate_revision_id
34
from bzrlib.delta import compare_trees
35
from bzrlib.tree import EmptyTree, RevisionTree
36
from bzrlib.inventory import Inventory
37
from bzrlib.weavestore import WeaveStore
38
from bzrlib.store import ImmutableStore
43
BZR_BRANCH_FORMAT_4 = "Bazaar-NG branch, format 0.0.4\n"
44
BZR_BRANCH_FORMAT_5 = "Bazaar-NG branch, format 5\n"
27
45
## 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')
48
# TODO: Some operations like log might retrieve the same revisions
49
# repeatedly to calculate deltas. We could perhaps have a weakref
50
# cache in memory to make this faster. In general anything can be
51
# cached in memory between lock and unlock operations.
53
def find_branch(*ignored, **ignored_too):
54
# XXX: leave this here for about one release, then remove it
55
raise NotImplementedError('find_branch() is not supported anymore, '
56
'please use one of the new branch constructors')
56
58
def _relpath(base, path):
57
59
"""Return path relative to base, or raise exception.
293
340
raise BzrError("invalid controlfile mode %r" % mode)
297
342
def _make_control(self):
298
from bzrlib.inventory import Inventory
299
from bzrlib.xml import pack_xml
301
343
os.mkdir(self.controlfilename([]))
302
344
self.controlfile('README', 'w').write(
303
345
"This is a Bazaar-NG control directory.\n"
304
346
"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'):
347
self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT_5)
348
for d in ('text-store', 'revision-store',
307
350
os.mkdir(self.controlfilename(d))
308
for f in ('revision-history', 'merged-patches',
309
'pending-merged-patches', 'branch-name',
351
for f in ('revision-history',
311
354
'pending-merges'):
312
355
self.controlfile(f, 'w').write('')
313
356
mutter('created control directory in ' + self.base)
315
pack_xml(Inventory(gen_root_id()), self.controlfile('inventory','w'))
318
def _check_format(self):
358
# if we want per-tree root ids then this is the place to set
359
# them; they're not needed for now and so ommitted for
361
f = self.controlfile('inventory','w')
362
bzrlib.xml5.serializer_v5.write_inventory(Inventory(), f)
365
def _check_format(self, relax_version_check):
319
366
"""Check this branch format is supported.
321
The current tool only supports the current unstable format.
368
The format level is stored, as an integer, in
369
self._branch_format for code that needs to check it later.
323
371
In the future, we might need different in-memory Branch
324
372
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:
375
fmt = self.controlfile('branch-format', 'r').read()
377
if e.errno == errno.ENOENT:
378
raise NotBranchError(self.base)
382
if fmt == BZR_BRANCH_FORMAT_5:
383
self._branch_format = 5
384
elif fmt == BZR_BRANCH_FORMAT_4:
385
self._branch_format = 4
387
if (not relax_version_check
388
and self._branch_format != 5):
332
389
raise BzrError('sorry, branch format %r not supported' % fmt,
333
390
['use a different bzr version',
334
391
'or remove the .bzr directory and "bzr init" again'])
625
def has_revision(self, revision_id):
626
"""True if this branch has a copy of the revision.
628
This does not necessarily imply the revision is merge
629
or on the mainline."""
630
return (revision_id is None
631
or revision_id in self.revision_store)
634
def get_revision_xml_file(self, revision_id):
635
"""Return XML file object for revision object."""
636
if not revision_id or not isinstance(revision_id, basestring):
637
raise InvalidRevisionId(revision_id)
642
return self.revision_store[revision_id]
643
except (IndexError, KeyError):
644
raise bzrlib.errors.NoSuchRevision(self, revision_id)
649
def get_revision_xml(self, revision_id):
650
return self.get_revision_xml_file(revision_id).read()
585
653
def get_revision(self, revision_id):
586
654
"""Return the Revision object for a named revision"""
587
from bzrlib.revision import Revision
588
from bzrlib.xml import unpack_xml
655
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])
658
r = bzrlib.xml5.serializer_v5.read_revision(xml_file)
659
except SyntaxError, e:
660
raise bzrlib.errors.BzrError('failed to unpack revision_xml',
598
664
assert r.revision_id == revision_id
668
def get_revision_delta(self, revno):
669
"""Return the delta for one revision.
671
The delta is relative to its mainline predecessor, or the
672
empty tree for revision 1.
674
assert isinstance(revno, int)
675
rh = self.revision_history()
676
if not (1 <= revno <= len(rh)):
677
raise InvalidRevisionNumber(revno)
679
# revno is 1-based; list is 0-based
681
new_tree = self.revision_tree(rh[revno-1])
683
old_tree = EmptyTree()
685
old_tree = self.revision_tree(rh[revno-2])
687
return compare_trees(old_tree, new_tree)
602
690
def get_revision_sha1(self, revision_id):
603
691
"""Hash the stored value of a revision, and return it."""
604
# In the future, revision entries will be signed. At that
605
# point, it is probably best *not* to include the signature
606
# in the revision hash. Because that lets you re-sign
607
# the revision, (add signatures/remove signatures) and still
608
# have all hash pointers stay consistent.
609
# 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):
692
return bzrlib.osutils.sha_file(self.get_revision_xml_file(revision_id))
695
def _get_ancestry_weave(self):
696
return self.control_weaves.get_weave('ancestry')
699
def get_ancestry(self, revision_id):
700
"""Return a list of revision-ids integrated by a revision.
703
if revision_id is None:
705
w = self._get_ancestry_weave()
706
return [None] + [l[:-1] for l in w.get_iter(w.lookup(revision_id))]
709
def get_inventory_weave(self):
710
return self.control_weaves.get_weave('inventory')
713
def get_inventory(self, revision_id):
714
"""Get Inventory object by hash."""
715
xml = self.get_inventory_xml(revision_id)
716
return bzrlib.xml5.serializer_v5.read_inventory_from_string(xml)
719
def get_inventory_xml(self, revision_id):
720
"""Get inventory XML as a file object."""
722
assert isinstance(revision_id, basestring), type(revision_id)
723
iw = self.get_inventory_weave()
724
return iw.get_text(iw.lookup(revision_id))
726
raise bzrlib.errors.HistoryMissing(self, 'inventory', revision_id)
729
def get_inventory_sha1(self, revision_id):
626
730
"""Return the sha1 hash of the inventory entry
628
return sha_file(self.inventory_store[inventory_id])
732
return self.get_revision(revision_id).inventory_sha1
631
735
def get_revision_inventory(self, revision_id):
632
736
"""Return inventory of a past revision."""
633
# bzr 0.0.6 imposes the constraint that the inventory_id
737
# TODO: Unify this with get_inventory()
738
# bzr 0.0.6 and later imposes the constraint that the inventory_id
634
739
# must be the same as its revision, so this is trivial.
635
740
if revision_id == None:
636
from bzrlib.inventory import Inventory
637
741
return Inventory(self.get_root_id())
639
743
return self.get_inventory(revision_id)
642
746
def revision_history(self):
643
"""Return sequence of revision hashes on to this branch.
645
>>> ScratchBranch().revision_history()
747
"""Return sequence of revision hashes on to this branch."""
650
750
return [l.rstrip('\r\n') for l in
777
860
if stop_revision is None:
778
861
stop_revision = other_len
779
elif stop_revision > other_len:
780
raise NoSuchRevision(self, stop_revision)
863
assert isinstance(stop_revision, int)
864
if stop_revision > other_len:
865
raise bzrlib.errors.NoSuchRevision(self, stop_revision)
782
866
return other_history[self_len:stop_revision]
785
868
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
812
pb.update('comparing histories')
813
revision_ids = self.missing_revisions(other, stop_revision)
815
if hasattr(other.revision_store, "prefetch"):
816
other.revision_store.prefetch(revision_ids)
817
if hasattr(other.inventory_store, "prefetch"):
818
inventory_ids = [other.get_revision(r).inventory_id
819
for r in revision_ids]
820
other.inventory_store.prefetch(inventory_ids)
825
for rev_id in revision_ids:
827
pb.update('fetching revision', i, len(revision_ids))
828
rev = other.get_revision(rev_id)
829
revisions.append(rev)
830
inv = other.get_inventory(str(rev.inventory_id))
831
for key, entry in inv.iter_entries():
832
if entry.text_id is None:
834
if entry.text_id not in self.text_store:
835
needed_texts.add(entry.text_id)
839
count = self.text_store.copy_multi(other.text_store, needed_texts)
840
print "Added %d texts." % count
841
inventory_ids = [ f.inventory_id for f in revisions ]
842
count = self.inventory_store.copy_multi(other.inventory_store,
844
print "Added %d inventories." % count
845
revision_ids = [ f.revision_id for f in revisions]
846
count = self.revision_store.copy_multi(other.revision_store,
848
for revision_id in revision_ids:
849
self.append_revision(revision_id)
850
print "Added %d revisions." % count
869
"""Pull in new perfect-fit revisions."""
870
from bzrlib.fetch import greedy_fetch
871
from bzrlib.revision import get_intervening_revisions
872
if stop_revision is None:
873
stop_revision = other.last_revision()
874
greedy_fetch(to_branch=self, from_branch=other,
875
revision=stop_revision)
876
pullable_revs = self.missing_revisions(
877
other, other.revision_id_to_revno(stop_revision))
879
greedy_fetch(to_branch=self,
881
revision=pullable_revs[-1])
882
self.append_revision(*pullable_revs)
853
884
def commit(self, *args, **kw):
854
from bzrlib.commit import commit
855
commit(self, *args, **kw)
858
def lookup_revision(self, revision):
859
"""Return the revision identifier for a given revision information."""
860
revno, info = self.get_revision_info(revision)
863
def get_revision_info(self, revision):
864
"""Return (revno, revision id) for revision identifier.
866
revision can be an integer, in which case it is assumed to be revno (though
867
this will translate negative values into positive ones)
868
revision can also be a string, in which case it is parsed for something like
869
'date:' or 'revid:' etc.
874
try:# Convert to int if possible
875
revision = int(revision)
878
revs = self.revision_history()
879
if isinstance(revision, int):
882
# Mabye we should do this first, but we don't need it if revision == 0
884
revno = len(revs) + revision + 1
887
elif isinstance(revision, basestring):
888
for prefix, func in Branch.REVISION_NAMESPACES.iteritems():
889
if revision.startswith(prefix):
890
revno = func(self, revs, revision)
893
raise BzrError('No namespace registered for string: %r' % revision)
895
if revno is None or revno <= 0 or revno > len(revs):
896
raise BzrError("no such revision %s" % revision)
897
return revno, revs[revno-1]
899
def _namespace_revno(self, revs, revision):
900
"""Lookup a revision by revision number"""
901
assert revision.startswith('revno:')
903
return int(revision[6:])
906
REVISION_NAMESPACES['revno:'] = _namespace_revno
908
def _namespace_revid(self, revs, revision):
909
assert revision.startswith('revid:')
911
return revs.index(revision[6:]) + 1
914
REVISION_NAMESPACES['revid:'] = _namespace_revid
916
def _namespace_last(self, revs, revision):
917
assert revision.startswith('last:')
919
offset = int(revision[5:])
924
raise BzrError('You must supply a positive value for --revision last:XXX')
925
return len(revs) - offset + 1
926
REVISION_NAMESPACES['last:'] = _namespace_last
928
def _namespace_tag(self, revs, revision):
929
assert revision.startswith('tag:')
930
raise BzrError('tag: namespace registered, but not implemented.')
931
REVISION_NAMESPACES['tag:'] = _namespace_tag
933
def _namespace_date(self, revs, revision):
934
assert revision.startswith('date:')
936
# Spec for date revisions:
938
# value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
939
# it can also start with a '+/-/='. '+' says match the first
940
# entry after the given date. '-' is match the first entry before the date
941
# '=' is match the first entry after, but still on the given date.
943
# +2005-05-12 says find the first matching entry after May 12th, 2005 at 0:00
944
# -2005-05-12 says find the first matching entry before May 12th, 2005 at 0:00
945
# =2005-05-12 says find the first match after May 12th, 2005 at 0:00 but before
946
# May 13th, 2005 at 0:00
948
# So the proper way of saying 'give me all entries for today' is:
949
# -r {date:+today}:{date:-tomorrow}
950
# The default is '=' when not supplied
953
if val[:1] in ('+', '-', '='):
954
match_style = val[:1]
957
today = datetime.datetime.today().replace(hour=0,minute=0,second=0,microsecond=0)
958
if val.lower() == 'yesterday':
959
dt = today - datetime.timedelta(days=1)
960
elif val.lower() == 'today':
962
elif val.lower() == 'tomorrow':
963
dt = today + datetime.timedelta(days=1)
966
# This should be done outside the function to avoid recompiling it.
967
_date_re = re.compile(
968
r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
970
r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
972
m = _date_re.match(val)
973
if not m or (not m.group('date') and not m.group('time')):
974
raise BzrError('Invalid revision date %r' % revision)
977
year, month, day = int(m.group('year')), int(m.group('month')), int(m.group('day'))
979
year, month, day = today.year, today.month, today.day
981
hour = int(m.group('hour'))
982
minute = int(m.group('minute'))
983
if m.group('second'):
984
second = int(m.group('second'))
988
hour, minute, second = 0,0,0
990
dt = datetime.datetime(year=year, month=month, day=day,
991
hour=hour, minute=minute, second=second)
995
if match_style == '-':
997
elif match_style == '=':
998
last = dt + datetime.timedelta(days=1)
1001
for i in range(len(revs)-1, -1, -1):
1002
r = self.get_revision(revs[i])
1003
# TODO: Handle timezone.
1004
dt = datetime.datetime.fromtimestamp(r.timestamp)
1005
if first >= dt and (last is None or dt >= last):
1008
for i in range(len(revs)):
1009
r = self.get_revision(revs[i])
1010
# TODO: Handle timezone.
1011
dt = datetime.datetime.fromtimestamp(r.timestamp)
1012
if first <= dt and (last is None or dt <= last):
1014
REVISION_NAMESPACES['date:'] = _namespace_date
885
from bzrlib.commit import Commit
886
Commit().commit(self, *args, **kw)
888
def revision_id_to_revno(self, revision_id):
889
"""Given a revision id, return its revno"""
890
if revision_id is None:
892
history = self.revision_history()
894
return history.index(revision_id) + 1
896
raise bzrlib.errors.NoSuchRevision(self, revision_id)
898
def get_rev_id(self, revno, history=None):
899
"""Find the revision id of the specified revno."""
903
history = self.revision_history()
904
elif revno <= 0 or revno > len(history):
905
raise bzrlib.errors.NoSuchRevision(self, revno)
906
return history[revno - 1]
1016
908
def revision_tree(self, revision_id):
1017
909
"""Return Tree for a revision on this branch.
1019
911
`revision_id` may be None for the null revision, in which case
1020
912
an `EmptyTree` is returned."""
1021
from bzrlib.tree import EmptyTree, RevisionTree
1022
913
# TODO: refactor this to use an existing revision object
1023
914
# so we don't need to read it in twice.
1024
915
if revision_id == None:
1025
return EmptyTree(self.get_root_id())
1027
918
inv = self.get_revision_inventory(revision_id)
1028
return RevisionTree(self.text_store, inv)
919
return RevisionTree(self.weave_store, inv, revision_id)
1031
922
def working_tree(self):
1032
923
"""Return a `Tree` for the working copy."""
1033
from workingtree import WorkingTree
924
from bzrlib.workingtree import WorkingTree
1034
925
return WorkingTree(self.base, self.read_working_inventory())