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 warnings import warn
23
import xml.sax.saxutils
24
from cStringIO import StringIO
28
from bzrlib.trace import mutter, note
29
from bzrlib.osutils import (isdir, quotefn,
30
rename, splitpath, sha_file,
31
file_kind, abspath, normpath, pathjoin)
32
import bzrlib.errors as errors
33
from bzrlib.errors import (BzrError, InvalidRevisionNumber, InvalidRevisionId,
34
NoSuchRevision, HistoryMissing, NotBranchError,
35
DivergedBranches, LockError, UnlistableStore,
36
UnlistableBranch, NoSuchFile, NotVersionedError,
38
from bzrlib.textui import show_status
39
from bzrlib.config import TreeConfig
40
from bzrlib.decorators import needs_read_lock, needs_write_lock
41
from bzrlib.delta import compare_trees
42
import bzrlib.inventory as inventory
43
from bzrlib.inventory import Inventory
44
from bzrlib.lockable_files import LockableFiles
45
from bzrlib.revision import (Revision, is_ancestor, get_intervening_revisions)
46
from bzrlib.repository import Repository
47
from bzrlib.store import copy_all
48
import bzrlib.transactions as transactions
49
from bzrlib.transport import Transport, get_transport
50
from bzrlib.tree import EmptyTree, RevisionTree
55
BZR_BRANCH_FORMAT_4 = "Bazaar-NG branch, format 0.0.4\n"
56
BZR_BRANCH_FORMAT_5 = "Bazaar-NG branch, format 5\n"
57
BZR_BRANCH_FORMAT_6 = "Bazaar-NG branch, format 6\n"
60
# TODO: Maybe include checks for common corruption of newlines, etc?
62
# TODO: Some operations like log might retrieve the same revisions
63
# repeatedly to calculate deltas. We could perhaps have a weakref
64
# cache in memory to make this faster. In general anything can be
65
# cached in memory between lock and unlock operations. .. nb thats
66
# what the transaction identity map provides
69
######################################################################
73
"""Branch holding a history of revisions.
76
Base directory/url of the branch.
80
def __init__(self, *ignored, **ignored_too):
81
raise NotImplementedError('The Branch class is abstract')
84
def open_downlevel(base):
85
"""Open a branch which may be of an old format.
87
Only local branches are supported."""
88
return BzrBranch(get_transport(base), relax_version_check=True)
92
"""Open an existing branch, rooted at 'base' (url)"""
93
t = get_transport(base)
94
mutter("trying to open %r with transport %r", base, t)
98
def open_containing(url):
99
"""Open an existing branch which contains url.
101
This probes for a branch at url, and searches upwards from there.
103
Basically we keep looking up until we find the control directory or
104
run into the root. If there isn't one, raises NotBranchError.
105
If there is one, it is returned, along with the unused portion of url.
107
t = get_transport(url)
110
return BzrBranch(t), t.relpath(url)
111
except NotBranchError, e:
112
mutter('not a branch in: %r %s', t.base, e)
113
new_t = t.clone('..')
114
if new_t.base == t.base:
115
# reached the root, whatever that may be
116
raise NotBranchError(path=url)
120
def initialize(base):
121
"""Create a new branch, rooted at 'base' (url)"""
122
t = get_transport(unicode(base))
123
return BzrBranch(t, init=True)
125
def setup_caching(self, cache_root):
126
"""Subclasses that care about caching should override this, and set
127
up cached stores located under cache_root.
129
# seems to be unused, 2006-01-13 mbp
130
warn('%s is deprecated' % self.setup_caching)
131
self.cache_root = cache_root
134
cfg = self.tree_config()
135
return cfg.get_option(u"nickname", default=self.base.split('/')[-2])
137
def _set_nick(self, nick):
138
cfg = self.tree_config()
139
cfg.set_option(nick, "nickname")
140
assert cfg.get_option("nickname") == nick
142
nick = property(_get_nick, _set_nick)
144
def push_stores(self, branch_to):
145
"""Copy the content of this branches store to branch_to."""
146
raise NotImplementedError('push_stores is abstract')
148
def lock_write(self):
149
raise NotImplementedError('lock_write is abstract')
152
raise NotImplementedError('lock_read is abstract')
155
raise NotImplementedError('unlock is abstract')
157
def peek_lock_mode(self):
158
"""Return lock mode for the Branch: 'r', 'w' or None"""
159
raise NotImplementedError(self.peek_lock_mode)
161
def abspath(self, name):
162
"""Return absolute filename for something in the branch
164
XXX: Robert Collins 20051017 what is this used for? why is it a branch
165
method and not a tree method.
167
raise NotImplementedError('abspath is abstract')
169
def get_root_id(self):
170
"""Return the id of this branches root"""
171
raise NotImplementedError('get_root_id is abstract')
173
def print_file(self, file, revision_id):
174
"""Print `file` to stdout."""
175
raise NotImplementedError('print_file is abstract')
177
def append_revision(self, *revision_ids):
178
raise NotImplementedError('append_revision is abstract')
180
def set_revision_history(self, rev_history):
181
raise NotImplementedError('set_revision_history is abstract')
183
def revision_history(self):
184
"""Return sequence of revision hashes on to this branch."""
185
raise NotImplementedError('revision_history is abstract')
188
"""Return current revision number for this branch.
190
That is equivalent to the number of revisions committed to
193
return len(self.revision_history())
195
def last_revision(self):
196
"""Return last patch hash, or None if no history."""
197
ph = self.revision_history()
203
def missing_revisions(self, other, stop_revision=None, diverged_ok=False):
204
"""Return a list of new revisions that would perfectly fit.
206
If self and other have not diverged, return a list of the revisions
207
present in other, but missing from self.
209
>>> from bzrlib.commit import commit
210
>>> bzrlib.trace.silent = True
211
>>> br1 = ScratchBranch()
212
>>> br2 = ScratchBranch()
213
>>> br1.missing_revisions(br2)
215
>>> commit(br2, "lala!", rev_id="REVISION-ID-1")
216
>>> br1.missing_revisions(br2)
218
>>> br2.missing_revisions(br1)
220
>>> commit(br1, "lala!", rev_id="REVISION-ID-1")
221
>>> br1.missing_revisions(br2)
223
>>> commit(br2, "lala!", rev_id="REVISION-ID-2A")
224
>>> br1.missing_revisions(br2)
226
>>> commit(br1, "lala!", rev_id="REVISION-ID-2B")
227
>>> br1.missing_revisions(br2)
228
Traceback (most recent call last):
229
DivergedBranches: These branches have diverged. Try merge.
231
self_history = self.revision_history()
232
self_len = len(self_history)
233
other_history = other.revision_history()
234
other_len = len(other_history)
235
common_index = min(self_len, other_len) -1
236
if common_index >= 0 and \
237
self_history[common_index] != other_history[common_index]:
238
raise DivergedBranches(self, other)
240
if stop_revision is None:
241
stop_revision = other_len
243
assert isinstance(stop_revision, int)
244
if stop_revision > other_len:
245
raise bzrlib.errors.NoSuchRevision(self, stop_revision)
246
return other_history[self_len:stop_revision]
248
def update_revisions(self, other, stop_revision=None):
249
"""Pull in new perfect-fit revisions."""
250
raise NotImplementedError('update_revisions is abstract')
252
def pullable_revisions(self, other, stop_revision):
253
raise NotImplementedError('pullable_revisions is abstract')
255
def revision_id_to_revno(self, revision_id):
256
"""Given a revision id, return its revno"""
257
if revision_id is None:
259
history = self.revision_history()
261
return history.index(revision_id) + 1
263
raise bzrlib.errors.NoSuchRevision(self, revision_id)
265
def get_rev_id(self, revno, history=None):
266
"""Find the revision id of the specified revno."""
270
history = self.revision_history()
271
elif revno <= 0 or revno > len(history):
272
raise bzrlib.errors.NoSuchRevision(self, revno)
273
return history[revno - 1]
275
def working_tree(self):
276
"""Return a `Tree` for the working copy if this is a local branch."""
277
raise NotImplementedError('working_tree is abstract')
279
def pull(self, source, overwrite=False):
280
raise NotImplementedError('pull is abstract')
282
def basis_tree(self):
283
"""Return `Tree` object for last revision.
285
If there are no revisions yet, return an `EmptyTree`.
287
return self.repository.revision_tree(self.last_revision())
289
def rename_one(self, from_rel, to_rel):
292
This can change the directory or the filename or both.
294
raise NotImplementedError('rename_one is abstract')
296
def move(self, from_paths, to_name):
299
to_name must exist as a versioned directory.
301
If to_name exists and is a directory, the files are moved into
302
it, keeping their old names. If it is a directory,
304
Note that to_name is only the last component of the new name;
305
this doesn't change the directory.
307
This returns a list of (from_path, to_path) pairs for each
310
raise NotImplementedError('move is abstract')
312
def get_parent(self):
313
"""Return the parent location of the branch.
315
This is the default location for push/pull/missing. The usual
316
pattern is that the user can override it by specifying a
319
raise NotImplementedError('get_parent is abstract')
321
def get_push_location(self):
322
"""Return the None or the location to push this branch to."""
323
raise NotImplementedError('get_push_location is abstract')
325
def set_push_location(self, location):
326
"""Set a new push location for this branch."""
327
raise NotImplementedError('set_push_location is abstract')
329
def set_parent(self, url):
330
raise NotImplementedError('set_parent is abstract')
332
def check_revno(self, revno):
334
Check whether a revno corresponds to any revision.
335
Zero (the NULL revision) is considered valid.
338
self.check_real_revno(revno)
340
def check_real_revno(self, revno):
342
Check whether a revno corresponds to a real revision.
343
Zero (the NULL revision) is considered invalid
345
if revno < 1 or revno > self.revno():
346
raise InvalidRevisionNumber(revno)
348
def sign_revision(self, revision_id, gpg_strategy):
349
raise NotImplementedError('sign_revision is abstract')
351
def store_revision_signature(self, gpg_strategy, plaintext, revision_id):
352
raise NotImplementedError('store_revision_signature is abstract')
354
def clone(self, to_location, revision=None, basis_branch=None, to_branch_type=None):
355
"""Copy this branch into the existing directory to_location.
357
Returns the newly created branch object.
360
If not None, only revisions up to this point will be copied.
361
The head of the new branch will be that revision. Must be a
364
to_location -- The destination directory; must either exist and be
365
empty, or not exist, in which case it is created.
368
A local branch to copy revisions from, related to this branch.
369
This is used when branching from a remote (slow) branch, and we have
370
a local branch that might contain some relevant revisions.
373
Branch type of destination branch
375
# circular import protection
376
from bzrlib.merge import build_working_dir
378
assert isinstance(to_location, basestring)
379
if not bzrlib.osutils.lexists(to_location):
380
os.mkdir(to_location)
381
if to_branch_type is None:
382
to_branch_type = BzrBranch
383
br_to = to_branch_type.initialize(to_location)
384
mutter("copy branch from %s to %s", self, br_to)
385
if basis_branch is not None:
386
basis_branch.push_stores(br_to)
387
br_to.working_tree().set_root_id(self.get_root_id())
389
revision = self.last_revision()
390
br_to.update_revisions(self, stop_revision=revision)
391
br_to.set_parent(self.base)
392
build_working_dir(to_location)
396
def fileid_involved_between_revs(self, from_revid, to_revid):
397
""" This function returns the file_id(s) involved in the
398
changes between the from_revid revision and the to_revid
401
raise NotImplementedError('fileid_involved_between_revs is abstract')
403
def fileid_involved(self, last_revid=None):
404
""" This function returns the file_id(s) involved in the
405
changes up to the revision last_revid
406
If no parametr is passed, then all file_id[s] present in the
407
repository are returned
409
raise NotImplementedError('fileid_involved is abstract')
411
def fileid_involved_by_set(self, changes):
412
""" This function returns the file_id(s) involved in the
413
changes present in the set 'changes'
415
raise NotImplementedError('fileid_involved_by_set is abstract')
417
def fileid_involved_between_revs(self, from_revid, to_revid):
418
""" This function returns the file_id(s) involved in the
419
changes between the from_revid revision and the to_revid
422
raise NotImplementedError('fileid_involved_between_revs is abstract')
424
def fileid_involved(self, last_revid=None):
425
""" This function returns the file_id(s) involved in the
426
changes up to the revision last_revid
427
If no parametr is passed, then all file_id[s] present in the
428
repository are returned
430
raise NotImplementedError('fileid_involved is abstract')
432
def fileid_involved_by_set(self, changes):
433
""" This function returns the file_id(s) involved in the
434
changes present in the set 'changes'
436
raise NotImplementedError('fileid_involved_by_set is abstract')
439
class BzrBranch(Branch):
440
"""A branch stored in the actual filesystem.
442
Note that it's "local" in the context of the filesystem; it doesn't
443
really matter if it's on an nfs/smb/afs/coda/... share, as long as
444
it's writable, and can be accessed via the normal filesystem API.
447
# We actually expect this class to be somewhat short-lived; part of its
448
# purpose is to try to isolate what bits of the branch logic are tied to
449
# filesystem access, so that in a later step, we can extricate them to
450
# a separarte ("storage") class.
451
_inventory_weave = None
453
# Map some sort of prefix into a namespace
454
# stuff like "revno:10", "revid:", etc.
455
# This should match a prefix with a function which accepts
456
REVISION_NAMESPACES = {}
458
def push_stores(self, branch_to):
459
"""See Branch.push_stores."""
460
if (self._branch_format != branch_to._branch_format
461
or self._branch_format != 4):
462
from bzrlib.fetch import greedy_fetch
463
mutter("falling back to fetch logic to push between %s(%s) and %s(%s)",
464
self, self._branch_format, branch_to, branch_to._branch_format)
465
greedy_fetch(to_branch=branch_to, from_branch=self,
466
revision=self.last_revision())
469
store_pairs = ((self.text_store, branch_to.text_store),
470
(self.inventory_store, branch_to.inventory_store),
471
(self.revision_store, branch_to.revision_store))
473
for from_store, to_store in store_pairs:
474
copy_all(from_store, to_store)
475
except UnlistableStore:
476
raise UnlistableBranch(from_store)
478
def __init__(self, transport, init=False,
479
relax_version_check=False):
480
"""Create new branch object at a particular location.
482
transport -- A Transport object, defining how to access files.
484
init -- If True, create new control files in a previously
485
unversioned directory. If False, the branch must already
488
relax_version_check -- If true, the usual check for the branch
489
version is not applied. This is intended only for
490
upgrade/recovery type use; it's not guaranteed that
491
all operations will work on old format branches.
493
In the test suite, creation of new trees is tested using the
494
`ScratchBranch` class.
496
assert isinstance(transport, Transport), \
497
"%r is not a Transport" % transport
498
# TODO: jam 20060103 We create a clone of this transport at .bzr/
499
# and then we forget about it, should we keep a handle to it?
500
self._base = transport.base
501
self.control_files = LockableFiles(transport.clone(bzrlib.BZRDIR),
505
self._check_format(relax_version_check)
506
self.repository = Repository(transport, self._branch_format)
509
return '%s(%r)' % (self.__class__.__name__, self.base)
514
# TODO: It might be best to do this somewhere else,
515
# but it is nice for a Branch object to automatically
516
# cache it's information.
517
# Alternatively, we could have the Transport objects cache requests
518
# See the earlier discussion about how major objects (like Branch)
519
# should never expect their __del__ function to run.
520
# XXX: cache_root seems to be unused, 2006-01-13 mbp
521
if hasattr(self, 'cache_root') and self.cache_root is not None:
523
shutil.rmtree(self.cache_root)
526
self.cache_root = None
531
base = property(_get_base, doc="The URL for the root of this branch.")
533
def _finish_transaction(self):
534
"""Exit the current transaction."""
535
return self.control_files._finish_transaction()
537
def get_transaction(self):
538
"""Return the current active transaction.
540
If no transaction is active, this returns a passthrough object
541
for which all data is immediately flushed and no caching happens.
543
# this is an explicit function so that we can do tricky stuff
544
# when the storage in rev_storage is elsewhere.
545
# we probably need to hook the two 'lock a location' and
546
# 'have a transaction' together more delicately, so that
547
# we can have two locks (branch and storage) and one transaction
548
# ... and finishing the transaction unlocks both, but unlocking
549
# does not. - RBC 20051121
550
return self.control_files.get_transaction()
552
def _set_transaction(self, transaction):
553
"""Set a new active transaction."""
554
return self.control_files._set_transaction(transaction)
556
def abspath(self, name):
557
"""See Branch.abspath."""
558
return self.control_files._transport.abspath(name)
560
def _make_control(self):
561
from bzrlib.inventory import Inventory
562
from bzrlib.weavefile import write_weave_v5
563
from bzrlib.weave import Weave
565
# Create an empty inventory
567
# if we want per-tree root ids then this is the place to set
568
# them; they're not needed for now and so ommitted for
570
bzrlib.xml5.serializer_v5.write_inventory(Inventory(), sio)
571
empty_inv = sio.getvalue()
573
bzrlib.weavefile.write_weave_v5(Weave(), sio)
574
empty_weave = sio.getvalue()
576
dirs = ['', 'revision-store', 'weaves']
578
"This is a Bazaar-NG control directory.\n"
579
"Do not change any files in this directory.\n"),
580
('branch-format', BZR_BRANCH_FORMAT_6),
581
('revision-history', ''),
584
('pending-merges', ''),
585
('inventory', empty_inv),
586
('inventory.weave', empty_weave),
588
cfe = self.control_files._escape
589
# FIXME: RBC 20060125 dont peek under the covers
590
self.control_files._transport.mkdir_multi([cfe(d) for d in dirs],
591
mode=self.control_files._dir_mode)
592
self.control_files.lock_write()
594
for file, content in files:
595
self.control_files.put_utf8(file, content)
596
mutter('created control directory in ' + self.base)
598
self.control_files.unlock()
600
def _check_format(self, relax_version_check):
601
"""Check this branch format is supported.
603
The format level is stored, as an integer, in
604
self._branch_format for code that needs to check it later.
606
In the future, we might need different in-memory Branch
607
classes to support downlevel branches. But not yet.
610
fmt = self.control_files.get_utf8('branch-format').read()
612
raise NotBranchError(path=self.base)
613
mutter("got branch format %r", fmt)
614
if fmt == BZR_BRANCH_FORMAT_6:
615
self._branch_format = 6
616
elif fmt == BZR_BRANCH_FORMAT_5:
617
self._branch_format = 5
618
elif fmt == BZR_BRANCH_FORMAT_4:
619
self._branch_format = 4
621
if (not relax_version_check
622
and self._branch_format not in (5, 6)):
623
raise errors.UnsupportedFormatError(
624
'sorry, branch format %r not supported' % fmt,
625
['use a different bzr version',
626
'or remove the .bzr directory'
627
' and "bzr init" again'])
630
def get_root_id(self):
631
"""See Branch.get_root_id."""
632
inv = self.repository.get_inventory(self.last_revision())
633
return inv.root.file_id
635
def lock_write(self):
636
# TODO: test for failed two phase locks. This is known broken.
637
self.control_files.lock_write()
638
self.repository.lock_write()
641
# TODO: test for failed two phase locks. This is known broken.
642
self.control_files.lock_read()
643
self.repository.lock_read()
646
# TODO: test for failed two phase locks. This is known broken.
647
self.repository.unlock()
648
self.control_files.unlock()
650
def peek_lock_mode(self):
651
if self.control_files._lock_count == 0:
654
return self.control_files._lock_mode
657
def print_file(self, file, revision_id):
658
"""See Branch.print_file."""
659
return self.repository.print_file(file, revision_id)
662
def append_revision(self, *revision_ids):
663
"""See Branch.append_revision."""
664
for revision_id in revision_ids:
665
mutter("add {%s} to revision-history" % revision_id)
666
rev_history = self.revision_history()
667
rev_history.extend(revision_ids)
668
self.set_revision_history(rev_history)
671
def set_revision_history(self, rev_history):
672
"""See Branch.set_revision_history."""
673
old_revision = self.last_revision()
674
new_revision = rev_history[-1]
675
self.control_files.put_utf8(
676
'revision-history', '\n'.join(rev_history))
678
# FIXME: RBC 20051207 this smells wrong, last_revision in the
679
# working tree may be != to last_revision in the branch - so
680
# why is this passing in the branches last_revision ?
681
self.working_tree().set_last_revision(new_revision, old_revision)
682
except NoWorkingTree:
683
mutter('Unable to set_last_revision without a working tree.')
685
def get_revision_delta(self, revno):
686
"""Return the delta for one revision.
688
The delta is relative to its mainline predecessor, or the
689
empty tree for revision 1.
691
assert isinstance(revno, int)
692
rh = self.revision_history()
693
if not (1 <= revno <= len(rh)):
694
raise InvalidRevisionNumber(revno)
696
# revno is 1-based; list is 0-based
698
new_tree = self.repository.revision_tree(rh[revno-1])
700
old_tree = EmptyTree()
702
old_tree = self.repository.revision_tree(rh[revno-2])
703
return compare_trees(old_tree, new_tree)
706
def revision_history(self):
707
"""See Branch.revision_history."""
708
# FIXME are transactions bound to control files ? RBC 20051121
709
transaction = self.get_transaction()
710
history = transaction.map.find_revision_history()
711
if history is not None:
712
mutter("cache hit for revision-history in %s", self)
714
history = [l.rstrip('\r\n') for l in
715
self.control_files.get_utf8('revision-history').readlines()]
716
transaction.map.add_revision_history(history)
717
# this call is disabled because revision_history is
718
# not really an object yet, and the transaction is for objects.
719
# transaction.register_clean(history, precious=True)
722
def update_revisions(self, other, stop_revision=None):
723
"""See Branch.update_revisions."""
724
from bzrlib.fetch import greedy_fetch
725
if stop_revision is None:
726
stop_revision = other.last_revision()
727
### Should this be checking is_ancestor instead of revision_history?
728
if (stop_revision is not None and
729
stop_revision in self.revision_history()):
731
greedy_fetch(to_branch=self, from_branch=other,
732
revision=stop_revision)
733
pullable_revs = self.pullable_revisions(other, stop_revision)
734
if len(pullable_revs) > 0:
735
self.append_revision(*pullable_revs)
737
def pullable_revisions(self, other, stop_revision):
738
"""See Branch.pullable_revisions."""
739
other_revno = other.revision_id_to_revno(stop_revision)
741
return self.missing_revisions(other, other_revno)
742
except DivergedBranches, e:
744
pullable_revs = get_intervening_revisions(self.last_revision(),
747
assert self.last_revision() not in pullable_revs
749
except bzrlib.errors.NotAncestor:
750
if is_ancestor(self.last_revision(), stop_revision, self):
755
def basis_tree(self):
756
"""See Branch.basis_tree."""
758
revision_id = self.revision_history()[-1]
759
# FIXME: This is an abstraction violation, the basis tree
760
# here as defined is on the working tree, the method should
761
# be too. The basis tree for a branch can be different than
762
# that for a working tree. RBC 20051207
763
xml = self.working_tree().read_basis_inventory(revision_id)
764
inv = bzrlib.xml5.serializer_v5.read_inventory_from_string(xml)
765
return RevisionTree(self.repository, inv, revision_id)
766
except (IndexError, NoSuchFile, NoWorkingTree), e:
767
return self.repository.revision_tree(self.last_revision())
769
def working_tree(self):
770
"""See Branch.working_tree."""
771
from bzrlib.workingtree import WorkingTree
772
if self.base.find('://') != -1:
773
raise NoWorkingTree(self.base)
774
return WorkingTree(self.base, branch=self)
777
def pull(self, source, overwrite=False):
778
"""See Branch.pull."""
781
old_count = len(self.revision_history())
783
self.update_revisions(source)
784
except DivergedBranches:
788
self.set_revision_history(source.revision_history())
789
new_count = len(self.revision_history())
790
return new_count - old_count
794
def get_parent(self):
795
"""See Branch.get_parent."""
797
_locs = ['parent', 'pull', 'x-pull']
800
return self.control_files.get_utf8(l).read().strip('\n')
805
def get_push_location(self):
806
"""See Branch.get_push_location."""
807
config = bzrlib.config.BranchConfig(self)
808
push_loc = config.get_user_option('push_location')
811
def set_push_location(self, location):
812
"""See Branch.set_push_location."""
813
config = bzrlib.config.LocationConfig(self.base)
814
config.set_user_option('push_location', location)
817
def set_parent(self, url):
818
"""See Branch.set_parent."""
819
# TODO: Maybe delete old location files?
820
# URLs should never be unicode, even on the local fs,
821
# FIXUP this and get_parent in a future branch format bump:
822
# read and rewrite the file, and have the new format code read
823
# using .get not .get_utf8. RBC 20060125
824
self.control_files.put_utf8('parent', url + '\n')
826
def tree_config(self):
827
return TreeConfig(self)
829
def _get_truncated_history(self, revision_id):
830
history = self.revision_history()
831
if revision_id is None:
834
idx = history.index(revision_id)
836
raise InvalidRevisionId(revision_id=revision, branch=self)
837
return history[:idx+1]
840
def _clone_weave(self, to_location, revision=None, basis_branch=None):
841
assert isinstance(to_location, basestring)
842
if basis_branch is not None:
843
note("basis_branch is not supported for fast weave copy yet.")
845
history = self._get_truncated_history(revision)
846
if not bzrlib.osutils.lexists(to_location):
847
os.mkdir(to_location)
848
branch_to = Branch.initialize(to_location)
849
mutter("copy branch from %s to %s", self, branch_to)
850
branch_to.working_tree().set_root_id(self.get_root_id())
852
self.repository.copy(branch_to.repository)
854
# must be done *after* history is copied across
855
# FIXME duplicate code with base .clone().
856
# .. would template method be useful here. RBC 20051207
857
branch_to.set_parent(self.base)
858
branch_to.append_revision(*history)
859
# circular import protection
860
from bzrlib.merge import build_working_dir
861
build_working_dir(to_location)
865
def clone(self, to_location, revision=None, basis_branch=None, to_branch_type=None):
866
if to_branch_type is None:
867
to_branch_type = BzrBranch
869
if to_branch_type == BzrBranch \
870
and self.repository.weave_store.listable() \
871
and self.repository.revision_store.listable():
872
return self._clone_weave(to_location, revision, basis_branch)
874
return Branch.clone(self, to_location, revision, basis_branch, to_branch_type)
876
def fileid_involved_between_revs(self, from_revid, to_revid):
877
"""Find file_id(s) which are involved in the changes between revisions.
879
This determines the set of revisions which are involved, and then
880
finds all file ids affected by those revisions.
882
# TODO: jam 20060119 This code assumes that w.inclusions will
883
# always be correct. But because of the presence of ghosts
884
# it is possible to be wrong.
885
# One specific example from Robert Collins:
886
# Two branches, with revisions ABC, and AD
887
# C is a ghost merge of D.
888
# Inclusions doesn't recognize D as an ancestor.
889
# If D is ever merged in the future, the weave
890
# won't be fixed, because AD never saw revision C
891
# to cause a conflict which would force a reweave.
892
w = self.repository.get_inventory_weave()
893
from_set = set(w.inclusions([w.lookup(from_revid)]))
894
to_set = set(w.inclusions([w.lookup(to_revid)]))
895
included = to_set.difference(from_set)
896
changed = map(w.idx_to_name, included)
897
return self._fileid_involved_by_set(changed)
899
def fileid_involved(self, last_revid=None):
900
"""Find all file_ids modified in the ancestry of last_revid.
902
:param last_revid: If None, last_revision() will be used.
904
w = self.repository.get_inventory_weave()
906
changed = set(w._names)
908
included = w.inclusions([w.lookup(last_revid)])
909
changed = map(w.idx_to_name, included)
910
return self._fileid_involved_by_set(changed)
912
def fileid_involved_by_set(self, changes):
913
"""Find all file_ids modified by the set of revisions passed in.
915
:param changes: A set() of revision ids
917
# TODO: jam 20060119 This line does *nothing*, remove it.
918
# or better yet, change _fileid_involved_by_set so
919
# that it takes the inventory weave, rather than
920
# pulling it out by itself.
921
w = self.repository.get_inventory_weave()
922
return self._fileid_involved_by_set(changes)
924
def _fileid_involved_by_set(self, changes):
925
"""Find the set of file-ids affected by the set of revisions.
927
:param changes: A set() of revision ids.
928
:return: A set() of file ids.
930
This peaks at the Weave, interpreting each line, looking to
931
see if it mentions one of the revisions. And if so, includes
932
the file id mentioned.
933
This expects both the Weave format, and the serialization
934
to have a single line per file/directory, and to have
935
fileid="" and revision="" on that line.
937
assert self._branch_format in (5, 6), \
938
"fileid_involved only supported for branches which store inventory as xml"
940
w = self.repository.get_inventory_weave()
942
for line in w._weave:
944
# it is ugly, but it is due to the weave structure
945
if not isinstance(line, basestring): continue
947
start = line.find('file_id="')+9
948
if start < 9: continue
949
end = line.find('"', start)
951
file_id = xml.sax.saxutils.unescape(line[start:end])
953
# check if file_id is already present
954
if file_id in file_ids: continue
956
start = line.find('revision="')+10
957
if start < 10: continue
958
end = line.find('"', start)
960
revision_id = xml.sax.saxutils.unescape(line[start:end])
962
if revision_id in changes:
963
file_ids.add(file_id)
968
class ScratchBranch(BzrBranch):
969
"""Special test class: a branch that cleans up after itself.
971
>>> b = ScratchBranch()
975
>>> b._transport.__del__()
980
def __init__(self, files=[], dirs=[], transport=None):
981
"""Make a test branch.
983
This creates a temporary directory and runs init-tree in it.
985
If any files are listed, they are created in the working copy.
987
if transport is None:
988
transport = bzrlib.transport.local.ScratchTransport()
989
super(ScratchBranch, self).__init__(transport, init=True)
991
super(ScratchBranch, self).__init__(transport)
993
# BzrBranch creates a clone to .bzr and then forgets about the
994
# original transport. A ScratchTransport() deletes itself and
995
# everything underneath it when it goes away, so we need to
996
# grab a local copy to prevent that from happening
997
self._transport = transport
1000
self._transport.mkdir(d)
1003
self._transport.put(f, 'content of %s' % f)
1007
>>> orig = ScratchBranch(files=["file1", "file2"])
1008
>>> os.listdir(orig.base)
1009
[u'.bzr', u'file1', u'file2']
1010
>>> clone = orig.clone()
1011
>>> if os.name != 'nt':
1012
... os.path.samefile(orig.base, clone.base)
1014
... orig.base == clone.base
1017
>>> os.listdir(clone.base)
1018
[u'.bzr', u'file1', u'file2']
1020
from shutil import copytree
1021
from bzrlib.osutils import mkdtemp
1024
copytree(self.base, base, symlinks=True)
1025
return ScratchBranch(
1026
transport=bzrlib.transport.local.ScratchTransport(base))
1029
######################################################################
1033
def is_control_file(filename):
1034
## FIXME: better check
1035
filename = normpath(filename)
1036
while filename != '':
1037
head, tail = os.path.split(filename)
1038
## mutter('check %r for control file' % ((head, tail),))
1039
if tail == bzrlib.BZRDIR:
1041
if filename == head: