15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18
import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile
19
import traceback, socket, fnmatch, difflib, time
20
from binascii import hexlify
22
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
28
from bzrlib.textui import show_status
29
from bzrlib.revision import Revision
30
from bzrlib.xml import unpack_xml
31
from bzrlib.delta import compare_trees
32
from bzrlib.tree import EmptyTree, RevisionTree
33
from bzrlib.progress import ProgressBar
23
from inventory import Inventory
24
from trace import mutter, note
25
from tree import Tree, EmptyTree, RevisionTree
26
from inventory import InventoryEntry, Inventory
27
from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \
28
format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \
29
joinpath, sha_string, file_kind, local_time_offset, appendpath
30
from store import ImmutableStore
31
from revision import Revision
32
from errors import BzrError
33
from textui import show_status
36
35
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
37
36
## TODO: Maybe include checks for common corruption of newlines, etc?
40
# TODO: Some operations like log might retrieve the same revisions
41
# repeatedly to calculate deltas. We could perhaps have a weakref
42
# cache in memory to make this faster.
44
# TODO: please move the revision-string syntax stuff out of the branch
45
# object; it's clutter
48
40
def find_branch(f, **args):
49
41
if f and (f.startswith('http://') or f.startswith('https://')):
574
511
return self.working_tree().unknowns()
577
def append_revision(self, *revision_ids):
578
from bzrlib.atomicfile import AtomicFile
580
for revision_id in revision_ids:
581
mutter("add {%s} to revision-history" % revision_id)
514
def append_revision(self, revision_id):
515
mutter("add {%s} to revision-history" % revision_id)
583
516
rev_history = self.revision_history()
584
rev_history.extend(revision_ids)
586
f = AtomicFile(self.controlfilename('revision-history'))
588
for rev_id in rev_history:
595
def get_revision_xml(self, revision_id):
596
"""Return XML file object for revision object."""
597
if not revision_id or not isinstance(revision_id, basestring):
598
raise InvalidRevisionId(revision_id)
603
return self.revision_store[revision_id]
605
raise bzrlib.errors.NoSuchRevision(self, revision_id)
518
tmprhname = self.controlfilename('revision-history.tmp')
519
rhname = self.controlfilename('revision-history')
521
f = file(tmprhname, 'wt')
522
rev_history.append(revision_id)
523
f.write('\n'.join(rev_history))
527
if sys.platform == 'win32':
529
os.rename(tmprhname, rhname)
610
533
def get_revision(self, revision_id):
611
534
"""Return the Revision object for a named revision"""
612
xml_file = self.get_revision_xml(revision_id)
615
r = unpack_xml(Revision, xml_file)
616
except SyntaxError, e:
617
raise bzrlib.errors.BzrError('failed to unpack revision_xml',
535
r = Revision.read_xml(self.revision_store[revision_id])
621
536
assert r.revision_id == revision_id
625
def get_revision_delta(self, revno):
626
"""Return the delta for one revision.
628
The delta is relative to its mainline predecessor, or the
629
empty tree for revision 1.
631
assert isinstance(revno, int)
632
rh = self.revision_history()
633
if not (1 <= revno <= len(rh)):
634
raise InvalidRevisionNumber(revno)
636
# revno is 1-based; list is 0-based
638
new_tree = self.revision_tree(rh[revno-1])
640
old_tree = EmptyTree()
642
old_tree = self.revision_tree(rh[revno-2])
644
return compare_trees(old_tree, new_tree)
648
def get_revision_sha1(self, revision_id):
649
"""Hash the stored value of a revision, and return it."""
650
# In the future, revision entries will be signed. At that
651
# point, it is probably best *not* to include the signature
652
# in the revision hash. Because that lets you re-sign
653
# the revision, (add signatures/remove signatures) and still
654
# have all hash pointers stay consistent.
655
# But for now, just hash the contents.
656
return bzrlib.osutils.sha_file(self.get_revision_xml(revision_id))
659
540
def get_inventory(self, inventory_id):
660
541
"""Get Inventory object by hash.
662
543
TODO: Perhaps for this and similar methods, take a revision
663
544
parameter which can be either an integer revno or a
665
from bzrlib.inventory import Inventory
666
from bzrlib.xml import unpack_xml
668
return unpack_xml(Inventory, self.get_inventory_xml(inventory_id))
671
def get_inventory_xml(self, inventory_id):
672
"""Get inventory XML as a file object."""
673
return self.inventory_store[inventory_id]
676
def get_inventory_sha1(self, inventory_id):
677
"""Return the sha1 hash of the inventory entry
679
return sha_file(self.get_inventory_xml(inventory_id))
546
i = Inventory.read_xml(self.inventory_store[inventory_id])
682
550
def get_revision_inventory(self, revision_id):
683
551
"""Return inventory of a past revision."""
684
# bzr 0.0.6 imposes the constraint that the inventory_id
685
# must be the same as its revision, so this is trivial.
686
552
if revision_id == None:
687
from bzrlib.inventory import Inventory
688
return Inventory(self.get_root_id())
690
return self.get_inventory(revision_id)
555
return self.get_inventory(self.get_revision(revision_id).inventory_id)
693
558
def revision_history(self):
829
712
>>> br2.revision_history()
830
713
[u'REVISION-ID-1']
831
714
>>> br2.update_revisions(br1)
832
717
Added 0 revisions.
833
718
>>> br1.text_store.total_size() == br2.text_store.total_size()
836
from bzrlib.fetch import greedy_fetch
838
pb.update('comparing histories')
839
revision_ids = self.missing_revisions(other, stop_revision)
840
if len(revision_ids) > 0:
841
count = greedy_fetch(self, other, revision_ids[-1], pb)[0]
844
self.append_revision(*revision_ids)
845
print "Added %d revisions." % count
847
def install_revisions(self, other, revision_ids, pb=None):
850
if hasattr(other.revision_store, "prefetch"):
851
other.revision_store.prefetch(revision_ids)
852
if hasattr(other.inventory_store, "prefetch"):
853
inventory_ids = [other.get_revision(r).inventory_id
854
for r in revision_ids]
855
other.inventory_store.prefetch(inventory_ids)
861
for i, rev_id in enumerate(revision_ids):
862
pb.update('fetching revision', i+1, len(revision_ids))
864
rev = other.get_revision(rev_id)
865
except bzrlib.errors.NoSuchRevision:
868
revisions.append(rev)
721
revision_ids = self.missing_revisions(other)
722
revisions = [other.get_revision(f) for f in revision_ids]
723
needed_texts = sets.Set()
724
for rev in revisions:
869
725
inv = other.get_inventory(str(rev.inventory_id))
870
726
for key, entry in inv.iter_entries():
871
727
if entry.text_id is None:
873
729
if entry.text_id not in self.text_store:
874
730
needed_texts.add(entry.text_id)
878
count, cp_fail = self.text_store.copy_multi(other.text_store,
731
count = self.text_store.copy_multi(other.text_store, needed_texts)
880
732
print "Added %d texts." % count
881
733
inventory_ids = [ f.inventory_id for f in revisions ]
882
count, cp_fail = self.inventory_store.copy_multi(other.inventory_store,
734
count = self.inventory_store.copy_multi(other.inventory_store,
884
736
print "Added %d inventories." % count
885
737
revision_ids = [ f.revision_id for f in revisions]
886
count, cp_fail = self.revision_store.copy_multi(other.revision_store,
889
assert len(cp_fail) == 0
890
return count, failures
738
count = self.revision_store.copy_multi(other.revision_store,
740
for revision_id in revision_ids:
741
self.append_revision(revision_id)
742
print "Added %d revisions." % count
892
745
def commit(self, *args, **kw):
893
747
from bzrlib.commit import commit
894
748
commit(self, *args, **kw)
897
def lookup_revision(self, revision):
898
"""Return the revision identifier for a given revision information."""
899
revno, info = self.get_revision_info(revision)
902
def get_revision_info(self, revision):
903
"""Return (revno, revision id) for revision identifier.
905
revision can be an integer, in which case it is assumed to be revno (though
906
this will translate negative values into positive ones)
907
revision can also be a string, in which case it is parsed for something like
908
'date:' or 'revid:' etc.
913
try:# Convert to int if possible
914
revision = int(revision)
917
revs = self.revision_history()
918
if isinstance(revision, int):
921
# Mabye we should do this first, but we don't need it if revision == 0
923
revno = len(revs) + revision + 1
926
elif isinstance(revision, basestring):
927
for prefix, func in Branch.REVISION_NAMESPACES.iteritems():
928
if revision.startswith(prefix):
929
revno = func(self, revs, revision)
932
raise BzrError('No namespace registered for string: %r' % revision)
934
if revno is None or revno <= 0 or revno > len(revs):
935
raise BzrError("no such revision %s" % revision)
936
return revno, revs[revno-1]
938
def _namespace_revno(self, revs, revision):
939
"""Lookup a revision by revision number"""
940
assert revision.startswith('revno:')
942
return int(revision[6:])
945
REVISION_NAMESPACES['revno:'] = _namespace_revno
947
def _namespace_revid(self, revs, revision):
948
assert revision.startswith('revid:')
950
return revs.index(revision[6:]) + 1
953
REVISION_NAMESPACES['revid:'] = _namespace_revid
955
def _namespace_last(self, revs, revision):
956
assert revision.startswith('last:')
958
offset = int(revision[5:])
963
raise BzrError('You must supply a positive value for --revision last:XXX')
964
return len(revs) - offset + 1
965
REVISION_NAMESPACES['last:'] = _namespace_last
967
def _namespace_tag(self, revs, revision):
968
assert revision.startswith('tag:')
969
raise BzrError('tag: namespace registered, but not implemented.')
970
REVISION_NAMESPACES['tag:'] = _namespace_tag
972
def _namespace_date(self, revs, revision):
973
assert revision.startswith('date:')
975
# Spec for date revisions:
977
# value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
978
# it can also start with a '+/-/='. '+' says match the first
979
# entry after the given date. '-' is match the first entry before the date
980
# '=' is match the first entry after, but still on the given date.
982
# +2005-05-12 says find the first matching entry after May 12th, 2005 at 0:00
983
# -2005-05-12 says find the first matching entry before May 12th, 2005 at 0:00
984
# =2005-05-12 says find the first match after May 12th, 2005 at 0:00 but before
985
# May 13th, 2005 at 0:00
987
# So the proper way of saying 'give me all entries for today' is:
988
# -r {date:+today}:{date:-tomorrow}
989
# The default is '=' when not supplied
992
if val[:1] in ('+', '-', '='):
993
match_style = val[:1]
996
today = datetime.datetime.today().replace(hour=0,minute=0,second=0,microsecond=0)
997
if val.lower() == 'yesterday':
998
dt = today - datetime.timedelta(days=1)
999
elif val.lower() == 'today':
1001
elif val.lower() == 'tomorrow':
1002
dt = today + datetime.timedelta(days=1)
1005
# This should be done outside the function to avoid recompiling it.
1006
_date_re = re.compile(
1007
r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
1009
r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
1011
m = _date_re.match(val)
1012
if not m or (not m.group('date') and not m.group('time')):
1013
raise BzrError('Invalid revision date %r' % revision)
1016
year, month, day = int(m.group('year')), int(m.group('month')), int(m.group('day'))
1018
year, month, day = today.year, today.month, today.day
1020
hour = int(m.group('hour'))
1021
minute = int(m.group('minute'))
1022
if m.group('second'):
1023
second = int(m.group('second'))
1027
hour, minute, second = 0,0,0
1029
dt = datetime.datetime(year=year, month=month, day=day,
1030
hour=hour, minute=minute, second=second)
1034
if match_style == '-':
1036
elif match_style == '=':
1037
last = dt + datetime.timedelta(days=1)
1040
for i in range(len(revs)-1, -1, -1):
1041
r = self.get_revision(revs[i])
1042
# TODO: Handle timezone.
1043
dt = datetime.datetime.fromtimestamp(r.timestamp)
1044
if first >= dt and (last is None or dt >= last):
1047
for i in range(len(revs)):
1048
r = self.get_revision(revs[i])
1049
# TODO: Handle timezone.
1050
dt = datetime.datetime.fromtimestamp(r.timestamp)
1051
if first <= dt and (last is None or dt <= last):
1053
REVISION_NAMESPACES['date:'] = _namespace_date
751
def lookup_revision(self, revno):
752
"""Return revision hash for revision number."""
757
# list is 0-based; revisions are 1-based
758
return self.revision_history()[revno-1]
760
raise BzrError("no such revision %s" % revno)
1055
763
def revision_tree(self, revision_id):
1056
764
"""Return Tree for a revision on this branch.
1203
def revert(self, filenames, old_tree=None, backups=True):
1204
"""Restore selected files to the versions from a previous tree.
1207
If true (default) backups are made of files before
1210
from bzrlib.errors import NotVersionedError, BzrError
1211
from bzrlib.atomicfile import AtomicFile
1212
from bzrlib.osutils import backup_file
1214
inv = self.read_working_inventory()
1215
if old_tree is None:
1216
old_tree = self.basis_tree()
1217
old_inv = old_tree.inventory
1220
for fn in filenames:
1221
file_id = inv.path2id(fn)
1223
raise NotVersionedError("not a versioned file", fn)
1224
if not old_inv.has_id(file_id):
1225
raise BzrError("file not present in old tree", fn, file_id)
1226
nids.append((fn, file_id))
1228
# TODO: Rename back if it was previously at a different location
1230
# TODO: If given a directory, restore the entire contents from
1231
# the previous version.
1233
# TODO: Make a backup to a temporary file.
1235
# TODO: If the file previously didn't exist, delete it?
1236
for fn, file_id in nids:
1239
f = AtomicFile(fn, 'wb')
1241
f.write(old_tree.get_file(file_id).read())
1247
def pending_merges(self):
1248
"""Return a list of pending merges.
1250
These are revisions that have been merged into the working
1251
directory but not yet committed.
1253
cfn = self.controlfilename('pending-merges')
1254
if not os.path.exists(cfn):
1257
for l in self.controlfile('pending-merges', 'r').readlines():
1258
p.append(l.rstrip('\n'))
1262
def add_pending_merge(self, revision_id):
1263
from bzrlib.revision import validate_revision_id
1265
validate_revision_id(revision_id)
1267
p = self.pending_merges()
1268
if revision_id in p:
1270
p.append(revision_id)
1271
self.set_pending_merges(p)
1274
def set_pending_merges(self, rev_list):
1275
from bzrlib.atomicfile import AtomicFile
1278
f = AtomicFile(self.controlfilename('pending-merges'))
1290
912
class ScratchBranch(Branch):
1291
913
"""Special test class: a branch that cleans up after itself.