15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21
22
from bzrlib.trace import mutter, note
22
from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, splitpath, \
23
from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, \
23
25
sha_file, appendpath, file_kind
24
from bzrlib.errors import BzrError
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
26
38
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
27
39
## 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
31
50
def find_branch(f, **args):
32
51
if f and (f.startswith('http://') or f.startswith('https://')):
108
128
head, tail = os.path.split(f)
110
130
# reached the root, whatever that may be
111
raise BzrError('%r is not in a branch' % orig_f)
131
raise bzrlib.errors.NotBranchError('%s is not in a branch' % orig_f)
136
# XXX: move into bzrlib.errors; subclass BzrError
114
137
class DivergedBranches(Exception):
115
138
def __init__(self, branch1, branch2):
116
139
self.branch1 = branch1
118
141
Exception.__init__(self, "These branches have diverged.")
121
class NoSuchRevision(BzrError):
122
def __init__(self, branch, revision):
124
self.revision = revision
125
msg = "Branch %s has no revision %d" % (branch, revision)
126
BzrError.__init__(self, msg)
129
144
######################################################################
247
258
self._lock = None
248
259
self._lock_mode = self._lock_count = None
251
261
def abspath(self, name):
252
262
"""Return absolute filename for something in the branch"""
253
263
return os.path.join(self.base, name)
256
265
def relpath(self, path):
257
266
"""Return path relative to this branch of something inside it.
259
268
Raises an error if path is not in this branch."""
260
269
return _relpath(self.base, path)
263
271
def controlfilename(self, file_or_path):
264
272
"""Return location relative to branch."""
265
273
if isinstance(file_or_path, basestring):
312
318
self.controlfile(f, 'w').write('')
313
319
mutter('created control directory in ' + self.base)
321
# if we want per-tree root ids then this is the place to set
322
# them; they're not needed for now and so ommitted for
315
324
pack_xml(Inventory(), self.controlfile('inventory','w'))
318
326
def _check_format(self):
319
327
"""Check this branch format is supported.
333
341
['use a different bzr version',
334
342
'or remove the .bzr directory and "bzr init" again'])
344
def get_root_id(self):
345
"""Return the id of this branches root"""
346
inv = self.read_working_inventory()
347
return inv.root.file_id
349
def set_root_id(self, file_id):
350
inv = self.read_working_inventory()
351
orig_root_id = inv.root.file_id
352
del inv._byid[inv.root.file_id]
353
inv.root.file_id = file_id
354
inv._byid[inv.root.file_id] = inv.root
357
if entry.parent_id in (None, orig_root_id):
358
entry.parent_id = inv.root.file_id
359
self._write_inventory(inv)
338
361
def read_working_inventory(self):
339
362
"""Read the working inventory."""
346
369
# ElementTree does its own conversion from UTF-8, so open in
348
371
inv = unpack_xml(Inventory,
349
self.controlfile('inventory', 'rb'))
372
self.controlfile('inventory', 'rb'))
350
373
mutter("loaded inventory of %d items in %f"
351
374
% (len(inv), time() - before))
381
404
"""Inventory for the working copy.""")
384
def add(self, files, verbose=False, ids=None):
407
def add(self, files, ids=None):
385
408
"""Make files versioned.
387
Note that the command line normally calls smart_add instead.
410
Note that the command line normally calls smart_add instead,
411
which can automatically recurse.
389
413
This puts the files in the Added state, so that they will be
390
414
recorded by the next commit.
400
424
TODO: Perhaps have an option to add the ids even if the files do
403
TODO: Perhaps return the ids of the files? But then again it
404
is easy to retrieve them if they're needed.
406
TODO: Adding a directory should optionally recurse down and
407
add all non-ignored children. Perhaps do that in a
427
TODO: Perhaps yield the ids and paths as they're added.
410
from bzrlib.textui import show_status
411
429
# TODO: Re-adding a file that is removed in the working copy
412
430
# should probably put it back with the previous ID.
413
431
if isinstance(files, basestring):
521
535
# FIXME: this doesn't need to be a branch method
522
536
def set_inventory(self, new_inventory_list):
523
537
from bzrlib.inventory import Inventory, InventoryEntry
538
inv = Inventory(self.get_root_id())
525
539
for path, file_id, parent, kind in new_inventory_list:
526
540
name = os.path.basename(path)
549
563
return self.working_tree().unknowns()
552
def append_revision(self, revision_id):
566
def append_revision(self, *revision_ids):
553
567
from bzrlib.atomicfile import AtomicFile
555
mutter("add {%s} to revision-history" % revision_id)
556
rev_history = self.revision_history() + [revision_id]
569
for revision_id in revision_ids:
570
mutter("add {%s} to revision-history" % revision_id)
572
rev_history = self.revision_history()
573
rev_history.extend(revision_ids)
558
575
f = AtomicFile(self.controlfilename('revision-history'))
584
def get_revision_xml(self, revision_id):
585
"""Return XML file object for revision object."""
586
if not revision_id or not isinstance(revision_id, basestring):
587
raise InvalidRevisionId(revision_id)
592
return self.revision_store[revision_id]
594
raise bzrlib.errors.NoSuchRevision(self, revision_id)
567
599
def get_revision(self, revision_id):
568
600
"""Return the Revision object for a named revision"""
569
from bzrlib.revision import Revision
570
from bzrlib.xml import unpack_xml
601
xml_file = self.get_revision_xml(revision_id)
574
if not revision_id or not isinstance(revision_id, basestring):
575
raise ValueError('invalid revision-id: %r' % revision_id)
576
r = unpack_xml(Revision, self.revision_store[revision_id])
604
r = unpack_xml(Revision, xml_file)
605
except SyntaxError, e:
606
raise bzrlib.errors.BzrError('failed to unpack revision_xml',
580
610
assert r.revision_id == revision_id
614
def get_revision_delta(self, revno):
615
"""Return the delta for one revision.
617
The delta is relative to its mainline predecessor, or the
618
empty tree for revision 1.
620
assert isinstance(revno, int)
621
rh = self.revision_history()
622
if not (1 <= revno <= len(rh)):
623
raise InvalidRevisionNumber(revno)
625
# revno is 1-based; list is 0-based
627
new_tree = self.revision_tree(rh[revno-1])
629
old_tree = EmptyTree()
631
old_tree = self.revision_tree(rh[revno-2])
633
return compare_trees(old_tree, new_tree)
584
637
def get_revision_sha1(self, revision_id):
589
642
# the revision, (add signatures/remove signatures) and still
590
643
# have all hash pointers stay consistent.
591
644
# But for now, just hash the contents.
592
return sha_file(self.revision_store[revision_id])
645
return bzrlib.osutils.sha_file(self.get_revision_xml(revision_id))
595
648
def get_inventory(self, inventory_id):
601
654
from bzrlib.inventory import Inventory
602
655
from bzrlib.xml import unpack_xml
604
return unpack_xml(Inventory, self.inventory_store[inventory_id])
657
return unpack_xml(Inventory, self.get_inventory_xml(inventory_id))
660
def get_inventory_xml(self, inventory_id):
661
"""Get inventory XML as a file object."""
662
return self.inventory_store[inventory_id]
607
665
def get_inventory_sha1(self, inventory_id):
608
666
"""Return the sha1 hash of the inventory entry
610
return sha_file(self.inventory_store[inventory_id])
668
return sha_file(self.get_inventory_xml(inventory_id))
613
671
def get_revision_inventory(self, revision_id):
679
737
return r+1, my_history[r]
680
738
return None, None
682
def enum_history(self, direction):
683
"""Return (revno, revision_id) for history of branch.
686
'forward' is from earliest to latest
687
'reverse' is from latest to earliest
689
rh = self.revision_history()
690
if direction == 'forward':
695
elif direction == 'reverse':
701
raise ValueError('invalid history direction', direction)
705
742
"""Return current revision number for this branch.
723
def missing_revisions(self, other, stop_revision=None):
760
def missing_revisions(self, other, stop_revision=None, diverged_ok=False):
725
762
If self and other have not diverged, return a list of the revisions
726
763
present in other, but missing from self.
759
796
if stop_revision is None:
760
797
stop_revision = other_len
761
798
elif stop_revision > other_len:
762
raise NoSuchRevision(self, stop_revision)
799
raise bzrlib.errors.NoSuchRevision(self, stop_revision)
764
801
return other_history[self_len:stop_revision]
767
804
def update_revisions(self, other, stop_revision=None):
768
805
"""Pull in all new revisions from other branch.
770
>>> from bzrlib.commit import commit
771
>>> bzrlib.trace.silent = True
772
>>> br1 = ScratchBranch(files=['foo', 'bar'])
775
>>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False)
776
>>> br2 = ScratchBranch()
777
>>> br2.update_revisions(br1)
781
>>> br2.revision_history()
783
>>> br2.update_revisions(br1)
787
>>> br1.text_store.total_size() == br2.text_store.total_size()
790
from bzrlib.progress import ProgressBar
794
from sets import Set as set
807
from bzrlib.fetch import greedy_fetch
809
pb = bzrlib.ui.ui_factory.progress_bar()
798
810
pb.update('comparing histories')
799
812
revision_ids = self.missing_revisions(other, stop_revision)
814
if len(revision_ids) > 0:
815
count = greedy_fetch(self, other, revision_ids[-1], pb)[0]
818
self.append_revision(*revision_ids)
819
## note("Added %d revisions." % count)
822
def install_revisions(self, other, revision_ids, pb):
801
823
if hasattr(other.revision_store, "prefetch"):
802
824
other.revision_store.prefetch(revision_ids)
803
825
if hasattr(other.inventory_store, "prefetch"):
804
826
inventory_ids = [other.get_revision(r).inventory_id
805
827
for r in revision_ids]
806
828
other.inventory_store.prefetch(inventory_ids)
831
pb = bzrlib.ui.ui_factory.progress_bar()
809
834
needed_texts = set()
811
for rev_id in revision_ids:
813
pb.update('fetching revision', i, len(revision_ids))
814
rev = other.get_revision(rev_id)
838
for i, rev_id in enumerate(revision_ids):
839
pb.update('fetching revision', i+1, len(revision_ids))
841
rev = other.get_revision(rev_id)
842
except bzrlib.errors.NoSuchRevision:
815
846
revisions.append(rev)
816
847
inv = other.get_inventory(str(rev.inventory_id))
817
848
for key, entry in inv.iter_entries():
825
count = self.text_store.copy_multi(other.text_store, needed_texts)
826
print "Added %d texts." % count
856
count, cp_fail = self.text_store.copy_multi(other.text_store,
858
#print "Added %d texts." % count
827
859
inventory_ids = [ f.inventory_id for f in revisions ]
828
count = self.inventory_store.copy_multi(other.inventory_store,
830
print "Added %d inventories." % count
860
count, cp_fail = self.inventory_store.copy_multi(other.inventory_store,
862
#print "Added %d inventories." % count
831
863
revision_ids = [ f.revision_id for f in revisions]
832
count = self.revision_store.copy_multi(other.revision_store,
834
for revision_id in revision_ids:
835
self.append_revision(revision_id)
836
print "Added %d revisions." % count
865
count, cp_fail = self.revision_store.copy_multi(other.revision_store,
868
assert len(cp_fail) == 0
869
return count, failures
839
872
def commit(self, *args, **kw):
840
873
from bzrlib.commit import commit
841
874
commit(self, *args, **kw)
846
879
revno, info = self.get_revision_info(revision)
883
def revision_id_to_revno(self, revision_id):
884
"""Given a revision id, return its revno"""
885
history = self.revision_history()
887
return history.index(revision_id) + 1
889
raise bzrlib.errors.NoSuchRevision(self, revision_id)
849
892
def get_revision_info(self, revision):
850
893
"""Return (revno, revision id) for revision identifier.
1135
1179
for f in from_paths:
1136
1180
name_tail = splitpath(f)[-1]
1137
1181
dest_path = appendpath(to_name, name_tail)
1138
print "%s => %s" % (f, dest_path)
1182
result.append((f, dest_path))
1139
1183
inv.rename(inv.path2id(f), to_dir_id, name_tail)
1141
1185
os.rename(self.abspath(f), self.abspath(dest_path))
1283
def get_parent(self):
1284
"""Return the parent location of the branch.
1286
This is the default location for push/pull/missing. The usual
1287
pattern is that the user can override it by specifying a
1291
_locs = ['parent', 'pull', 'x-pull']
1294
return self.controlfile(l, 'r').read().strip('\n')
1296
if e.errno != errno.ENOENT:
1301
def set_parent(self, url):
1302
# TODO: Maybe delete old location files?
1303
from bzrlib.atomicfile import AtomicFile
1306
f = AtomicFile(self.controlfilename('parent'))
1238
1318
class ScratchBranch(Branch):
1239
1319
"""Special test class: a branch that cleans up after itself.
1351
1433
s = hexlify(rand_bytes(8))
1352
1434
return '-'.join((name, compact_date(time()), s))
1438
"""Return a new tree-root file id."""
1439
return gen_file_id('TREE_ROOT')
1442
def pull_loc(branch):
1443
# TODO: Should perhaps just make attribute be 'base' in
1444
# RemoteBranch and Branch?
1445
if hasattr(branch, "baseurl"):
1446
return branch.baseurl
1451
def copy_branch(branch_from, to_location, revision=None):
1452
"""Copy branch_from into the existing directory to_location.
1455
If not None, only revisions up to this point will be copied.
1456
The head of the new branch will be that revision.
1459
The name of a local directory that exists but is empty.
1461
from bzrlib.merge import merge
1462
from bzrlib.branch import Branch
1464
assert isinstance(branch_from, Branch)
1465
assert isinstance(to_location, basestring)
1467
br_to = Branch(to_location, init=True)
1468
br_to.set_root_id(branch_from.get_root_id())
1469
if revision is None:
1470
revno = branch_from.revno()
1472
revno, rev_id = branch_from.get_revision_info(revision)
1473
br_to.update_revisions(branch_from, stop_revision=revno)
1474
merge((to_location, -1), (to_location, 0), this_dir=to_location,
1475
check_clean=False, ignore_zero=True)
1477
from_location = pull_loc(branch_from)
1478
br_to.set_parent(pull_loc(branch_from))