23
23
from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, \
25
25
sha_file, appendpath, file_kind
26
from bzrlib.errors import BzrError, InvalidRevisionNumber, InvalidRevisionId
27
from bzrlib.errors import (BzrError, InvalidRevisionNumber, InvalidRevisionId,
28
29
from bzrlib.textui import show_status
29
30
from bzrlib.revision import Revision
30
from bzrlib.xml import unpack_xml
31
31
from bzrlib.delta import compare_trees
32
32
from bzrlib.tree import EmptyTree, RevisionTree
34
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
33
from bzrlib.inventory import Inventory
34
from bzrlib.weavestore import WeaveStore
40
BZR_BRANCH_FORMAT_4 = "Bazaar-NG branch, format 0.0.4\n"
41
BZR_BRANCH_FORMAT_5 = "Bazaar-NG branch, format 5\n"
35
42
## TODO: Maybe include checks for common corruption of newlines, etc?
120
131
head, tail = os.path.split(f)
122
133
# reached the root, whatever that may be
123
raise BzrError('%r is not in a branch' % orig_f)
134
raise bzrlib.errors.NotBranchError('%s is not in a branch' % orig_f)
139
# XXX: move into bzrlib.errors; subclass BzrError
126
140
class DivergedBranches(Exception):
127
141
def __init__(self, branch1, branch2):
128
142
self.branch1 = branch1
251
261
self._lock = None
252
262
self._lock_mode = self._lock_count = None
255
264
def abspath(self, name):
256
265
"""Return absolute filename for something in the branch"""
257
266
return os.path.join(self.base, name)
260
268
def relpath(self, path):
261
269
"""Return path relative to this branch of something inside it.
263
271
Raises an error if path is not in this branch."""
264
272
return _relpath(self.base, path)
267
274
def controlfilename(self, file_or_path):
268
275
"""Return location relative to branch."""
269
276
if isinstance(file_or_path, basestring):
297
304
raise BzrError("invalid controlfile mode %r" % mode)
301
306
def _make_control(self):
302
from bzrlib.inventory import Inventory
303
from bzrlib.xml import pack_xml
305
307
os.mkdir(self.controlfilename([]))
306
308
self.controlfile('README', 'w').write(
307
309
"This is a Bazaar-NG control directory.\n"
308
310
"Do not change any files in this directory.\n")
309
self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT)
310
for d in ('text-store', 'inventory-store', 'revision-store'):
311
self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT_5)
312
for d in ('text-store', 'inventory-store', 'revision-store',
311
314
os.mkdir(self.controlfilename(d))
312
315
for f in ('revision-history', 'merged-patches',
313
316
'pending-merged-patches', 'branch-name',
316
319
self.controlfile(f, 'w').write('')
317
320
mutter('created control directory in ' + self.base)
319
pack_xml(Inventory(gen_root_id()), self.controlfile('inventory','w'))
322
# if we want per-tree root ids then this is the place to set
323
# them; they're not needed for now and so ommitted for
325
f = self.controlfile('inventory','w')
326
bzrlib.xml5.serializer_v5.write_inventory(Inventory(), f)
322
329
def _check_format(self):
323
330
"""Check this branch format is supported.
325
The current tool only supports the current unstable format.
332
The format level is stored, as an integer, in
333
self._branch_format for code that needs to check it later.
327
335
In the future, we might need different in-memory Branch
328
336
classes to support downlevel branches. But not yet.
330
# This ignores newlines so that we can open branches created
331
# on Windows from Linux and so on. I think it might be better
332
# to always make all internal files in unix format.
333
338
fmt = self.controlfile('branch-format', 'r').read()
334
fmt.replace('\r\n', '')
335
if fmt != BZR_BRANCH_FORMAT:
336
raise BzrError('sorry, branch format %r not supported' % fmt,
337
['use a different bzr version',
338
'or remove the .bzr directory and "bzr init" again'])
339
if fmt == BZR_BRANCH_FORMAT_5:
340
self._branch_format = 5
342
raise BzrError('sorry, branch format "%s" not supported; '
343
'use a different bzr version, '
344
'or run "bzr upgrade", '
345
'or remove the .bzr directory and "bzr init" again'
346
% fmt.rstrip('\n\r'))
340
348
def get_root_id(self):
341
349
"""Return the id of this branches root"""
357
365
def read_working_inventory(self):
358
366
"""Read the working inventory."""
359
from bzrlib.inventory import Inventory
360
from bzrlib.xml import unpack_xml
361
from time import time
365
369
# ElementTree does its own conversion from UTF-8, so open in
367
inv = unpack_xml(Inventory,
368
self.controlfile('inventory', 'rb'))
369
mutter("loaded inventory of %d items in %f"
370
% (len(inv), time() - before))
371
f = self.controlfile('inventory', 'rb')
372
return bzrlib.xml5.serializer_v5.read_inventory(f)
400
400
"""Inventory for the working copy.""")
403
def add(self, files, verbose=False, ids=None):
403
def add(self, files, ids=None):
404
404
"""Make files versioned.
406
Note that the command line normally calls smart_add instead.
406
Note that the command line normally calls smart_add instead,
407
which can automatically recurse.
408
409
This puts the files in the Added state, so that they will be
409
410
recorded by the next commit.
419
420
TODO: Perhaps have an option to add the ids even if the files do
422
TODO: Perhaps return the ids of the files? But then again it
423
is easy to retrieve them if they're needed.
425
TODO: Adding a directory should optionally recurse down and
426
add all non-ignored children. Perhaps do that in a
423
TODO: Perhaps yield the ids and paths as they're added.
429
425
# TODO: Re-adding a file that is removed in the working copy
430
426
# should probably put it back with the previous ID.
595
588
return self.revision_store[revision_id]
596
589
except IndexError:
597
raise bzrlib.errors.NoSuchRevision(revision_id)
590
raise bzrlib.errors.NoSuchRevision(self, revision_id)
596
get_revision_xml = get_revision_xml_file
602
599
def get_revision(self, revision_id):
603
600
"""Return the Revision object for a named revision"""
604
xml_file = self.get_revision_xml(revision_id)
601
xml_file = self.get_revision_xml_file(revision_id)
607
r = unpack_xml(Revision, xml_file)
604
r = bzrlib.xml5.serializer_v5.read_revision(xml_file)
608
605
except SyntaxError, e:
609
606
raise bzrlib.errors.BzrError('failed to unpack revision_xml',
648
645
return bzrlib.osutils.sha_file(self.get_revision_xml(revision_id))
651
def get_inventory(self, inventory_id):
648
def get_inventory(self, revision_id):
652
649
"""Get Inventory object by hash.
654
651
TODO: Perhaps for this and similar methods, take a revision
655
652
parameter which can be either an integer revno or a
657
from bzrlib.inventory import Inventory
658
from bzrlib.xml import unpack_xml
660
return unpack_xml(Inventory, self.inventory_store[inventory_id])
654
f = self.get_inventory_xml_file(revision_id)
655
return bzrlib.xml5.serializer_v5.read_inventory(f)
658
def get_inventory_xml(self, revision_id):
659
"""Get inventory XML as a file object."""
661
assert isinstance(revision_id, basestring), type(revision_id)
662
return self.inventory_store[revision_id]
664
raise bzrlib.errors.HistoryMissing(self, 'inventory', revision_id)
666
get_inventory_xml_file = get_inventory_xml
663
def get_inventory_sha1(self, inventory_id):
669
def get_inventory_sha1(self, revision_id):
664
670
"""Return the sha1 hash of the inventory entry
666
return sha_file(self.inventory_store[inventory_id])
672
return sha_file(self.get_inventory_xml_file(revision_id))
669
675
def get_revision_inventory(self, revision_id):
758
def missing_revisions(self, other, stop_revision=None):
763
def missing_revisions(self, other, stop_revision=None, diverged_ok=False):
760
765
If self and other have not diverged, return a list of the revisions
761
766
present in other, but missing from self.
794
799
if stop_revision is None:
795
800
stop_revision = other_len
796
801
elif stop_revision > other_len:
797
raise NoSuchRevision(self, stop_revision)
802
raise bzrlib.errors.NoSuchRevision(self, stop_revision)
799
804
return other_history[self_len:stop_revision]
802
807
def update_revisions(self, other, stop_revision=None):
803
808
"""Pull in all new revisions from other branch.
805
>>> from bzrlib.commit import commit
806
>>> bzrlib.trace.silent = True
807
>>> br1 = ScratchBranch(files=['foo', 'bar'])
810
>>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False)
811
>>> br2 = ScratchBranch()
812
>>> br2.update_revisions(br1)
816
>>> br2.revision_history()
818
>>> br2.update_revisions(br1)
822
>>> br1.text_store.total_size() == br2.text_store.total_size()
825
from bzrlib.progress import ProgressBar
810
from bzrlib.fetch import greedy_fetch
812
pb = bzrlib.ui.ui_factory.progress_bar()
829
813
pb.update('comparing histories')
830
815
revision_ids = self.missing_revisions(other, stop_revision)
817
if len(revision_ids) > 0:
818
count = greedy_fetch(self, other, revision_ids[-1], pb)[0]
821
self.append_revision(*revision_ids)
822
## note("Added %d revisions." % count)
825
def install_revisions(self, other, revision_ids, pb):
832
826
if hasattr(other.revision_store, "prefetch"):
833
827
other.revision_store.prefetch(revision_ids)
834
828
if hasattr(other.inventory_store, "prefetch"):
835
829
inventory_ids = [other.get_revision(r).inventory_id
836
830
for r in revision_ids]
837
831
other.inventory_store.prefetch(inventory_ids)
834
pb = bzrlib.ui.ui_factory.progress_bar()
840
837
needed_texts = set()
842
for rev_id in revision_ids:
844
pb.update('fetching revision', i, len(revision_ids))
845
rev = other.get_revision(rev_id)
841
for i, rev_id in enumerate(revision_ids):
842
pb.update('fetching revision', i+1, len(revision_ids))
844
rev = other.get_revision(rev_id)
845
except bzrlib.errors.NoSuchRevision:
846
849
revisions.append(rev)
847
850
inv = other.get_inventory(str(rev.inventory_id))
848
851
for key, entry in inv.iter_entries():
856
count = self.text_store.copy_multi(other.text_store, needed_texts)
857
print "Added %d texts." % count
859
count, cp_fail = self.text_store.copy_multi(other.text_store,
861
#print "Added %d texts." % count
858
862
inventory_ids = [ f.inventory_id for f in revisions ]
859
count = self.inventory_store.copy_multi(other.inventory_store,
861
print "Added %d inventories." % count
863
count, cp_fail = self.inventory_store.copy_multi(other.inventory_store,
865
#print "Added %d inventories." % count
862
866
revision_ids = [ f.revision_id for f in revisions]
863
count = self.revision_store.copy_multi(other.revision_store,
865
for revision_id in revision_ids:
866
self.append_revision(revision_id)
867
print "Added %d revisions." % count
868
count, cp_fail = self.revision_store.copy_multi(other.revision_store,
871
assert len(cp_fail) == 0
872
return count, failures
870
875
def commit(self, *args, **kw):
871
from bzrlib.commit import commit
872
commit(self, *args, **kw)
876
from bzrlib.commit import Commit
877
Commit().commit(self, *args, **kw)
875
880
def lookup_revision(self, revision):
876
881
"""Return the revision identifier for a given revision information."""
877
revno, info = self.get_revision_info(revision)
882
revno, info = self._get_revision_info(revision)
886
def revision_id_to_revno(self, revision_id):
887
"""Given a revision id, return its revno"""
888
history = self.revision_history()
890
return history.index(revision_id) + 1
892
raise bzrlib.errors.NoSuchRevision(self, revision_id)
880
895
def get_revision_info(self, revision):
881
896
"""Return (revno, revision id) for revision identifier.
885
900
revision can also be a string, in which case it is parsed for something like
886
901
'date:' or 'revid:' etc.
903
revno, rev_id = self._get_revision_info(revision)
905
raise bzrlib.errors.NoSuchRevision(self, revision)
908
def get_rev_id(self, revno, history=None):
909
"""Find the revision id of the specified revno."""
913
history = self.revision_history()
914
elif revno <= 0 or revno > len(history):
915
raise bzrlib.errors.NoSuchRevision(self, revno)
916
return history[revno - 1]
918
def _get_revision_info(self, revision):
919
"""Return (revno, revision id) for revision specifier.
921
revision can be an integer, in which case it is assumed to be revno
922
(though this will translate negative values into positive ones)
923
revision can also be a string, in which case it is parsed for something
924
like 'date:' or 'revid:' etc.
926
A revid is always returned. If it is None, the specifier referred to
927
the null revision. If the revid does not occur in the revision
928
history, revno will be None.
888
931
if revision is None:
895
938
revs = self.revision_history()
896
939
if isinstance(revision, int):
899
# Mabye we should do this first, but we don't need it if revision == 0
901
941
revno = len(revs) + revision + 1
944
rev_id = self.get_rev_id(revno, revs)
904
945
elif isinstance(revision, basestring):
905
946
for prefix, func in Branch.REVISION_NAMESPACES.iteritems():
906
947
if revision.startswith(prefix):
907
revno = func(self, revs, revision)
948
result = func(self, revs, revision)
950
revno, rev_id = result
953
rev_id = self.get_rev_id(revno, revs)
910
raise BzrError('No namespace registered for string: %r' % revision)
956
raise BzrError('No namespace registered for string: %r' %
959
raise TypeError('Unhandled revision type %s' % revision)
912
if revno is None or revno <= 0 or revno > len(revs):
913
raise BzrError("no such revision %s" % revision)
914
return revno, revs[revno-1]
963
raise bzrlib.errors.NoSuchRevision(self, revision)
916
966
def _namespace_revno(self, revs, revision):
917
967
"""Lookup a revision by revision number"""
918
968
assert revision.startswith('revno:')
920
return int(revision[6:])
970
return (int(revision[6:]),)
921
971
except ValueError:
923
973
REVISION_NAMESPACES['revno:'] = _namespace_revno
925
975
def _namespace_revid(self, revs, revision):
926
976
assert revision.startswith('revid:')
977
rev_id = revision[len('revid:'):]
928
return revs.index(revision[6:]) + 1
979
return revs.index(rev_id) + 1, rev_id
929
980
except ValueError:
931
982
REVISION_NAMESPACES['revid:'] = _namespace_revid
933
984
def _namespace_last(self, revs, revision):
936
987
offset = int(revision[5:])
937
988
except ValueError:
941
992
raise BzrError('You must supply a positive value for --revision last:XXX')
942
return len(revs) - offset + 1
993
return (len(revs) - offset + 1,)
943
994
REVISION_NAMESPACES['last:'] = _namespace_last
945
996
def _namespace_tag(self, revs, revision):
1020
1071
# TODO: Handle timezone.
1021
1072
dt = datetime.datetime.fromtimestamp(r.timestamp)
1022
1073
if first >= dt and (last is None or dt >= last):
1025
1076
for i in range(len(revs)):
1026
1077
r = self.get_revision(revs[i])
1027
1078
# TODO: Handle timezone.
1028
1079
dt = datetime.datetime.fromtimestamp(r.timestamp)
1029
1080
if first <= dt and (last is None or dt <= last):
1031
1082
REVISION_NAMESPACES['date:'] = _namespace_date
1033
1084
def revision_tree(self, revision_id):
1165
1213
for f in from_paths:
1166
1214
name_tail = splitpath(f)[-1]
1167
1215
dest_path = appendpath(to_name, name_tail)
1168
print "%s => %s" % (f, dest_path)
1216
result.append((f, dest_path))
1169
1217
inv.rename(inv.path2id(f), to_dir_id, name_tail)
1171
1219
os.rename(self.abspath(f), self.abspath(dest_path))
1317
def get_parent(self):
1318
"""Return the parent location of the branch.
1320
This is the default location for push/pull/missing. The usual
1321
pattern is that the user can override it by specifying a
1325
_locs = ['parent', 'pull', 'x-pull']
1328
return self.controlfile(l, 'r').read().strip('\n')
1330
if e.errno != errno.ENOENT:
1335
def set_parent(self, url):
1336
# TODO: Maybe delete old location files?
1337
from bzrlib.atomicfile import AtomicFile
1340
f = AtomicFile(self.controlfilename('parent'))
1349
def check_revno(self, revno):
1351
Check whether a revno corresponds to any revision.
1352
Zero (the NULL revision) is considered valid.
1355
self.check_real_revno(revno)
1357
def check_real_revno(self, revno):
1359
Check whether a revno corresponds to a real revision.
1360
Zero (the NULL revision) is considered invalid
1362
if revno < 1 or revno > self.revno():
1363
raise InvalidRevisionNumber(revno)
1268
1368
class ScratchBranch(Branch):
1269
1369
"""Special test class: a branch that cleans up after itself.
1386
1488
"""Return a new tree-root file id."""
1387
1489
return gen_file_id('TREE_ROOT')
1492
def pull_loc(branch):
1493
# TODO: Should perhaps just make attribute be 'base' in
1494
# RemoteBranch and Branch?
1495
if hasattr(branch, "baseurl"):
1496
return branch.baseurl
1501
def copy_branch(branch_from, to_location, revision=None):
1502
"""Copy branch_from into the existing directory to_location.
1505
If not None, only revisions up to this point will be copied.
1506
The head of the new branch will be that revision.
1509
The name of a local directory that exists but is empty.
1511
from bzrlib.merge import merge
1512
from bzrlib.branch import Branch
1514
assert isinstance(branch_from, Branch)
1515
assert isinstance(to_location, basestring)
1517
br_to = Branch(to_location, init=True)
1518
br_to.set_root_id(branch_from.get_root_id())
1519
if revision is None:
1520
revno = branch_from.revno()
1522
revno, rev_id = branch_from.get_revision_info(revision)
1523
br_to.update_revisions(branch_from, stop_revision=revno)
1524
merge((to_location, -1), (to_location, 0), this_dir=to_location,
1525
check_clean=False, ignore_zero=True)
1527
from_location = pull_loc(branch_from)
1528
br_to.set_parent(pull_loc(branch_from))