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
27
from bzrlib.errors import BzrError, InvalidRevisionNumber, InvalidRevisionId
29
from bzrlib.textui import show_status
30
from bzrlib.revision import Revision
31
from bzrlib.xml import unpack_xml
32
from bzrlib.delta import compare_trees
33
from bzrlib.tree import EmptyTree, RevisionTree
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_file, 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
38
35
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
39
36
## TODO: Maybe include checks for common corruption of newlines, etc?
42
# TODO: Some operations like log might retrieve the same revisions
43
# repeatedly to calculate deltas. We could perhaps have a weakref
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
40
def find_branch(f, **args):
51
41
if f and (f.startswith('http://') or f.startswith('https://')):
570
511
return self.working_tree().unknowns()
573
def append_revision(self, *revision_ids):
574
from bzrlib.atomicfile import AtomicFile
576
for revision_id in revision_ids:
577
mutter("add {%s} to revision-history" % revision_id)
514
def append_revision(self, revision_id):
515
mutter("add {%s} to revision-history" % revision_id)
579
516
rev_history = self.revision_history()
580
rev_history.extend(revision_ids)
582
f = AtomicFile(self.controlfilename('revision-history'))
584
for rev_id in rev_history:
591
def get_revision_xml(self, revision_id):
592
"""Return XML file object for revision object."""
593
if not revision_id or not isinstance(revision_id, basestring):
594
raise InvalidRevisionId(revision_id)
599
return self.revision_store[revision_id]
601
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)
606
533
def get_revision(self, revision_id):
607
534
"""Return the Revision object for a named revision"""
608
xml_file = self.get_revision_xml(revision_id)
611
r = unpack_xml(Revision, xml_file)
612
except SyntaxError, e:
613
raise bzrlib.errors.BzrError('failed to unpack revision_xml',
535
if not revision_id or not isinstance(revision_id, basestring):
536
raise ValueError('invalid revision-id: %r' % revision_id)
537
r = Revision.read_xml(self.revision_store[revision_id])
617
538
assert r.revision_id == revision_id
621
def get_revision_delta(self, revno):
622
"""Return the delta for one revision.
624
The delta is relative to its mainline predecessor, or the
625
empty tree for revision 1.
627
assert isinstance(revno, int)
628
rh = self.revision_history()
629
if not (1 <= revno <= len(rh)):
630
raise InvalidRevisionNumber(revno)
632
# revno is 1-based; list is 0-based
634
new_tree = self.revision_tree(rh[revno-1])
636
old_tree = EmptyTree()
638
old_tree = self.revision_tree(rh[revno-2])
640
return compare_trees(old_tree, new_tree)
644
541
def get_revision_sha1(self, revision_id):
645
542
"""Hash the stored value of a revision, and return it."""
646
543
# In the future, revision entries will be signed. At that
799
706
if common_index >= 0 and \
800
707
self_history[common_index] != other_history[common_index]:
801
708
raise DivergedBranches(self, other)
803
if stop_revision is None:
804
stop_revision = other_len
805
elif stop_revision > other_len:
806
raise bzrlib.errors.NoSuchRevision(self, stop_revision)
808
return other_history[self_len:stop_revision]
811
def update_revisions(self, other, stop_revision=None):
709
if self_len < other_len:
710
return other_history[self_len:]
714
def update_revisions(self, other):
812
715
"""Pull in all new revisions from other branch.
717
>>> from bzrlib.commit import commit
718
>>> bzrlib.trace.silent = True
719
>>> br1 = ScratchBranch(files=['foo', 'bar'])
722
>>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False)
723
>>> br2 = ScratchBranch()
724
>>> br2.update_revisions(br1)
728
>>> br2.revision_history()
730
>>> br2.update_revisions(br1)
734
>>> br1.text_store.total_size() == br2.text_store.total_size()
814
from bzrlib.fetch import greedy_fetch
816
pb = bzrlib.ui.ui_factory.progress_bar()
737
from bzrlib.progress import ProgressBar
817
741
pb.update('comparing histories')
819
revision_ids = self.missing_revisions(other, stop_revision)
821
if len(revision_ids) > 0:
822
count = greedy_fetch(self, other, revision_ids[-1], pb)[0]
825
self.append_revision(*revision_ids)
826
## note("Added %d revisions." % count)
829
def install_revisions(self, other, revision_ids, pb):
830
if hasattr(other.revision_store, "prefetch"):
831
other.revision_store.prefetch(revision_ids)
832
if hasattr(other.inventory_store, "prefetch"):
833
inventory_ids = [other.get_revision(r).inventory_id
834
for r in revision_ids]
835
other.inventory_store.prefetch(inventory_ids)
838
pb = bzrlib.ui.ui_factory.progress_bar()
742
revision_ids = self.missing_revisions(other)
744
needed_texts = sets.Set()
845
for i, rev_id in enumerate(revision_ids):
846
pb.update('fetching revision', i+1, len(revision_ids))
848
rev = other.get_revision(rev_id)
849
except bzrlib.errors.NoSuchRevision:
746
for rev_id in revision_ids:
748
pb.update('fetching revision', i, len(revision_ids))
749
rev = other.get_revision(rev_id)
853
750
revisions.append(rev)
854
751
inv = other.get_inventory(str(rev.inventory_id))
855
752
for key, entry in inv.iter_entries():
863
count, cp_fail = self.text_store.copy_multi(other.text_store,
865
#print "Added %d texts." % count
760
count = self.text_store.copy_multi(other.text_store, needed_texts)
761
print "Added %d texts." % count
866
762
inventory_ids = [ f.inventory_id for f in revisions ]
867
count, cp_fail = self.inventory_store.copy_multi(other.inventory_store,
869
#print "Added %d inventories." % count
763
count = self.inventory_store.copy_multi(other.inventory_store,
765
print "Added %d inventories." % count
870
766
revision_ids = [ f.revision_id for f in revisions]
872
count, cp_fail = self.revision_store.copy_multi(other.revision_store,
875
assert len(cp_fail) == 0
876
return count, failures
767
count = self.revision_store.copy_multi(other.revision_store,
769
for revision_id in revision_ids:
770
self.append_revision(revision_id)
771
print "Added %d revisions." % count
879
774
def commit(self, *args, **kw):
880
776
from bzrlib.commit import commit
881
777
commit(self, *args, **kw)
884
def lookup_revision(self, revision):
885
"""Return the revision identifier for a given revision information."""
886
revno, info = self._get_revision_info(revision)
890
def revision_id_to_revno(self, revision_id):
891
"""Given a revision id, return its revno"""
892
history = self.revision_history()
894
return history.index(revision_id) + 1
896
raise bzrlib.errors.NoSuchRevision(self, revision_id)
899
def get_revision_info(self, revision):
900
"""Return (revno, revision id) for revision identifier.
902
revision can be an integer, in which case it is assumed to be revno (though
903
this will translate negative values into positive ones)
904
revision can also be a string, in which case it is parsed for something like
905
'date:' or 'revid:' etc.
907
revno, rev_id = self._get_revision_info(revision)
909
raise bzrlib.errors.NoSuchRevision(self, revision)
912
def get_rev_id(self, revno, history=None):
913
"""Find the revision id of the specified revno."""
780
def lookup_revision(self, revno):
781
"""Return revision hash for revision number."""
917
history = self.revision_history()
918
elif revno <= 0 or revno > len(history):
919
raise bzrlib.errors.NoSuchRevision(self, revno)
920
return history[revno - 1]
922
def _get_revision_info(self, revision):
923
"""Return (revno, revision id) for revision specifier.
925
revision can be an integer, in which case it is assumed to be revno
926
(though this will translate negative values into positive ones)
927
revision can also be a string, in which case it is parsed for something
928
like 'date:' or 'revid:' etc.
930
A revid is always returned. If it is None, the specifier referred to
931
the null revision. If the revid does not occur in the revision
932
history, revno will be None.
938
try:# Convert to int if possible
939
revision = int(revision)
942
revs = self.revision_history()
943
if isinstance(revision, int):
945
revno = len(revs) + revision + 1
948
rev_id = self.get_rev_id(revno, revs)
949
elif isinstance(revision, basestring):
950
for prefix, func in Branch.REVISION_NAMESPACES.iteritems():
951
if revision.startswith(prefix):
952
result = func(self, revs, revision)
954
revno, rev_id = result
957
rev_id = self.get_rev_id(revno, revs)
960
raise BzrError('No namespace registered for string: %r' %
963
raise TypeError('Unhandled revision type %s' % revision)
967
raise bzrlib.errors.NoSuchRevision(self, revision)
970
def _namespace_revno(self, revs, revision):
971
"""Lookup a revision by revision number"""
972
assert revision.startswith('revno:')
974
return (int(revision[6:]),)
977
REVISION_NAMESPACES['revno:'] = _namespace_revno
979
def _namespace_revid(self, revs, revision):
980
assert revision.startswith('revid:')
981
rev_id = revision[len('revid:'):]
983
return revs.index(rev_id) + 1, rev_id
986
REVISION_NAMESPACES['revid:'] = _namespace_revid
988
def _namespace_last(self, revs, revision):
989
assert revision.startswith('last:')
991
offset = int(revision[5:])
996
raise BzrError('You must supply a positive value for --revision last:XXX')
997
return (len(revs) - offset + 1,)
998
REVISION_NAMESPACES['last:'] = _namespace_last
1000
def _namespace_tag(self, revs, revision):
1001
assert revision.startswith('tag:')
1002
raise BzrError('tag: namespace registered, but not implemented.')
1003
REVISION_NAMESPACES['tag:'] = _namespace_tag
1005
def _namespace_date(self, revs, revision):
1006
assert revision.startswith('date:')
1008
# Spec for date revisions:
1010
# value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
1011
# it can also start with a '+/-/='. '+' says match the first
1012
# entry after the given date. '-' is match the first entry before the date
1013
# '=' is match the first entry after, but still on the given date.
1015
# +2005-05-12 says find the first matching entry after May 12th, 2005 at 0:00
1016
# -2005-05-12 says find the first matching entry before May 12th, 2005 at 0:00
1017
# =2005-05-12 says find the first match after May 12th, 2005 at 0:00 but before
1018
# May 13th, 2005 at 0:00
1020
# So the proper way of saying 'give me all entries for today' is:
1021
# -r {date:+today}:{date:-tomorrow}
1022
# The default is '=' when not supplied
1025
if val[:1] in ('+', '-', '='):
1026
match_style = val[:1]
1029
today = datetime.datetime.today().replace(hour=0,minute=0,second=0,microsecond=0)
1030
if val.lower() == 'yesterday':
1031
dt = today - datetime.timedelta(days=1)
1032
elif val.lower() == 'today':
1034
elif val.lower() == 'tomorrow':
1035
dt = today + datetime.timedelta(days=1)
1038
# This should be done outside the function to avoid recompiling it.
1039
_date_re = re.compile(
1040
r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
1042
r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
1044
m = _date_re.match(val)
1045
if not m or (not m.group('date') and not m.group('time')):
1046
raise BzrError('Invalid revision date %r' % revision)
1049
year, month, day = int(m.group('year')), int(m.group('month')), int(m.group('day'))
1051
year, month, day = today.year, today.month, today.day
1053
hour = int(m.group('hour'))
1054
minute = int(m.group('minute'))
1055
if m.group('second'):
1056
second = int(m.group('second'))
1060
hour, minute, second = 0,0,0
1062
dt = datetime.datetime(year=year, month=month, day=day,
1063
hour=hour, minute=minute, second=second)
1067
if match_style == '-':
1069
elif match_style == '=':
1070
last = dt + datetime.timedelta(days=1)
1073
for i in range(len(revs)-1, -1, -1):
1074
r = self.get_revision(revs[i])
1075
# TODO: Handle timezone.
1076
dt = datetime.datetime.fromtimestamp(r.timestamp)
1077
if first >= dt and (last is None or dt >= last):
1080
for i in range(len(revs)):
1081
r = self.get_revision(revs[i])
1082
# TODO: Handle timezone.
1083
dt = datetime.datetime.fromtimestamp(r.timestamp)
1084
if first <= dt and (last is None or dt <= last):
1086
REVISION_NAMESPACES['date:'] = _namespace_date
786
# list is 0-based; revisions are 1-based
787
return self.revision_history()[revno-1]
789
raise BzrError("no such revision %s" % revno)
1088
792
def revision_tree(self, revision_id):
1089
793
"""Return Tree for a revision on this branch.
1236
def revert(self, filenames, old_tree=None, backups=True):
1237
"""Restore selected files to the versions from a previous tree.
1240
If true (default) backups are made of files before
1243
from bzrlib.errors import NotVersionedError, BzrError
1244
from bzrlib.atomicfile import AtomicFile
1245
from bzrlib.osutils import backup_file
1247
inv = self.read_working_inventory()
1248
if old_tree is None:
1249
old_tree = self.basis_tree()
1250
old_inv = old_tree.inventory
1253
for fn in filenames:
1254
file_id = inv.path2id(fn)
1256
raise NotVersionedError("not a versioned file", fn)
1257
if not old_inv.has_id(file_id):
1258
raise BzrError("file not present in old tree", fn, file_id)
1259
nids.append((fn, file_id))
1261
# TODO: Rename back if it was previously at a different location
1263
# TODO: If given a directory, restore the entire contents from
1264
# the previous version.
1266
# TODO: Make a backup to a temporary file.
1268
# TODO: If the file previously didn't exist, delete it?
1269
for fn, file_id in nids:
1272
f = AtomicFile(fn, 'wb')
1274
f.write(old_tree.get_file(file_id).read())
1280
def pending_merges(self):
1281
"""Return a list of pending merges.
1283
These are revisions that have been merged into the working
1284
directory but not yet committed.
1286
cfn = self.controlfilename('pending-merges')
1287
if not os.path.exists(cfn):
1290
for l in self.controlfile('pending-merges', 'r').readlines():
1291
p.append(l.rstrip('\n'))
1295
def add_pending_merge(self, revision_id):
1296
from bzrlib.revision import validate_revision_id
1298
validate_revision_id(revision_id)
1300
p = self.pending_merges()
1301
if revision_id in p:
1303
p.append(revision_id)
1304
self.set_pending_merges(p)
1307
def set_pending_merges(self, rev_list):
1308
from bzrlib.atomicfile import AtomicFile
1311
f = AtomicFile(self.controlfilename('pending-merges'))
1323
941
class ScratchBranch(Branch):
1324
942
"""Special test class: a branch that cleans up after itself.
1434
1046
name = re.sub(r'[^\w.]', '', name)
1436
1048
s = hexlify(rand_bytes(8))
1437
return '-'.join((name, compact_date(time()), s))
1441
"""Return a new tree-root file id."""
1442
return gen_file_id('TREE_ROOT')
1445
def pull_loc(branch):
1446
# TODO: Should perhaps just make attribute be 'base' in
1447
# RemoteBranch and Branch?
1448
if hasattr(branch, "baseurl"):
1449
return branch.baseurl
1454
def copy_branch(branch_from, to_location, revision=None):
1455
"""Copy branch_from into the existing directory to_location.
1457
If revision is not None, the head of the new branch will be revision.
1459
from bzrlib.merge import merge
1460
from bzrlib.branch import Branch
1461
br_to = Branch(to_location, init=True)
1462
br_to.set_root_id(branch_from.get_root_id())
1463
if revision is None:
1464
revno = branch_from.revno()
1466
revno, rev_id = branch_from.get_revision_info(revision)
1467
br_to.update_revisions(branch_from, stop_revision=revno)
1468
merge((to_location, -1), (to_location, 0), this_dir=to_location,
1469
check_clean=False, ignore_zero=True)
1470
from_location = pull_loc(branch_from)
1471
br_to.controlfile("x-pull", "wb").write(from_location + "\n")
1049
return '-'.join((name, compact_date(time.time()), s))