15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20
from cStringIO import StringIO
23
21
from bzrlib.trace import mutter, note
24
from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, \
22
from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, splitpath, \
26
23
sha_file, appendpath, file_kind
28
from bzrlib.errors import (BzrError, InvalidRevisionNumber, InvalidRevisionId,
29
NoSuchRevision, HistoryMissing, NotBranchError,
31
from bzrlib.textui import show_status
32
from bzrlib.revision import Revision, validate_revision_id
33
from bzrlib.delta import compare_trees
34
from bzrlib.tree import EmptyTree, RevisionTree
35
from bzrlib.inventory import Inventory
36
from bzrlib.weavestore import WeaveStore
37
from bzrlib.store import ImmutableStore
42
INVENTORY_FILEID = '__inventory'
43
ANCESTRY_FILEID = '__ancestry'
46
BZR_BRANCH_FORMAT_4 = "Bazaar-NG branch, format 0.0.4\n"
47
BZR_BRANCH_FORMAT_5 = "Bazaar-NG branch, format 5\n"
24
from bzrlib.errors import BzrError
26
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
48
27
## TODO: Maybe include checks for common corruption of newlines, etc?
51
# TODO: Some operations like log might retrieve the same revisions
52
# repeatedly to calculate deltas. We could perhaps have a weakref
53
# cache in memory to make this faster. In general anything can be
54
# cached in memory between lock and unlock operations.
56
# TODO: please move the revision-string syntax stuff out of the branch
57
# object; it's clutter
60
31
def find_branch(f, **args):
61
32
if f and (f.startswith('http://') or f.startswith('https://')):
333
307
self.controlfile(f, 'w').write('')
334
308
mutter('created control directory in ' + self.base)
336
# if we want per-tree root ids then this is the place to set
337
# them; they're not needed for now and so ommitted for
339
f = self.controlfile('inventory','w')
340
bzrlib.xml5.serializer_v5.write_inventory(Inventory(), f)
344
def _check_format(self, relax_version_check):
310
pack_xml(Inventory(), self.controlfile('inventory','w'))
313
def _check_format(self):
345
314
"""Check this branch format is supported.
347
The format level is stored, as an integer, in
348
self._branch_format for code that needs to check it later.
316
The current tool only supports the current unstable format.
350
318
In the future, we might need different in-memory Branch
351
319
classes to support downlevel branches. But not yet.
321
# This ignores newlines so that we can open branches created
322
# on Windows from Linux and so on. I think it might be better
323
# to always make all internal files in unix format.
353
324
fmt = self.controlfile('branch-format', 'r').read()
354
if fmt == BZR_BRANCH_FORMAT_5:
355
self._branch_format = 5
356
elif fmt == BZR_BRANCH_FORMAT_4:
357
self._branch_format = 4
359
if (not relax_version_check
360
and self._branch_format != 5):
361
raise BzrError('sorry, branch format "%s" not supported; '
362
'use a different bzr version, '
363
'or run "bzr upgrade"'
364
% fmt.rstrip('\n\r'))
367
def get_root_id(self):
368
"""Return the id of this branches root"""
369
inv = self.read_working_inventory()
370
return inv.root.file_id
372
def set_root_id(self, file_id):
373
inv = self.read_working_inventory()
374
orig_root_id = inv.root.file_id
375
del inv._byid[inv.root.file_id]
376
inv.root.file_id = file_id
377
inv._byid[inv.root.file_id] = inv.root
380
if entry.parent_id in (None, orig_root_id):
381
entry.parent_id = inv.root.file_id
382
self._write_inventory(inv)
325
fmt.replace('\r\n', '')
326
if fmt != BZR_BRANCH_FORMAT:
327
raise BzrError('sorry, branch format %r not supported' % fmt,
328
['use a different bzr version',
329
'or remove the .bzr directory and "bzr init" again'])
384
333
def read_working_inventory(self):
385
334
"""Read the working inventory."""
335
from bzrlib.inventory import Inventory
336
from bzrlib.xml import unpack_xml
337
from time import time
388
341
# ElementTree does its own conversion from UTF-8, so open in
390
f = self.controlfile('inventory', 'rb')
391
return bzrlib.xml5.serializer_v5.read_inventory(f)
343
inv = unpack_xml(Inventory,
344
self.controlfile('inventory', 'rb'))
345
mutter("loaded inventory of %d items in %f"
346
% (len(inv), time() - before))
599
def has_revision(self, revision_id):
600
"""True if this branch has a copy of the revision.
602
This does not necessarily imply the revision is merge
603
or on the mainline."""
604
return revision_id in self.revision_store
607
def get_revision_xml_file(self, revision_id):
608
"""Return XML file object for revision object."""
609
if not revision_id or not isinstance(revision_id, basestring):
610
raise InvalidRevisionId(revision_id)
615
return self.revision_store[revision_id]
617
raise bzrlib.errors.NoSuchRevision(self, revision_id)
622
def get_revision_xml(self, revision_id):
623
return self.get_revision_xml_file(revision_id).read()
626
562
def get_revision(self, revision_id):
627
563
"""Return the Revision object for a named revision"""
628
xml_file = self.get_revision_xml_file(revision_id)
564
from bzrlib.revision import Revision
565
from bzrlib.xml import unpack_xml
631
r = bzrlib.xml5.serializer_v5.read_revision(xml_file)
632
except SyntaxError, e:
633
raise bzrlib.errors.BzrError('failed to unpack revision_xml',
569
if not revision_id or not isinstance(revision_id, basestring):
570
raise ValueError('invalid revision-id: %r' % revision_id)
571
r = unpack_xml(Revision, self.revision_store[revision_id])
637
575
assert r.revision_id == revision_id
641
def get_revision_delta(self, revno):
642
"""Return the delta for one revision.
644
The delta is relative to its mainline predecessor, or the
645
empty tree for revision 1.
647
assert isinstance(revno, int)
648
rh = self.revision_history()
649
if not (1 <= revno <= len(rh)):
650
raise InvalidRevisionNumber(revno)
652
# revno is 1-based; list is 0-based
654
new_tree = self.revision_tree(rh[revno-1])
656
old_tree = EmptyTree()
658
old_tree = self.revision_tree(rh[revno-2])
660
return compare_trees(old_tree, new_tree)
664
579
def get_revision_sha1(self, revision_id):
665
580
"""Hash the stored value of a revision, and return it."""
666
return bzrlib.osutils.sha_file(self.get_revision_xml_file(revision_id))
669
def get_ancestry(self, revision_id):
670
"""Return a list of revision-ids integrated by a revision.
672
w = self.weave_store.get_weave(ANCESTRY_FILEID)
674
return [l[:-1] for l in w.get_iter(w.lookup(revision_id))]
677
def get_inventory_weave(self):
678
return self.weave_store.get_weave(INVENTORY_FILEID)
681
def get_inventory(self, revision_id):
682
"""Get Inventory object by hash."""
683
# FIXME: The text gets passed around a lot coming from the weave.
684
f = StringIO(self.get_inventory_xml(revision_id))
685
return bzrlib.xml5.serializer_v5.read_inventory(f)
688
def get_inventory_xml(self, revision_id):
689
"""Get inventory XML as a file object."""
691
assert isinstance(revision_id, basestring), type(revision_id)
692
iw = self.get_inventory_weave()
693
return iw.get_text(iw.lookup(revision_id))
695
raise bzrlib.errors.HistoryMissing(self, 'inventory', revision_id)
698
def get_inventory_sha1(self, revision_id):
581
# In the future, revision entries will be signed. At that
582
# point, it is probably best *not* to include the signature
583
# in the revision hash. Because that lets you re-sign
584
# the revision, (add signatures/remove signatures) and still
585
# have all hash pointers stay consistent.
586
# But for now, just hash the contents.
587
return sha_file(self.revision_store[revision_id])
590
def get_inventory(self, inventory_id):
591
"""Get Inventory object by hash.
593
TODO: Perhaps for this and similar methods, take a revision
594
parameter which can be either an integer revno or a
596
from bzrlib.inventory import Inventory
597
from bzrlib.xml import unpack_xml
599
return unpack_xml(Inventory, self.inventory_store[inventory_id])
602
def get_inventory_sha1(self, inventory_id):
699
603
"""Return the sha1 hash of the inventory entry
701
return self.get_revision(revision_id).inventory_sha1
605
return sha_file(self.inventory_store[inventory_id])
704
608
def get_revision_inventory(self, revision_id):
705
609
"""Return inventory of a past revision."""
706
# bzr 0.0.6 and later imposes the constraint that the inventory_id
707
# must be the same as its revision, so this is trivial.
708
610
if revision_id == None:
709
return Inventory(self.get_root_id())
611
from bzrlib.inventory import Inventory
711
return self.get_inventory(revision_id)
614
return self.get_inventory(self.get_revision(revision_id).inventory_id)
714
617
def revision_history(self):
715
"""Return sequence of revision hashes on to this branch."""
618
"""Return sequence of revision hashes on to this branch.
620
>>> ScratchBranch().revision_history()
718
625
return [l.rstrip('\r\n') for l in
827
752
if stop_revision is None:
828
753
stop_revision = other_len
830
assert isinstance(stop_revision, int)
831
if stop_revision > other_len:
832
raise bzrlib.errors.NoSuchRevision(self, stop_revision)
754
elif stop_revision > other_len:
755
raise NoSuchRevision(self, stop_revision)
834
757
return other_history[self_len:stop_revision]
837
def update_revisions(self, other, stop_revno=None):
838
"""Pull in new perfect-fit revisions.
760
def update_revisions(self, other, stop_revision=None):
761
"""Pull in all new revisions from other branch.
763
>>> from bzrlib.commit import commit
764
>>> bzrlib.trace.silent = True
765
>>> br1 = ScratchBranch(files=['foo', 'bar'])
768
>>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False)
769
>>> br2 = ScratchBranch()
770
>>> br2.update_revisions(br1)
774
>>> br2.revision_history()
776
>>> br2.update_revisions(br1)
780
>>> br1.text_store.total_size() == br2.text_store.total_size()
840
from bzrlib.fetch import greedy_fetch
843
stop_revision = other.lookup_revision(stop_revno)
846
greedy_fetch(to_branch=self, from_branch=other,
847
revision=stop_revision)
849
pullable_revs = self.missing_revisions(other, stop_revision)
852
greedy_fetch(to_branch=self,
854
revision=pullable_revs[-1])
855
self.append_revision(*pullable_revs)
783
from bzrlib.progress import ProgressBar
787
from sets import Set as set
791
pb.update('comparing histories')
792
revision_ids = self.missing_revisions(other, stop_revision)
794
if hasattr(other.revision_store, "prefetch"):
795
other.revision_store.prefetch(revision_ids)
796
if hasattr(other.inventory_store, "prefetch"):
797
inventory_ids = [other.get_revision(r).inventory_id
798
for r in revision_ids]
799
other.inventory_store.prefetch(inventory_ids)
804
for rev_id in revision_ids:
806
pb.update('fetching revision', i, len(revision_ids))
807
rev = other.get_revision(rev_id)
808
revisions.append(rev)
809
inv = other.get_inventory(str(rev.inventory_id))
810
for key, entry in inv.iter_entries():
811
if entry.text_id is None:
813
if entry.text_id not in self.text_store:
814
needed_texts.add(entry.text_id)
818
count = self.text_store.copy_multi(other.text_store, needed_texts)
819
print "Added %d texts." % count
820
inventory_ids = [ f.inventory_id for f in revisions ]
821
count = self.inventory_store.copy_multi(other.inventory_store,
823
print "Added %d inventories." % count
824
revision_ids = [ f.revision_id for f in revisions]
825
count = self.revision_store.copy_multi(other.revision_store,
827
for revision_id in revision_ids:
828
self.append_revision(revision_id)
829
print "Added %d revisions." % count
858
832
def commit(self, *args, **kw):
859
from bzrlib.commit import Commit
860
Commit().commit(self, *args, **kw)
833
from bzrlib.commit import commit
834
commit(self, *args, **kw)
863
def lookup_revision(self, revision):
864
"""Return the revision identifier for a given revision information."""
865
revno, info = self._get_revision_info(revision)
869
def revision_id_to_revno(self, revision_id):
870
"""Given a revision id, return its revno"""
871
history = self.revision_history()
873
return history.index(revision_id) + 1
875
raise bzrlib.errors.NoSuchRevision(self, revision_id)
878
def get_revision_info(self, revision):
879
"""Return (revno, revision id) for revision identifier.
881
revision can be an integer, in which case it is assumed to be revno (though
882
this will translate negative values into positive ones)
883
revision can also be a string, in which case it is parsed for something like
884
'date:' or 'revid:' etc.
886
revno, rev_id = self._get_revision_info(revision)
888
raise bzrlib.errors.NoSuchRevision(self, revision)
891
def get_rev_id(self, revno, history=None):
892
"""Find the revision id of the specified revno."""
837
def lookup_revision(self, revno):
838
"""Return revision hash for revision number."""
896
history = self.revision_history()
897
elif revno <= 0 or revno > len(history):
898
raise bzrlib.errors.NoSuchRevision(self, revno)
899
return history[revno - 1]
901
def _get_revision_info(self, revision):
902
"""Return (revno, revision id) for revision specifier.
904
revision can be an integer, in which case it is assumed to be revno
905
(though this will translate negative values into positive ones)
906
revision can also be a string, in which case it is parsed for something
907
like 'date:' or 'revid:' etc.
909
A revid is always returned. If it is None, the specifier referred to
910
the null revision. If the revid does not occur in the revision
911
history, revno will be None.
917
try:# Convert to int if possible
918
revision = int(revision)
921
revs = self.revision_history()
922
if isinstance(revision, int):
924
revno = len(revs) + revision + 1
927
rev_id = self.get_rev_id(revno, revs)
928
elif isinstance(revision, basestring):
929
for prefix, func in Branch.REVISION_NAMESPACES.iteritems():
930
if revision.startswith(prefix):
931
result = func(self, revs, revision)
933
revno, rev_id = result
936
rev_id = self.get_rev_id(revno, revs)
939
raise BzrError('No namespace registered for string: %r' %
942
raise TypeError('Unhandled revision type %s' % revision)
946
raise bzrlib.errors.NoSuchRevision(self, revision)
949
def _namespace_revno(self, revs, revision):
950
"""Lookup a revision by revision number"""
951
assert revision.startswith('revno:')
953
return (int(revision[6:]),)
956
REVISION_NAMESPACES['revno:'] = _namespace_revno
958
def _namespace_revid(self, revs, revision):
959
assert revision.startswith('revid:')
960
rev_id = revision[len('revid:'):]
962
return revs.index(rev_id) + 1, rev_id
965
REVISION_NAMESPACES['revid:'] = _namespace_revid
967
def _namespace_last(self, revs, revision):
968
assert revision.startswith('last:')
970
offset = int(revision[5:])
975
raise BzrError('You must supply a positive value for --revision last:XXX')
976
return (len(revs) - offset + 1,)
977
REVISION_NAMESPACES['last:'] = _namespace_last
979
def _namespace_tag(self, revs, revision):
980
assert revision.startswith('tag:')
981
raise BzrError('tag: namespace registered, but not implemented.')
982
REVISION_NAMESPACES['tag:'] = _namespace_tag
984
def _namespace_date(self, revs, revision):
985
assert revision.startswith('date:')
987
# Spec for date revisions:
989
# value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
990
# it can also start with a '+/-/='. '+' says match the first
991
# entry after the given date. '-' is match the first entry before the date
992
# '=' is match the first entry after, but still on the given date.
994
# +2005-05-12 says find the first matching entry after May 12th, 2005 at 0:00
995
# -2005-05-12 says find the first matching entry before May 12th, 2005 at 0:00
996
# =2005-05-12 says find the first match after May 12th, 2005 at 0:00 but before
997
# May 13th, 2005 at 0:00
999
# So the proper way of saying 'give me all entries for today' is:
1000
# -r {date:+today}:{date:-tomorrow}
1001
# The default is '=' when not supplied
1004
if val[:1] in ('+', '-', '='):
1005
match_style = val[:1]
1008
today = datetime.datetime.today().replace(hour=0,minute=0,second=0,microsecond=0)
1009
if val.lower() == 'yesterday':
1010
dt = today - datetime.timedelta(days=1)
1011
elif val.lower() == 'today':
1013
elif val.lower() == 'tomorrow':
1014
dt = today + datetime.timedelta(days=1)
1017
# This should be done outside the function to avoid recompiling it.
1018
_date_re = re.compile(
1019
r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
1021
r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
1023
m = _date_re.match(val)
1024
if not m or (not m.group('date') and not m.group('time')):
1025
raise BzrError('Invalid revision date %r' % revision)
1028
year, month, day = int(m.group('year')), int(m.group('month')), int(m.group('day'))
1030
year, month, day = today.year, today.month, today.day
1032
hour = int(m.group('hour'))
1033
minute = int(m.group('minute'))
1034
if m.group('second'):
1035
second = int(m.group('second'))
1039
hour, minute, second = 0,0,0
1041
dt = datetime.datetime(year=year, month=month, day=day,
1042
hour=hour, minute=minute, second=second)
1046
if match_style == '-':
1048
elif match_style == '=':
1049
last = dt + datetime.timedelta(days=1)
1052
for i in range(len(revs)-1, -1, -1):
1053
r = self.get_revision(revs[i])
1054
# TODO: Handle timezone.
1055
dt = datetime.datetime.fromtimestamp(r.timestamp)
1056
if first >= dt and (last is None or dt >= last):
1059
for i in range(len(revs)):
1060
r = self.get_revision(revs[i])
1061
# TODO: Handle timezone.
1062
dt = datetime.datetime.fromtimestamp(r.timestamp)
1063
if first <= dt and (last is None or dt <= last):
1065
REVISION_NAMESPACES['date:'] = _namespace_date
843
# list is 0-based; revisions are 1-based
844
return self.revision_history()[revno-1]
846
raise BzrError("no such revision %s" % revno)
1067
849
def revision_tree(self, revision_id):
1068
850
"""Return Tree for a revision on this branch.
1070
852
`revision_id` may be None for the null revision, in which case
1071
853
an `EmptyTree` is returned."""
854
from bzrlib.tree import EmptyTree, RevisionTree
1072
855
# TODO: refactor this to use an existing revision object
1073
856
# so we don't need to read it in twice.
1074
857
if revision_id == None:
1075
858
return EmptyTree()
1077
860
inv = self.get_revision_inventory(revision_id)
1078
return RevisionTree(self.weave_store, inv, revision_id)
861
return RevisionTree(self.text_store, inv)
1081
864
def working_tree(self):