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
sha_file, appendpath, file_kind
24
from bzrlib.errors import BzrError
23
from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, \
24
rename, splitpath, sha_file, appendpath, file_kind
26
from bzrlib.errors import BzrError, InvalidRevisionNumber, InvalidRevisionId, \
27
DivergedBranches, NotBranchError
28
from bzrlib.textui import show_status
29
from bzrlib.revision import Revision
30
from bzrlib.delta import compare_trees
31
from bzrlib.tree import EmptyTree, RevisionTree
26
37
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
27
38
## TODO: Maybe include checks for common corruption of newlines, etc?
31
def find_branch(f, **args):
32
if f and (f.startswith('http://') or f.startswith('https://')):
34
return remotebranch.RemoteBranch(f, **args)
36
return Branch(f, **args)
39
def find_cached_branch(f, cache_root, **args):
40
from remotebranch import RemoteBranch
41
br = find_branch(f, **args)
42
def cacheify(br, store_name):
43
from meta_store import CachedStore
44
cache_path = os.path.join(cache_root, store_name)
46
new_store = CachedStore(getattr(br, store_name), cache_path)
47
setattr(br, store_name, new_store)
49
if isinstance(br, RemoteBranch):
50
cacheify(br, 'inventory_store')
51
cacheify(br, 'text_store')
52
cacheify(br, 'revision_store')
41
# TODO: Some operations like log might retrieve the same revisions
42
# repeatedly to calculate deltas. We could perhaps have a weakref
43
# cache in memory to make this faster.
45
def find_branch(*ignored, **ignored_too):
46
# XXX: leave this here for about one release, then remove it
47
raise NotImplementedError('find_branch() is not supported anymore, '
48
'please use one of the new branch constructors')
56
50
def _relpath(base, path):
57
51
"""Return path relative to base, or raise exception.
108
99
head, tail = os.path.split(f)
110
101
# reached the root, whatever that may be
111
raise BzrError('%r is not in a branch' % orig_f)
102
raise NotBranchError('%s is not in a branch' % orig_f)
114
class DivergedBranches(Exception):
115
def __init__(self, branch1, branch2):
116
self.branch1 = branch1
117
self.branch2 = branch2
118
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
108
######################################################################
133
112
"""Branch holding a history of revisions.
136
Base directory of the branch.
115
Base directory/url of the branch.
119
def __init__(self, *ignored, **ignored_too):
120
raise NotImplementedError('The Branch class is abstract')
124
"""Open an existing branch, rooted at 'base' (url)"""
125
if base and (base.startswith('http://') or base.startswith('https://')):
126
from bzrlib.remotebranch import RemoteBranch
127
return RemoteBranch(base, find_root=False)
129
return LocalBranch(base, find_root=False)
132
def open_containing(url):
133
"""Open an existing branch which contains url.
135
This probes for a branch at url, and searches upwards from there.
137
if url and (url.startswith('http://') or url.startswith('https://')):
138
from bzrlib.remotebranch import RemoteBranch
139
return RemoteBranch(url)
141
return LocalBranch(url)
144
def initialize(base):
145
"""Create a new branch, rooted at 'base' (url)"""
146
if base and (base.startswith('http://') or base.startswith('https://')):
147
from bzrlib.remotebranch import RemoteBranch
148
return RemoteBranch(base, init=True)
150
return LocalBranch(base, init=True)
152
def setup_caching(self, cache_root):
153
"""Subclasses that care about caching should override this, and set
154
up cached stores located under cache_root.
158
class LocalBranch(Branch):
159
"""A branch stored in the actual filesystem.
161
Note that it's "local" in the context of the filesystem; it doesn't
162
really matter if it's on an nfs/smb/afs/coda/... share, as long as
163
it's writable, and can be accessed via the normal filesystem API.
139
166
None, or 'r' or 'w'
146
173
Lock object from bzrlib.lock.
175
# We actually expect this class to be somewhat short-lived; part of its
176
# purpose is to try to isolate what bits of the branch logic are tied to
177
# filesystem access, so that in a later step, we can extricate them to
178
# a separarte ("storage") class.
149
179
_lock_mode = None
150
180
_lock_count = None
153
183
def __init__(self, base, init=False, find_root=True):
154
184
"""Create new branch object at a particular location.
156
base -- Base directory for the branch.
186
base -- Base directory for the branch. May be a file:// url.
158
188
init -- If True, create new control files in a previously
159
189
unversioned directory. If False, the branch must already
242
268
self._lock = None
243
269
self._lock_mode = self._lock_count = None
246
271
def abspath(self, name):
247
272
"""Return absolute filename for something in the branch"""
248
273
return os.path.join(self.base, name)
251
275
def relpath(self, path):
252
276
"""Return path relative to this branch of something inside it.
254
278
Raises an error if path is not in this branch."""
255
279
return _relpath(self.base, path)
258
281
def controlfilename(self, file_or_path):
259
282
"""Return location relative to branch."""
260
283
if isinstance(file_or_path, basestring):
322
346
# on Windows from Linux and so on. I think it might be better
323
347
# to always make all internal files in unix format.
324
348
fmt = self.controlfile('branch-format', 'r').read()
325
fmt.replace('\r\n', '')
349
fmt = fmt.replace('\r\n', '\n')
326
350
if fmt != BZR_BRANCH_FORMAT:
327
351
raise BzrError('sorry, branch format %r not supported' % fmt,
328
352
['use a different bzr version',
329
353
'or remove the .bzr directory and "bzr init" again'])
355
def get_root_id(self):
356
"""Return the id of this branches root"""
357
inv = self.read_working_inventory()
358
return inv.root.file_id
360
def set_root_id(self, file_id):
361
inv = self.read_working_inventory()
362
orig_root_id = inv.root.file_id
363
del inv._byid[inv.root.file_id]
364
inv.root.file_id = file_id
365
inv._byid[inv.root.file_id] = inv.root
368
if entry.parent_id in (None, orig_root_id):
369
entry.parent_id = inv.root.file_id
370
self._write_inventory(inv)
333
372
def read_working_inventory(self):
334
373
"""Read the working inventory."""
335
374
from bzrlib.inventory import Inventory
336
from bzrlib.xml import unpack_xml
337
from time import time
341
377
# ElementTree does its own conversion from UTF-8, so open in
343
inv = unpack_xml(Inventory,
344
self.controlfile('inventory', 'rb'))
345
mutter("loaded inventory of %d items in %f"
346
% (len(inv), time() - before))
379
f = self.controlfile('inventory', 'rb')
380
return bzrlib.xml.serializer_v4.read_inventory(f)
376
408
"""Inventory for the working copy.""")
379
def add(self, files, verbose=False, ids=None):
411
def add(self, files, ids=None):
380
412
"""Make files versioned.
382
Note that the command line normally calls smart_add instead.
414
Note that the command line normally calls smart_add instead,
415
which can automatically recurse.
384
417
This puts the files in the Added state, so that they will be
385
418
recorded by the next commit.
395
428
TODO: Perhaps have an option to add the ids even if the files do
398
TODO: Perhaps return the ids of the files? But then again it
399
is easy to retrieve them if they're needed.
401
TODO: Adding a directory should optionally recurse down and
402
add all non-ignored children. Perhaps do that in a
431
TODO: Perhaps yield the ids and paths as they're added.
405
from bzrlib.textui import show_status
406
433
# TODO: Re-adding a file that is removed in the working copy
407
434
# should probably put it back with the previous ID.
408
435
if isinstance(files, basestring):
434
461
kind = file_kind(fullpath)
436
463
# maybe something better?
437
raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
464
raise BzrError('cannot add: not a regular file, symlink or directory: %s' % quotefn(f))
439
if kind != 'file' and kind != 'directory':
440
raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
466
if kind not in ('file', 'directory', 'symlink'):
467
raise BzrError('cannot add: not a regular file, symlink or directory: %s' % quotefn(f))
442
469
if file_id is None:
443
470
file_id = gen_file_id(f)
444
471
inv.add_path(f, kind=kind, file_id=file_id)
447
print 'added', quotefn(f)
449
473
mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
451
475
self._write_inventory(inv)
544
565
return self.working_tree().unknowns()
547
def append_revision(self, revision_id):
568
def append_revision(self, *revision_ids):
548
569
from bzrlib.atomicfile import AtomicFile
550
mutter("add {%s} to revision-history" % revision_id)
551
rev_history = self.revision_history() + [revision_id]
571
for revision_id in revision_ids:
572
mutter("add {%s} to revision-history" % revision_id)
574
rev_history = self.revision_history()
575
rev_history.extend(revision_ids)
553
577
f = AtomicFile(self.controlfilename('revision-history'))
585
def get_revision_xml_file(self, revision_id):
586
"""Return XML file object for revision object."""
587
if not revision_id or not isinstance(revision_id, basestring):
588
raise InvalidRevisionId(revision_id)
593
return self.revision_store[revision_id]
594
except (IndexError, KeyError):
595
raise bzrlib.errors.NoSuchRevision(self, revision_id)
600
get_revision_xml = get_revision_xml_file
603
get_revision_xml = get_revision_xml_file
562
606
def get_revision(self, revision_id):
563
607
"""Return the Revision object for a named revision"""
564
from bzrlib.revision import Revision
565
from bzrlib.xml import unpack_xml
608
xml_file = self.get_revision_xml_file(revision_id)
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])
611
r = bzrlib.xml.serializer_v4.read_revision(xml_file)
612
except SyntaxError, e:
613
raise bzrlib.errors.BzrError('failed to unpack revision_xml',
575
617
assert r.revision_id == revision_id
620
def get_revision_delta(self, revno):
621
"""Return the delta for one revision.
623
The delta is relative to its mainline predecessor, or the
624
empty tree for revision 1.
626
assert isinstance(revno, int)
627
rh = self.revision_history()
628
if not (1 <= revno <= len(rh)):
629
raise InvalidRevisionNumber(revno)
631
# revno is 1-based; list is 0-based
633
new_tree = self.revision_tree(rh[revno-1])
635
old_tree = EmptyTree()
637
old_tree = self.revision_tree(rh[revno-2])
639
return compare_trees(old_tree, new_tree)
579
641
def get_revision_sha1(self, revision_id):
580
642
"""Hash the stored value of a revision, and return it."""
594
655
parameter which can be either an integer revno or a
596
657
from bzrlib.inventory import Inventory
597
from bzrlib.xml import unpack_xml
599
return unpack_xml(Inventory, self.inventory_store[inventory_id])
658
f = self.get_inventory_xml_file(inventory_id)
659
return bzrlib.xml.serializer_v4.read_inventory(f)
661
def get_inventory_xml(self, inventory_id):
662
"""Get inventory XML as a file object."""
663
return self.inventory_store[inventory_id]
665
get_inventory_xml_file = get_inventory_xml
602
667
def get_inventory_sha1(self, inventory_id):
603
668
"""Return the sha1 hash of the inventory entry
605
return sha_file(self.inventory_store[inventory_id])
670
return sha_file(self.get_inventory_xml(inventory_id))
608
672
def get_revision_inventory(self, revision_id):
609
673
"""Return inventory of a past revision."""
633
695
def common_ancestor(self, other, self_revno=None, other_revno=None):
697
>>> from bzrlib.commit import commit
636
698
>>> sb = ScratchBranch(files=['foo', 'foo~'])
637
699
>>> sb.common_ancestor(sb) == (None, None)
639
>>> commit.commit(sb, "Committing first revision", verbose=False)
701
>>> commit(sb, "Committing first revision", verbose=False)
640
702
>>> sb.common_ancestor(sb)[0]
642
704
>>> clone = sb.clone()
643
>>> commit.commit(sb, "Committing second revision", verbose=False)
705
>>> commit(sb, "Committing second revision", verbose=False)
644
706
>>> sb.common_ancestor(sb)[0]
646
708
>>> sb.common_ancestor(clone)[0]
648
>>> commit.commit(clone, "Committing divergent second revision",
710
>>> commit(clone, "Committing divergent second revision",
649
711
... verbose=False)
650
712
>>> sb.common_ancestor(clone)[0]
674
736
return r+1, my_history[r]
675
737
return None, None
677
def enum_history(self, direction):
678
"""Return (revno, revision_id) for history of branch.
681
'forward' is from earliest to latest
682
'reverse' is from latest to earliest
684
rh = self.revision_history()
685
if direction == 'forward':
690
elif direction == 'reverse':
696
raise ValueError('invalid history direction', direction)
700
741
"""Return current revision number for this branch.
754
795
if stop_revision is None:
755
796
stop_revision = other_len
756
797
elif stop_revision > other_len:
757
raise NoSuchRevision(self, stop_revision)
798
raise bzrlib.errors.NoSuchRevision(self, stop_revision)
759
800
return other_history[self_len:stop_revision]
762
803
def update_revisions(self, other, stop_revision=None):
763
804
"""Pull in all new revisions from other branch.
765
>>> from bzrlib.commit import commit
766
>>> bzrlib.trace.silent = True
767
>>> br1 = ScratchBranch(files=['foo', 'bar'])
770
>>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False)
771
>>> br2 = ScratchBranch()
772
>>> br2.update_revisions(br1)
776
>>> br2.revision_history()
778
>>> br2.update_revisions(br1)
782
>>> br1.text_store.total_size() == br2.text_store.total_size()
785
from bzrlib.progress import ProgressBar
789
from sets import Set as set
806
from bzrlib.fetch import greedy_fetch
807
from bzrlib.revision import get_intervening_revisions
809
pb = bzrlib.ui.ui_factory.progress_bar()
793
810
pb.update('comparing histories')
794
revision_ids = self.missing_revisions(other, stop_revision)
811
if stop_revision is None:
812
other_revision = other.last_patch()
814
other_revision = other.get_rev_id(stop_revision)
815
count = greedy_fetch(self, other, other_revision, pb)[0]
817
revision_ids = self.missing_revisions(other, stop_revision)
818
except DivergedBranches, e:
820
revision_ids = get_intervening_revisions(self.last_patch(),
821
other_revision, self)
822
assert self.last_patch() not in revision_ids
823
except bzrlib.errors.NotAncestor:
826
self.append_revision(*revision_ids)
829
def install_revisions(self, other, revision_ids, pb):
796
830
if hasattr(other.revision_store, "prefetch"):
797
831
other.revision_store.prefetch(revision_ids)
798
832
if hasattr(other.inventory_store, "prefetch"):
799
inventory_ids = [other.get_revision(r).inventory_id
800
for r in revision_ids]
834
for rev_id in revision_ids:
836
revision = other.get_revision(rev_id).inventory_id
837
inventory_ids.append(revision)
838
except bzrlib.errors.NoSuchRevision:
801
840
other.inventory_store.prefetch(inventory_ids)
843
pb = bzrlib.ui.ui_factory.progress_bar()
804
846
needed_texts = set()
806
for rev_id in revision_ids:
808
pb.update('fetching revision', i, len(revision_ids))
809
rev = other.get_revision(rev_id)
850
for i, rev_id in enumerate(revision_ids):
851
pb.update('fetching revision', i+1, len(revision_ids))
853
rev = other.get_revision(rev_id)
854
except bzrlib.errors.NoSuchRevision:
810
858
revisions.append(rev)
811
859
inv = other.get_inventory(str(rev.inventory_id))
812
860
for key, entry in inv.iter_entries():
820
count = self.text_store.copy_multi(other.text_store, needed_texts)
821
print "Added %d texts." % count
868
count, cp_fail = self.text_store.copy_multi(other.text_store,
870
#print "Added %d texts." % count
822
871
inventory_ids = [ f.inventory_id for f in revisions ]
823
count = self.inventory_store.copy_multi(other.inventory_store,
825
print "Added %d inventories." % count
872
count, cp_fail = self.inventory_store.copy_multi(other.inventory_store,
874
#print "Added %d inventories." % count
826
875
revision_ids = [ f.revision_id for f in revisions]
827
count = self.revision_store.copy_multi(other.revision_store,
829
for revision_id in revision_ids:
830
self.append_revision(revision_id)
831
print "Added %d revisions." % count
877
count, cp_fail = self.revision_store.copy_multi(other.revision_store,
880
assert len(cp_fail) == 0
881
return count, failures
834
884
def commit(self, *args, **kw):
835
885
from bzrlib.commit import commit
836
886
commit(self, *args, **kw)
888
def revision_id_to_revno(self, revision_id):
889
"""Given a revision id, return its revno"""
890
history = self.revision_history()
892
return history.index(revision_id) + 1
894
raise bzrlib.errors.NoSuchRevision(self, revision_id)
839
def lookup_revision(self, revno):
840
"""Return revision hash for revision number."""
896
def get_rev_id(self, revno, history=None):
897
"""Find the revision id of the specified revno."""
845
# list is 0-based; revisions are 1-based
846
return self.revision_history()[revno-1]
848
raise BzrError("no such revision %s" % revno)
901
history = self.revision_history()
902
elif revno <= 0 or revno > len(history):
903
raise bzrlib.errors.NoSuchRevision(self, revno)
904
return history[revno - 1]
851
906
def revision_tree(self, revision_id):
852
907
"""Return Tree for a revision on this branch.
854
909
`revision_id` may be None for the null revision, in which case
855
910
an `EmptyTree` is returned."""
856
from bzrlib.tree import EmptyTree, RevisionTree
857
911
# TODO: refactor this to use an existing revision object
858
912
# so we don't need to read it in twice.
859
913
if revision_id == None:
919
972
inv.rename(file_id, to_dir_id, to_tail)
921
print "%s => %s" % (from_rel, to_rel)
923
974
from_abs = self.abspath(from_rel)
924
975
to_abs = self.abspath(to_rel)
926
os.rename(from_abs, to_abs)
977
rename(from_abs, to_abs)
927
978
except OSError, e:
928
979
raise BzrError("failed to rename %r to %r: %s"
929
980
% (from_abs, to_abs, e[1]),
985
1040
for f in from_paths:
986
1041
name_tail = splitpath(f)[-1]
987
1042
dest_path = appendpath(to_name, name_tail)
988
print "%s => %s" % (f, dest_path)
1043
result.append((f, dest_path))
989
1044
inv.rename(inv.path2id(f), to_dir_id, name_tail)
991
os.rename(self.abspath(f), self.abspath(dest_path))
1046
rename(self.abspath(f), self.abspath(dest_path))
992
1047
except OSError, e:
993
1048
raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
994
1049
["rename rolled back"])
1088
class ScratchBranch(Branch):
1144
def get_parent(self):
1145
"""Return the parent location of the branch.
1147
This is the default location for push/pull/missing. The usual
1148
pattern is that the user can override it by specifying a
1152
_locs = ['parent', 'pull', 'x-pull']
1155
return self.controlfile(l, 'r').read().strip('\n')
1157
if e.errno != errno.ENOENT:
1162
def set_parent(self, url):
1163
# TODO: Maybe delete old location files?
1164
from bzrlib.atomicfile import AtomicFile
1167
f = AtomicFile(self.controlfilename('parent'))
1176
def check_revno(self, revno):
1178
Check whether a revno corresponds to any revision.
1179
Zero (the NULL revision) is considered valid.
1182
self.check_real_revno(revno)
1184
def check_real_revno(self, revno):
1186
Check whether a revno corresponds to a real revision.
1187
Zero (the NULL revision) is considered invalid
1189
if revno < 1 or revno > self.revno():
1190
raise InvalidRevisionNumber(revno)
1196
class ScratchBranch(LocalBranch):
1089
1197
"""Special test class: a branch that cleans up after itself.
1091
1199
>>> b = ScratchBranch()
1201
1315
s = hexlify(rand_bytes(8))
1202
1316
return '-'.join((name, compact_date(time()), s))
1320
"""Return a new tree-root file id."""
1321
return gen_file_id('TREE_ROOT')
1324
def copy_branch(branch_from, to_location, revno=None):
1325
"""Copy branch_from into the existing directory to_location.
1328
If not None, only revisions up to this point will be copied.
1329
The head of the new branch will be that revision.
1332
The name of a local directory that exists but is empty.
1334
from bzrlib.merge import merge
1336
assert isinstance(branch_from, Branch)
1337
assert isinstance(to_location, basestring)
1339
br_to = Branch.initialize(to_location)
1340
br_to.set_root_id(branch_from.get_root_id())
1342
revno = branch_from.revno()
1343
br_to.update_revisions(branch_from, stop_revision=revno)
1344
merge((to_location, -1), (to_location, 0), this_dir=to_location,
1345
check_clean=False, ignore_zero=True)
1346
br_to.set_parent(branch_from.base)