1
# Copyright (C) 2005 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
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, \
28
DivergedBranches, NotBranchError
29
from bzrlib.textui import show_status
30
from bzrlib.revision import Revision
31
from bzrlib.delta import compare_trees
32
from bzrlib.tree import EmptyTree, RevisionTree
35
import bzrlib.transport
39
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
40
## TODO: Maybe include checks for common corruption of newlines, etc?
43
# TODO: Some operations like log might retrieve the same revisions
44
# repeatedly to calculate deltas. We could perhaps have a weakref
45
# cache in memory to make this faster.
47
def find_branch(*ignored, **ignored_too):
48
# XXX: leave this here for about one release, then remove it
49
raise NotImplementedError('find_branch() is not supported anymore, '
50
'please use one of the new branch constructors')
51
def _relpath(base, path):
52
"""Return path relative to base, or raise exception.
54
The path may be either an absolute path or a path relative to the
55
current working directory.
57
Lifted out of Branch.relpath for ease of testing.
59
os.path.commonprefix (python2.4) has a bad bug that it works just
60
on string prefixes, assuming that '/u' is a prefix of '/u2'. This
61
avoids that problem."""
62
rp = os.path.abspath(path)
66
while len(head) >= len(base):
69
head, tail = os.path.split(head)
73
raise NotBranchError("path %r is not within branch %r" % (rp, base))
78
def find_branch_root(t):
79
"""Find the branch root enclosing the transport's base.
81
t is a Transport object.
83
It is not necessary that the base of t exists.
85
Basically we keep looking up until we find the control directory or
86
run into the root. If there isn't one, raises NotBranchError.
89
if t.has(bzrlib.BZRDIR):
92
if new_t.base == t.base:
93
# reached the root, whatever that may be
94
raise NotBranchError('%s is not in a branch' % orig_f)
98
######################################################################
101
class Branch(object):
102
"""Branch holding a history of revisions.
105
Base directory/url of the branch.
109
def __init__(self, *ignored, **ignored_too):
110
raise NotImplementedError('The Branch class is abstract')
114
"""Open an existing branch, rooted at 'base' (url)"""
115
t = bzrlib.transport.transport(base)
116
return LocalBranch(t)
119
def open_containing(url):
120
"""Open an existing branch, containing url (search upwards for the root)
122
t = bzrlib.transport.transport(base)
123
found_t = find_branch_root(t)
124
return LocalBranch(t)
127
def initialize(base):
128
"""Create a new branch, rooted at 'base' (url)"""
129
t = bzrlib.transport.transport(base)
130
return LocalBranch(t, init=True)
132
def setup_caching(self, cache_root):
133
"""Subclasses that care about caching should override this, and set
134
up cached stores located under cache_root.
138
class LocalBranch(Branch):
139
"""A branch stored in the actual filesystem.
141
Note that it's "local" in the context of the filesystem; it doesn't
142
really matter if it's on an nfs/smb/afs/coda/... share, as long as
143
it's writable, and can be accessed via the normal filesystem API.
149
If _lock_mode is true, a positive count of the number of times the
153
Lock object from bzrlib.lock.
155
# We actually expect this class to be somewhat short-lived; part of its
156
# purpose is to try to isolate what bits of the branch logic are tied to
157
# filesystem access, so that in a later step, we can extricate them to
158
# a separarte ("storage") class.
163
def __init__(self, transport, init=False):
164
"""Create new branch object at a particular location.
166
transport -- A Transport object, defining how to access files.
167
(If a string, transport.transport() will be used to
168
create a Transport object)
170
init -- If True, create new control files in a previously
171
unversioned directory. If False, the branch must already
174
In the test suite, creation of new trees is tested using the
175
`ScratchBranch` class.
177
if isinstance(transport, basestring):
178
from transport import transport as get_transport
179
transport = get_transport(transport)
181
self._transport = transport
188
return '%s(%r)' % (self.__class__.__name__, self._transport.base)
195
if self._lock_mode or self._lock:
196
from bzrlib.warnings import warn
197
warn("branch %r was not explicitly unlocked" % self)
200
# TODO: It might be best to do this somewhere else,
201
# but it is nice for a Branch object to automatically
202
# cache it's information.
203
# Alternatively, we could have the Transport objects cache requests
204
# See the earlier discussion about how major objects (like Branch)
205
# should never expect their __del__ function to run.
206
if self.cache_root is not None:
207
#from warnings import warn
208
#warn("branch %r auto-cleanup of cache files" % self)
211
shutil.rmtree(self.cache_root)
214
self.cache_root = None
218
return self._transport.base
221
base = property(_get_base)
224
def lock_write(self):
225
# TODO: Upgrade locking to support using a Transport,
226
# and potentially a remote locking protocol
228
if self._lock_mode != 'w':
229
from bzrlib.errors import LockError
230
raise LockError("can't upgrade to a write lock from %r" %
232
self._lock_count += 1
234
self._lock = self._transport.lock_write(
235
self._rel_controlfilename('branch-lock'))
236
self._lock_mode = 'w'
242
assert self._lock_mode in ('r', 'w'), \
243
"invalid lock mode %r" % self._lock_mode
244
self._lock_count += 1
246
self._lock = self._transport.lock_read(
247
self._rel_controlfilename('branch-lock'))
248
self._lock_mode = 'r'
252
if not self._lock_mode:
253
from bzrlib.errors import LockError
254
raise LockError('branch %r is not locked' % (self))
256
if self._lock_count > 1:
257
self._lock_count -= 1
261
self._lock_mode = self._lock_count = None
263
def abspath(self, name):
264
"""Return absolute filename for something in the branch"""
265
return self._transport.abspath(name)
267
def relpath(self, path):
268
"""Return path relative to this branch of something inside it.
270
Raises an error if path is not in this branch."""
271
return self._transport.relpath(path)
274
def _rel_controlfilename(self, file_or_path):
275
if isinstance(file_or_path, basestring):
276
file_or_path = [file_or_path]
277
return [bzrlib.BZRDIR] + file_or_path
279
def controlfilename(self, file_or_path):
280
"""Return location relative to branch."""
281
return self._transport.abspath(self._rel_controlfilename(file_or_path))
284
def controlfile(self, file_or_path, mode='r'):
285
"""Open a control file for this branch.
287
There are two classes of file in the control directory: text
288
and binary. binary files are untranslated byte streams. Text
289
control files are stored with Unix newlines and in UTF-8, even
290
if the platform or locale defaults are different.
292
Controlfiles should almost never be opened in write mode but
293
rather should be atomically copied and replaced using atomicfile.
297
relpath = self._rel_controlfilename(file_or_path)
298
#TODO: codecs.open() buffers linewise, so it was overloaded with
299
# a much larger buffer, do we need to do the same for getreader/getwriter?
301
return self._transport.get(relpath)
303
raise BzrError("Branch.controlfile(mode='wb') is not supported, use put_controlfiles")
305
return codecs.getreader('utf-8')(self._transport.get(relpath), errors='replace')
307
raise BzrError("Branch.controlfile(mode='w') is not supported, use put_controlfiles")
309
raise BzrError("invalid controlfile mode %r" % mode)
311
def put_controlfile(self, path, f, encode=True):
312
"""Write an entry as a controlfile.
314
:param path: The path to put the file, relative to the .bzr control
316
:param f: A file-like or string object whose contents should be copied.
317
:param encode: If true, encode the contents as utf-8
319
self.put_controlfiles([(path, f)], encode=encode)
321
def put_controlfiles(self, files, encode=True):
322
"""Write several entries as controlfiles.
324
:param files: A list of [(path, file)] pairs, where the path is the directory
325
underneath the bzr control directory
326
:param encode: If true, encode the contents as utf-8
330
for path, f in files:
332
if isinstance(f, basestring):
333
f = f.encode('utf-8', 'replace')
335
f = codecs.getwriter('utf-8')(f, errors='replace')
336
path = self._rel_controlfilename(path)
337
ctrl_files.append((path, f))
338
self._transport.put_multi(ctrl_files)
340
def _make_control(self):
341
from bzrlib.inventory import Inventory
342
from cStringIO import StringIO
344
# Create an empty inventory
346
# if we want per-tree root ids then this is the place to set
347
# them; they're not needed for now and so ommitted for
349
bzrlib.xml.serializer_v4.write_inventory(Inventory(), sio)
351
dirs = [[], 'text-store', 'inventory-store', 'revision-store']
353
"This is a Bazaar-NG control directory.\n"
354
"Do not change any files in this directory.\n"),
355
('branch-format', BZR_BRANCH_FORMAT),
356
('revision-history', ''),
357
('merged-patches', ''),
358
('pending-merged-patches', ''),
361
('pending-merges', ''),
362
('inventory', sio.getvalue())
364
self._transport.mkdir_multi([self._rel_controlfilename(d) for d in dirs])
365
self.put_controlfiles(files)
366
mutter('created control directory in ' + self._transport.base)
368
def _check_format(self):
369
"""Check this branch format is supported.
371
The current tool only supports the current unstable format.
373
In the future, we might need different in-memory Branch
374
classes to support downlevel branches. But not yet.
376
# This ignores newlines so that we can open branches created
377
# on Windows from Linux and so on. I think it might be better
378
# to always make all internal files in unix format.
379
fmt = self.controlfile('branch-format', 'r').read()
380
fmt = fmt.replace('\r\n', '\n')
381
if fmt != BZR_BRANCH_FORMAT:
382
raise BzrError('sorry, branch format %r not supported' % fmt,
383
['use a different bzr version',
384
'or remove the .bzr directory and "bzr init" again'])
386
# We know that the format is the currently supported one.
387
# So create the rest of the entries.
388
from bzrlib.store.compressed_text import CompressedTextStore
390
if self._transport.should_cache():
392
self.cache_root = tempfile.mkdtemp(prefix='bzr-cache')
393
mutter('Branch %r using caching in %r' % (self, self.cache_root))
395
self.cache_root = None
398
relpath = self._rel_controlfilename(name)
399
store = CompressedTextStore(self._transport.clone(relpath))
400
if self._transport.should_cache():
401
from meta_store import CachedStore
402
cache_path = os.path.join(self.cache_root, name)
404
store = CachedStore(store, cache_path)
407
self.text_store = get_store('text-store')
408
self.revision_store = get_store('revision-store')
409
self.inventory_store = get_store('inventory-store')
411
def get_root_id(self):
412
"""Return the id of this branches root"""
413
inv = self.read_working_inventory()
414
return inv.root.file_id
416
def set_root_id(self, file_id):
417
inv = self.read_working_inventory()
418
orig_root_id = inv.root.file_id
419
del inv._byid[inv.root.file_id]
420
inv.root.file_id = file_id
421
inv._byid[inv.root.file_id] = inv.root
424
if entry.parent_id in (None, orig_root_id):
425
entry.parent_id = inv.root.file_id
426
self._write_inventory(inv)
428
def read_working_inventory(self):
429
"""Read the working inventory."""
430
from bzrlib.inventory import Inventory
433
# ElementTree does its own conversion from UTF-8, so open in
435
f = self.controlfile('inventory', 'rb')
436
return bzrlib.xml.serializer_v4.read_inventory(f)
441
def _write_inventory(self, inv):
442
"""Update the working inventory.
444
That is to say, the inventory describing changes underway, that
445
will be committed to the next revision.
447
from cStringIO import StringIO
451
bzrlib.xml.serializer_v4.write_inventory(inv, sio)
453
# Transport handles atomicity
454
self.put_controlfile('inventory', sio)
458
mutter('wrote working inventory')
461
inventory = property(read_working_inventory, _write_inventory, None,
462
"""Inventory for the working copy.""")
465
def add(self, files, ids=None):
466
"""Make files versioned.
468
Note that the command line normally calls smart_add instead,
469
which can automatically recurse.
471
This puts the files in the Added state, so that they will be
472
recorded by the next commit.
475
List of paths to add, relative to the base of the tree.
478
If set, use these instead of automatically generated ids.
479
Must be the same length as the list of files, but may
480
contain None for ids that are to be autogenerated.
482
TODO: Perhaps have an option to add the ids even if the files do
485
TODO: Perhaps yield the ids and paths as they're added.
487
# TODO: Re-adding a file that is removed in the working copy
488
# should probably put it back with the previous ID.
489
if isinstance(files, basestring):
490
assert(ids is None or isinstance(ids, basestring))
496
ids = [None] * len(files)
498
assert(len(ids) == len(files))
502
inv = self.read_working_inventory()
503
for f,file_id in zip(files, ids):
504
if is_control_file(f):
505
raise BzrError("cannot add control file %s" % quotefn(f))
510
raise BzrError("cannot add top-level %r" % f)
512
fullpath = os.path.normpath(self.abspath(f))
515
kind = file_kind(fullpath)
517
# maybe something better?
518
raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
520
if kind != 'file' and kind != 'directory':
521
raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
524
file_id = gen_file_id(f)
525
inv.add_path(f, kind=kind, file_id=file_id)
527
mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
529
self._write_inventory(inv)
534
def print_file(self, file, revno):
535
"""Print `file` to stdout."""
538
tree = self.revision_tree(self.get_rev_id(revno))
539
# use inventory as it was in that revision
540
file_id = tree.inventory.path2id(file)
542
raise BzrError("%r is not present in revision %s" % (file, revno))
543
tree.print_file(file_id)
548
def remove(self, files, verbose=False):
549
"""Mark nominated files for removal from the inventory.
551
This does not remove their text. This does not run on
553
TODO: Refuse to remove modified files unless --force is given?
555
TODO: Do something useful with directories.
557
TODO: Should this remove the text or not? Tough call; not
558
removing may be useful and the user can just use use rm, and
559
is the opposite of add. Removing it is consistent with most
560
other tools. Maybe an option.
562
## TODO: Normalize names
563
## TODO: Remove nested loops; better scalability
564
if isinstance(files, basestring):
570
tree = self.working_tree()
573
# do this before any modifications
577
raise BzrError("cannot remove unversioned file %s" % quotefn(f))
578
mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
580
# having remove it, it must be either ignored or unknown
581
if tree.is_ignored(f):
585
show_status(new_status, inv[fid].kind, quotefn(f))
588
self._write_inventory(inv)
593
# FIXME: this doesn't need to be a branch method
594
def set_inventory(self, new_inventory_list):
595
from bzrlib.inventory import Inventory, InventoryEntry
596
inv = Inventory(self.get_root_id())
597
for path, file_id, parent, kind in new_inventory_list:
598
name = os.path.basename(path)
601
inv.add(InventoryEntry(file_id, name, kind, parent))
602
self._write_inventory(inv)
606
"""Return all unknown files.
608
These are files in the working directory that are not versioned or
609
control files or ignored.
611
>>> b = ScratchBranch(files=['foo', 'foo~'])
612
>>> list(b.unknowns())
615
>>> list(b.unknowns())
618
>>> list(b.unknowns())
621
return self.working_tree().unknowns()
624
def append_revision(self, *revision_ids):
625
for revision_id in revision_ids:
626
mutter("add {%s} to revision-history" % revision_id)
628
rev_history = self.revision_history()
629
rev_history.extend(revision_ids)
633
self.put_controlfile('revision-history', '\n'.join(rev_history))
638
def get_revision_xml_file(self, revision_id):
639
"""Return XML file object for revision object."""
640
if not revision_id or not isinstance(revision_id, basestring):
641
raise InvalidRevisionId(revision_id)
646
return self.revision_store[revision_id]
647
except (IndexError, KeyError):
648
raise bzrlib.errors.NoSuchRevision(self, revision_id)
654
get_revision_xml = get_revision_xml_file
657
def get_revision(self, revision_id):
658
"""Return the Revision object for a named revision"""
659
xml_file = self.get_revision_xml_file(revision_id)
662
r = bzrlib.xml.serializer_v4.read_revision(xml_file)
663
except SyntaxError, e:
664
raise bzrlib.errors.BzrError('failed to unpack revision_xml',
668
assert r.revision_id == revision_id
672
def get_revision_delta(self, revno):
673
"""Return the delta for one revision.
675
The delta is relative to its mainline predecessor, or the
676
empty tree for revision 1.
678
assert isinstance(revno, int)
679
rh = self.revision_history()
680
if not (1 <= revno <= len(rh)):
681
raise InvalidRevisionNumber(revno)
683
# revno is 1-based; list is 0-based
685
new_tree = self.revision_tree(rh[revno-1])
687
old_tree = EmptyTree()
689
old_tree = self.revision_tree(rh[revno-2])
691
return compare_trees(old_tree, new_tree)
694
def get_revisions(self, revision_ids, pb=None):
695
"""Return the Revision object for a set of named revisions"""
696
from bzrlib.revision import Revision
697
from bzrlib.xml import unpack_xml
699
# TODO: We need to decide what to do here
700
# we cannot use a generator with a try/finally, because
701
# you cannot guarantee that the caller will iterate through
703
# in the past, get_inventory wasn't even wrapped in a
704
# try/finally locking block.
705
# We could either lock without the try/finally, or just
706
# not lock at all. We are reading entries that should
708
# I prefer locking with no finally, so that if someone
709
# asks for a list of revisions, but doesn't consume them,
710
# that is their problem, and they will suffer the consequences
712
for xml_file in self.revision_store.get(revision_ids, pb=pb):
714
r = bzrlib.xml.serializer_v4.read_revision(xml_file)
715
except SyntaxError, e:
716
raise bzrlib.errors.BzrError('failed to unpack revision_xml',
722
def get_revision_sha1(self, revision_id):
723
"""Hash the stored value of a revision, and return it."""
724
# In the future, revision entries will be signed. At that
725
# point, it is probably best *not* to include the signature
726
# in the revision hash. Because that lets you re-sign
727
# the revision, (add signatures/remove signatures) and still
728
# have all hash pointers stay consistent.
729
# But for now, just hash the contents.
730
return bzrlib.osutils.sha_file(self.get_revision_xml(revision_id))
733
def get_inventory(self, inventory_id):
734
"""Get Inventory object by hash.
736
TODO: Perhaps for this and similar methods, take a revision
737
parameter which can be either an integer revno or a
740
f = self.get_inventory_xml_file(inventory_id)
741
return bzrlib.xml.serializer_v4.read_inventory(f)
744
def get_inventory_xml(self, inventory_id):
745
"""Get inventory XML as a file object."""
746
# Shouldn't this have a read-lock around it?
747
# As well as some sort of trap for missing ids?
748
return self.inventory_store[inventory_id]
750
get_inventory_xml_file = get_inventory_xml
752
def get_inventories(self, inventory_ids, pb=None, ignore_missing=False):
753
"""Get Inventory objects by id
755
from bzrlib.inventory import Inventory
757
# See the discussion in get_revisions for why
758
# we don't use a try/finally block here
760
for f in self.inventory_store.get(inventory_ids, pb=pb, ignore_missing=ignore_missing):
762
# TODO: Possibly put a try/except around this to handle
763
# read serialization errors
764
r = bzrlib.xml.serializer_v4.read_inventory(f)
769
raise bzrlib.errors.NoSuchRevision(self, revision_id)
772
def get_inventory_sha1(self, inventory_id):
773
"""Return the sha1 hash of the inventory entry
775
return sha_file(self.get_inventory_xml(inventory_id))
778
def get_revision_inventory(self, revision_id):
779
"""Return inventory of a past revision."""
780
# bzr 0.0.6 imposes the constraint that the inventory_id
781
# must be the same as its revision, so this is trivial.
782
if revision_id == None:
783
from bzrlib.inventory import Inventory
784
return Inventory(self.get_root_id())
786
return self.get_inventory(revision_id)
789
def revision_history(self):
790
"""Return sequence of revision hashes on to this branch.
792
>>> ScratchBranch().revision_history()
797
return [l.rstrip('\r\n') for l in
798
self.controlfile('revision-history', 'r').readlines()]
803
def common_ancestor(self, other, self_revno=None, other_revno=None):
805
>>> from bzrlib.commit import commit
806
>>> sb = ScratchBranch(files=['foo', 'foo~'])
807
>>> sb.common_ancestor(sb) == (None, None)
809
>>> commit(sb, "Committing first revision", verbose=False)
810
>>> sb.common_ancestor(sb)[0]
812
>>> clone = sb.clone()
813
>>> commit(sb, "Committing second revision", verbose=False)
814
>>> sb.common_ancestor(sb)[0]
816
>>> sb.common_ancestor(clone)[0]
818
>>> commit(clone, "Committing divergent second revision",
820
>>> sb.common_ancestor(clone)[0]
822
>>> sb.common_ancestor(clone) == clone.common_ancestor(sb)
824
>>> sb.common_ancestor(sb) != clone.common_ancestor(clone)
826
>>> clone2 = sb.clone()
827
>>> sb.common_ancestor(clone2)[0]
829
>>> sb.common_ancestor(clone2, self_revno=1)[0]
831
>>> sb.common_ancestor(clone2, other_revno=1)[0]
834
my_history = self.revision_history()
835
other_history = other.revision_history()
836
if self_revno is None:
837
self_revno = len(my_history)
838
if other_revno is None:
839
other_revno = len(other_history)
840
indices = range(min((self_revno, other_revno)))
843
if my_history[r] == other_history[r]:
844
return r+1, my_history[r]
849
"""Return current revision number for this branch.
851
That is equivalent to the number of revisions committed to
854
return len(self.revision_history())
857
def last_patch(self):
858
"""Return last patch hash, or None if no history.
860
ph = self.revision_history()
867
def missing_revisions(self, other, stop_revision=None, diverged_ok=False):
869
If self and other have not diverged, return a list of the revisions
870
present in other, but missing from self.
872
>>> from bzrlib.commit import commit
873
>>> bzrlib.trace.silent = True
874
>>> br1 = ScratchBranch()
875
>>> br2 = ScratchBranch()
876
>>> br1.missing_revisions(br2)
878
>>> commit(br2, "lala!", rev_id="REVISION-ID-1")
879
>>> br1.missing_revisions(br2)
881
>>> br2.missing_revisions(br1)
883
>>> commit(br1, "lala!", rev_id="REVISION-ID-1")
884
>>> br1.missing_revisions(br2)
886
>>> commit(br2, "lala!", rev_id="REVISION-ID-2A")
887
>>> br1.missing_revisions(br2)
889
>>> commit(br1, "lala!", rev_id="REVISION-ID-2B")
890
>>> br1.missing_revisions(br2)
891
Traceback (most recent call last):
892
DivergedBranches: These branches have diverged.
894
self_history = self.revision_history()
895
self_len = len(self_history)
896
other_history = other.revision_history()
897
other_len = len(other_history)
898
common_index = min(self_len, other_len) -1
899
if common_index >= 0 and \
900
self_history[common_index] != other_history[common_index]:
901
raise DivergedBranches(self, other)
903
if stop_revision is None:
904
stop_revision = other_len
905
elif stop_revision > other_len:
906
raise bzrlib.errors.NoSuchRevision(self, stop_revision)
908
return other_history[self_len:stop_revision]
911
def update_revisions(self, other, stop_revision=None):
912
"""Pull in all new revisions from other branch.
914
from bzrlib.fetch import greedy_fetch
915
from bzrlib.revision import get_intervening_revisions
917
pb = bzrlib.ui.ui_factory.progress_bar()
918
pb.update('comparing histories')
919
if stop_revision is None:
920
other_revision = other.last_patch()
922
other_revision = other.get_rev_id(stop_revision)
923
count = greedy_fetch(self, other, other_revision, pb)[0]
925
revision_ids = self.missing_revisions(other, stop_revision)
926
except DivergedBranches, e:
928
revision_ids = get_intervening_revisions(self.last_patch(),
929
other_revision, self)
930
assert self.last_patch() not in revision_ids
931
except bzrlib.errors.NotAncestor:
934
self.append_revision(*revision_ids)
937
def install_revisions(self, other, revision_ids, pb):
938
# We are going to iterate this many times, so make sure
939
# that it is a list, and not a generator
940
revision_ids = list(revision_ids)
941
if hasattr(other.revision_store, "prefetch"):
942
other.revision_store.prefetch(revision_ids)
943
if hasattr(other.inventory_store, "prefetch"):
944
other.inventory_store.prefetch(inventory_ids)
947
pb = bzrlib.ui.ui_factory.progress_bar()
949
# This entire next section is generally done
950
# with either generators, or bulk updates
951
inventories = other.get_inventories(revision_ids, ignore_missing=True)
955
good_revisions = set()
956
for i, (inv, rev_id) in enumerate(zip(inventories, revision_ids)):
957
pb.update('fetching revision', i+1, len(revision_ids))
959
# We don't really need to get the revision here, because
960
# the only thing we needed was the inventory_id, which now
961
# is (by design) identical to the revision_id
963
# rev = other.get_revision(rev_id)
964
# except bzrlib.errors.NoSuchRevision:
965
# failures.add(rev_id)
972
good_revisions.add(rev_id)
975
for key, entry in inv.iter_entries():
976
if entry.text_id is None:
978
text_ids.append(entry.text_id)
980
has_ids = self.text_store.has(text_ids)
981
for has, text_id in zip(has_ids, text_ids):
983
needed_texts.add(text_id)
987
count, cp_fail = self.text_store.copy_multi(other.text_store,
989
#print "Added %d texts." % count
990
count, cp_fail = self.inventory_store.copy_multi(other.inventory_store,
992
#print "Added %d inventories." % count
993
count, cp_fail = self.revision_store.copy_multi(other.revision_store,
996
assert len(cp_fail) == 0
997
return count, failures
1000
def commit(self, *args, **kw):
1001
from bzrlib.commit import commit
1002
commit(self, *args, **kw)
1005
def revision_id_to_revno(self, revision_id):
1006
"""Given a revision id, return its revno"""
1007
history = self.revision_history()
1009
return history.index(revision_id) + 1
1011
raise bzrlib.errors.NoSuchRevision(self, revision_id)
1014
def get_rev_id(self, revno, history=None):
1015
"""Find the revision id of the specified revno."""
1019
history = self.revision_history()
1020
elif revno <= 0 or revno > len(history):
1021
raise bzrlib.errors.NoSuchRevision(self, revno)
1022
return history[revno - 1]
1025
def revision_tree(self, revision_id):
1026
"""Return Tree for a revision on this branch.
1028
`revision_id` may be None for the null revision, in which case
1029
an `EmptyTree` is returned."""
1030
# TODO: refactor this to use an existing revision object
1031
# so we don't need to read it in twice.
1032
if revision_id == None:
1035
inv = self.get_revision_inventory(revision_id)
1036
return RevisionTree(self.text_store, inv)
1039
def working_tree(self):
1040
"""Return a `Tree` for the working copy."""
1041
from bzrlib.workingtree import WorkingTree
1042
# TODO: In the future, WorkingTree should utilize Transport
1043
return WorkingTree(self._transport.base, self.read_working_inventory())
1046
def basis_tree(self):
1047
"""Return `Tree` object for last revision.
1049
If there are no revisions yet, return an `EmptyTree`.
1051
r = self.last_patch()
1055
return RevisionTree(self.text_store, self.get_revision_inventory(r))
1059
def rename_one(self, from_rel, to_rel):
1062
This can change the directory or the filename or both.
1066
tree = self.working_tree()
1067
inv = tree.inventory
1068
if not tree.has_filename(from_rel):
1069
raise BzrError("can't rename: old working file %r does not exist" % from_rel)
1070
if tree.has_filename(to_rel):
1071
raise BzrError("can't rename: new working file %r already exists" % to_rel)
1073
file_id = inv.path2id(from_rel)
1075
raise BzrError("can't rename: old name %r is not versioned" % from_rel)
1077
if inv.path2id(to_rel):
1078
raise BzrError("can't rename: new name %r is already versioned" % to_rel)
1080
to_dir, to_tail = os.path.split(to_rel)
1081
to_dir_id = inv.path2id(to_dir)
1082
if to_dir_id == None and to_dir != '':
1083
raise BzrError("can't determine destination directory id for %r" % to_dir)
1085
mutter("rename_one:")
1086
mutter(" file_id {%s}" % file_id)
1087
mutter(" from_rel %r" % from_rel)
1088
mutter(" to_rel %r" % to_rel)
1089
mutter(" to_dir %r" % to_dir)
1090
mutter(" to_dir_id {%s}" % to_dir_id)
1092
inv.rename(file_id, to_dir_id, to_tail)
1094
from_abs = self.abspath(from_rel)
1095
to_abs = self.abspath(to_rel)
1097
os.rename(from_abs, to_abs)
1099
raise BzrError("failed to rename %r to %r: %s"
1100
% (from_abs, to_abs, e[1]),
1101
["rename rolled back"])
1103
self._write_inventory(inv)
1108
def move(self, from_paths, to_name):
1111
to_name must exist as a versioned directory.
1113
If to_name exists and is a directory, the files are moved into
1114
it, keeping their old names. If it is a directory,
1116
Note that to_name is only the last component of the new name;
1117
this doesn't change the directory.
1119
This returns a list of (from_path, to_path) pairs for each
1120
entry that is moved.
1125
## TODO: Option to move IDs only
1126
assert not isinstance(from_paths, basestring)
1127
tree = self.working_tree()
1128
inv = tree.inventory
1129
to_abs = self.abspath(to_name)
1130
if not isdir(to_abs):
1131
raise BzrError("destination %r is not a directory" % to_abs)
1132
if not tree.has_filename(to_name):
1133
raise BzrError("destination %r not in working directory" % to_abs)
1134
to_dir_id = inv.path2id(to_name)
1135
if to_dir_id == None and to_name != '':
1136
raise BzrError("destination %r is not a versioned directory" % to_name)
1137
to_dir_ie = inv[to_dir_id]
1138
if to_dir_ie.kind not in ('directory', 'root_directory'):
1139
raise BzrError("destination %r is not a directory" % to_abs)
1141
to_idpath = inv.get_idpath(to_dir_id)
1143
for f in from_paths:
1144
if not tree.has_filename(f):
1145
raise BzrError("%r does not exist in working tree" % f)
1146
f_id = inv.path2id(f)
1148
raise BzrError("%r is not versioned" % f)
1149
name_tail = splitpath(f)[-1]
1150
dest_path = appendpath(to_name, name_tail)
1151
if tree.has_filename(dest_path):
1152
raise BzrError("destination %r already exists" % dest_path)
1153
if f_id in to_idpath:
1154
raise BzrError("can't move %r to a subdirectory of itself" % f)
1156
# OK, so there's a race here, it's possible that someone will
1157
# create a file in this interval and then the rename might be
1158
# left half-done. But we should have caught most problems.
1160
for f in from_paths:
1161
name_tail = splitpath(f)[-1]
1162
dest_path = appendpath(to_name, name_tail)
1163
result.append((f, dest_path))
1164
inv.rename(inv.path2id(f), to_dir_id, name_tail)
1166
os.rename(self.abspath(f), self.abspath(dest_path))
1168
raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
1169
["rename rolled back"])
1171
self._write_inventory(inv)
1178
def revert(self, filenames, old_tree=None, backups=True):
1179
"""Restore selected files to the versions from a previous tree.
1182
If true (default) backups are made of files before
1185
from bzrlib.errors import NotVersionedError, BzrError
1186
from bzrlib.atomicfile import AtomicFile
1187
from bzrlib.osutils import backup_file
1189
inv = self.read_working_inventory()
1190
if old_tree is None:
1191
old_tree = self.basis_tree()
1192
old_inv = old_tree.inventory
1195
for fn in filenames:
1196
file_id = inv.path2id(fn)
1198
raise NotVersionedError("not a versioned file", fn)
1199
if not old_inv.has_id(file_id):
1200
raise BzrError("file not present in old tree", fn, file_id)
1201
nids.append((fn, file_id))
1203
# TODO: Rename back if it was previously at a different location
1205
# TODO: If given a directory, restore the entire contents from
1206
# the previous version.
1208
# TODO: Make a backup to a temporary file.
1210
# TODO: If the file previously didn't exist, delete it?
1211
for fn, file_id in nids:
1214
f = AtomicFile(fn, 'wb')
1216
f.write(old_tree.get_file(file_id).read())
1222
def pending_merges(self):
1223
"""Return a list of pending merges.
1225
These are revisions that have been merged into the working
1226
directory but not yet committed.
1228
cfn = self._rel_controlfilename('pending-merges')
1229
if not self._transport.has(cfn):
1232
for l in self.controlfile('pending-merges', 'r').readlines():
1233
p.append(l.rstrip('\n'))
1237
def add_pending_merge(self, *revision_ids):
1238
from bzrlib.revision import validate_revision_id
1240
for rev_id in revision_ids:
1241
validate_revision_id(rev_id)
1243
p = self.pending_merges()
1245
for rev_id in revision_ids:
1251
self.set_pending_merges(p)
1253
def set_pending_merges(self, rev_list):
1256
self.put_controlfile('pending-merges', '\n'.join(rev_list))
1261
def get_parent(self):
1262
"""Return the parent location of the branch.
1264
This is the default location for push/pull/missing. The usual
1265
pattern is that the user can override it by specifying a
1269
_locs = ['parent', 'pull', 'x-pull']
1272
return self.controlfile(l, 'r').read().strip('\n')
1274
if e.errno != errno.ENOENT:
1279
def set_parent(self, url):
1280
# TODO: Maybe delete old location files?
1281
from bzrlib.atomicfile import AtomicFile
1284
f = AtomicFile(self.controlfilename('parent'))
1293
def check_revno(self, revno):
1295
Check whether a revno corresponds to any revision.
1296
Zero (the NULL revision) is considered valid.
1299
self.check_real_revno(revno)
1301
def check_real_revno(self, revno):
1303
Check whether a revno corresponds to a real revision.
1304
Zero (the NULL revision) is considered invalid
1306
if revno < 1 or revno > self.revno():
1307
raise InvalidRevisionNumber(revno)
1313
class ScratchBranch(LocalBranch):
1314
"""Special test class: a branch that cleans up after itself.
1316
>>> b = ScratchBranch()
1324
def __init__(self, files=[], dirs=[], base=None):
1325
"""Make a test branch.
1327
This creates a temporary directory and runs init-tree in it.
1329
If any files are listed, they are created in the working copy.
1331
from tempfile import mkdtemp
1336
LocalBranch.__init__(self, base, init=init)
1338
self._transport.mkdir(d)
1341
self._transport.put(f, 'content of %s' % f)
1346
>>> orig = ScratchBranch(files=["file1", "file2"])
1347
>>> clone = orig.clone()
1348
>>> os.path.samefile(orig.base, clone.base)
1350
>>> os.path.isfile(os.path.join(clone.base, "file1"))
1353
from shutil import copytree
1354
from tempfile import mkdtemp
1357
copytree(self.base, base, symlinks=True)
1358
return ScratchBranch(base=base)
1366
"""Destroy the test branch, removing the scratch directory."""
1367
from shutil import rmtree
1370
mutter("delete ScratchBranch %s" % self.base)
1373
# Work around for shutil.rmtree failing on Windows when
1374
# readonly files are encountered
1375
mutter("hit exception in destroying ScratchBranch: %s" % e)
1376
for root, dirs, files in os.walk(self.base, topdown=False):
1378
os.chmod(os.path.join(root, name), 0700)
1380
self._transport = None
1384
######################################################################
1388
def is_control_file(filename):
1389
## FIXME: better check
1390
filename = os.path.normpath(filename)
1391
while filename != '':
1392
head, tail = os.path.split(filename)
1393
## mutter('check %r for control file' % ((head, tail), ))
1394
if tail == bzrlib.BZRDIR:
1396
if filename == head:
1403
def gen_file_id(name):
1404
"""Return new file id.
1406
This should probably generate proper UUIDs, but for the moment we
1407
cope with just randomness because running uuidgen every time is
1410
from binascii import hexlify
1411
from time import time
1413
# get last component
1414
idx = name.rfind('/')
1416
name = name[idx+1 : ]
1417
idx = name.rfind('\\')
1419
name = name[idx+1 : ]
1421
# make it not a hidden file
1422
name = name.lstrip('.')
1424
# remove any wierd characters; we don't escape them but rather
1425
# just pull them out
1426
name = re.sub(r'[^\w.]', '', name)
1428
s = hexlify(rand_bytes(8))
1429
return '-'.join((name, compact_date(time()), s))
1433
"""Return a new tree-root file id."""
1434
return gen_file_id('TREE_ROOT')
1437
def copy_branch(branch_from, to_location, revision=None):
1438
"""Copy branch_from into the existing directory to_location.
1441
If not None, only revisions up to this point will be copied.
1442
The head of the new branch will be that revision.
1445
The name of a local directory that exists but is empty.
1447
from bzrlib.merge import merge
1448
from bzrlib.revisionspec import RevisionSpec
1450
assert isinstance(branch_from, Branch)
1451
assert isinstance(to_location, basestring)
1453
br_to = Branch.initialize(to_location)
1454
br_to.set_root_id(branch_from.get_root_id())
1455
if revision is None:
1456
revno = branch_from.revno()
1458
revno, rev_id = RevisionSpec(revision).in_history(branch_from)
1459
br_to.update_revisions(branch_from, stop_revision=revno)
1460
merge((to_location, -1), (to_location, 0), this_dir=to_location,
1461
check_clean=False, ignore_zero=True)
1462
br_to.set_parent(branch_from.base)