15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18
from copy import deepcopy
19
from cStringIO import StringIO
24
from unittest import TestSuite
25
from warnings import warn
27
import xml.sax.saxutils
29
raise ImportError("We were unable to import 'xml.sax.saxutils',"
30
" most likely you have an xml.pyc or xml.pyo file"
31
" lying around in your bzrlib directory."
33
from cStringIO import StringIO
22
from bzrlib.trace import mutter, note
23
from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, \
25
sha_file, appendpath, file_kind
37
from bzrlib.config import TreeConfig
38
from bzrlib.decorators import needs_read_lock, needs_write_lock
39
from bzrlib.delta import compare_trees
40
import bzrlib.errors as errors
27
41
from bzrlib.errors import (BzrError, InvalidRevisionNumber, InvalidRevisionId,
42
NoSuchRevision, HistoryMissing, NotBranchError,
43
DivergedBranches, LockError,
44
UninitializableFormat,
46
UnlistableBranch, NoSuchFile, NotVersionedError,
48
import bzrlib.inventory as inventory
49
from bzrlib.inventory import Inventory
50
from bzrlib.lockable_files import LockableFiles
51
from bzrlib.osutils import (isdir, quotefn,
52
rename, splitpath, sha_file,
53
file_kind, abspath, normpath, pathjoin,
29
56
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
33
from bzrlib.inventory import Inventory
34
from bzrlib.weavestore import WeaveStore
57
from bzrlib.trace import mutter, note
58
from bzrlib.tree import EmptyTree, RevisionTree
59
from bzrlib.repository import Repository
60
from bzrlib.revision import (Revision, is_ancestor, get_intervening_revisions)
61
from bzrlib.store import copy_all
62
from bzrlib.symbol_versioning import *
63
import bzrlib.transactions as transactions
64
from bzrlib.transport import Transport, get_transport
65
from bzrlib.tree import EmptyTree, RevisionTree
40
70
BZR_BRANCH_FORMAT_4 = "Bazaar-NG branch, format 0.0.4\n"
41
71
BZR_BRANCH_FORMAT_5 = "Bazaar-NG branch, format 5\n"
42
## TODO: Maybe include checks for common corruption of newlines, etc?
72
BZR_BRANCH_FORMAT_6 = "Bazaar-NG branch, format 6\n"
75
# TODO: Maybe include checks for common corruption of newlines, etc?
45
77
# TODO: Some operations like log might retrieve the same revisions
46
78
# repeatedly to calculate deltas. We could perhaps have a weakref
47
# cache in memory to make this faster.
49
# TODO: please move the revision-string syntax stuff out of the branch
50
# object; it's clutter
53
def find_branch(f, **args):
54
if f and (f.startswith('http://') or f.startswith('https://')):
56
return remotebranch.RemoteBranch(f, **args)
58
return Branch(f, **args)
61
def find_cached_branch(f, cache_root, **args):
62
from remotebranch import RemoteBranch
63
br = find_branch(f, **args)
64
def cacheify(br, store_name):
65
from meta_store import CachedStore
66
cache_path = os.path.join(cache_root, store_name)
68
new_store = CachedStore(getattr(br, store_name), cache_path)
69
setattr(br, store_name, new_store)
71
if isinstance(br, RemoteBranch):
72
cacheify(br, 'inventory_store')
73
cacheify(br, 'text_store')
74
cacheify(br, 'revision_store')
78
def _relpath(base, path):
79
"""Return path relative to base, or raise exception.
81
The path may be either an absolute path or a path relative to the
82
current working directory.
84
Lifted out of Branch.relpath for ease of testing.
86
os.path.commonprefix (python2.4) has a bad bug that it works just
87
on string prefixes, assuming that '/u' is a prefix of '/u2'. This
88
avoids that problem."""
89
rp = os.path.abspath(path)
93
while len(head) >= len(base):
96
head, tail = os.path.split(head)
100
from errors import NotBranchError
101
raise NotBranchError("path %r is not within branch %r" % (rp, base))
103
return os.sep.join(s)
106
def find_branch_root(f=None):
107
"""Find the branch root enclosing f, or pwd.
109
f may be a filename or a URL.
111
It is not necessary that f exists.
113
Basically we keep looking up until we find the control directory or
114
run into the root. If there isn't one, raises NotBranchError.
118
elif hasattr(os.path, 'realpath'):
119
f = os.path.realpath(f)
121
f = os.path.abspath(f)
122
if not os.path.exists(f):
123
raise BzrError('%r does not exist' % f)
129
if os.path.exists(os.path.join(f, bzrlib.BZRDIR)):
131
head, tail = os.path.split(f)
133
# reached the root, whatever that may be
134
raise bzrlib.errors.NotBranchError('%s is not in a branch' % orig_f)
139
# XXX: move into bzrlib.errors; subclass BzrError
140
class DivergedBranches(Exception):
141
def __init__(self, branch1, branch2):
142
self.branch1 = branch1
143
self.branch2 = branch2
144
Exception.__init__(self, "These branches have diverged.")
79
# cache in memory to make this faster. In general anything can be
80
# cached in memory between lock and unlock operations. .. nb thats
81
# what the transaction identity map provides
147
84
######################################################################
151
88
"""Branch holding a history of revisions.
154
Base directory of the branch.
160
If _lock_mode is true, a positive count of the number of times the
164
Lock object from bzrlib.lock.
91
Base directory/url of the branch.
93
# this is really an instance variable - FIXME move it there
171
# Map some sort of prefix into a namespace
172
# stuff like "revno:10", "revid:", etc.
173
# This should match a prefix with a function which accepts
174
REVISION_NAMESPACES = {}
176
def __init__(self, base, init=False, find_root=True):
177
"""Create new branch object at a particular location.
179
base -- Base directory for the branch.
181
init -- If True, create new control files in a previously
182
unversioned directory. If False, the branch must already
185
find_root -- If true and init is false, find the root of the
186
existing branch containing base.
188
In the test suite, creation of new trees is tested using the
189
`ScratchBranch` class.
191
from bzrlib.store import ImmutableStore
193
self.base = os.path.realpath(base)
196
self.base = find_branch_root(base)
198
self.base = os.path.realpath(base)
199
if not isdir(self.controlfilename('.')):
200
from errors import NotBranchError
201
raise NotBranchError("not a bzr branch: %s" % quotefn(base),
202
['use "bzr init" to initialize a new working tree',
203
'current bzr can only operate from top-of-tree'])
206
self.weave_store = WeaveStore(self.controlfilename('weaves'))
207
self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
208
self.inventory_store = ImmutableStore(self.controlfilename('inventory-store'))
212
return '%s(%r)' % (self.__class__.__name__, self.base)
219
if self._lock_mode or self._lock:
220
from warnings import warn
221
warn("branch %r was not explicitly unlocked" % self)
97
_default_initializer = None
98
"""The default initializer for making new branches."""
100
def __init__(self, *ignored, **ignored_too):
101
raise NotImplementedError('The Branch class is abstract')
104
def open_downlevel(base):
105
"""Open a branch which may be of an old format."""
106
return Branch.open(base, _unsupported=True)
109
def open(base, _unsupported=False):
110
"""Open an existing branch, rooted at 'base' (url)
112
_unsupported is a private parameter to the Branch class.
114
t = get_transport(base)
115
mutter("trying to open %r with transport %r", base, t)
116
format = BzrBranchFormat.find_format(t)
117
if not _unsupported and not format.is_supported():
118
# see open_downlevel to open legacy branches.
119
raise errors.UnsupportedFormatError(
120
'sorry, branch format %s not supported' % format,
121
['use a different bzr version',
122
'or remove the .bzr directory'
123
' and "bzr init" again'])
124
return format.open(t)
127
def open_containing(url):
128
"""Open an existing branch which contains url.
130
This probes for a branch at url, and searches upwards from there.
132
Basically we keep looking up until we find the control directory or
133
run into the root. If there isn't one, raises NotBranchError.
134
If there is one and it is either an unrecognised format or an unsupported
135
format, UnknownFormatError or UnsupportedFormatError are raised.
136
If there is one, it is returned, along with the unused portion of url.
138
t = get_transport(url)
139
# this gets the normalised url back. I.e. '.' -> the full path.
143
format = BzrBranchFormat.find_format(t)
144
return format.open(t), t.relpath(url)
145
except NotBranchError, e:
146
mutter('not a branch in: %r %s', t.base, e)
147
new_t = t.clone('..')
148
if new_t.base == t.base:
149
# reached the root, whatever that may be
150
raise NotBranchError(path=url)
155
"""Create a new Branch at the url 'bzr'.
157
This will call the current default initializer with base
158
as the only parameter.
160
return Branch._default_initializer(safe_unicode(base))
163
@deprecated_function(zero_eight)
164
def initialize(base):
165
"""Create a new working tree and branch, rooted at 'base' (url)
167
NOTE: This will soon be deprecated in favour of creation
170
# imported here to prevent scope creep as this is going.
171
from bzrlib.workingtree import WorkingTree
172
return WorkingTree.create_standalone(safe_unicode(base)).branch
175
def get_default_initializer():
176
"""Return the initializer being used for new branches."""
177
return Branch._default_initializer
180
def set_default_initializer(initializer):
181
"""Set the initializer to be used for new branches."""
182
Branch._default_initializer = staticmethod(initializer)
184
def setup_caching(self, cache_root):
185
"""Subclasses that care about caching should override this, and set
186
up cached stores located under cache_root.
188
# seems to be unused, 2006-01-13 mbp
189
warn('%s is deprecated' % self.setup_caching)
190
self.cache_root = cache_root
193
cfg = self.tree_config()
194
return cfg.get_option(u"nickname", default=self.base.split('/')[-2])
196
def _set_nick(self, nick):
197
cfg = self.tree_config()
198
cfg.set_option(nick, "nickname")
199
assert cfg.get_option("nickname") == nick
201
nick = property(_get_nick, _set_nick)
203
def push_stores(self, branch_to):
204
"""Copy the content of this branches store to branch_to."""
205
raise NotImplementedError('push_stores is abstract')
225
207
def lock_write(self):
227
if self._lock_mode != 'w':
228
from errors import LockError
229
raise LockError("can't upgrade to a write lock from %r" %
231
self._lock_count += 1
233
from bzrlib.lock import WriteLock
235
self._lock = WriteLock(self.controlfilename('branch-lock'))
236
self._lock_mode = 'w'
208
raise NotImplementedError('lock_write is abstract')
240
210
def lock_read(self):
242
assert self._lock_mode in ('r', 'w'), \
243
"invalid lock mode %r" % self._lock_mode
244
self._lock_count += 1
246
from bzrlib.lock import ReadLock
211
raise NotImplementedError('lock_read is abstract')
248
self._lock = ReadLock(self.controlfilename('branch-lock'))
249
self._lock_mode = 'r'
252
213
def unlock(self):
253
if not self._lock_mode:
254
from errors import LockError
255
raise LockError('branch %r is not locked' % (self))
214
raise NotImplementedError('unlock is abstract')
257
if self._lock_count > 1:
258
self._lock_count -= 1
262
self._lock_mode = self._lock_count = None
216
def peek_lock_mode(self):
217
"""Return lock mode for the Branch: 'r', 'w' or None"""
218
raise NotImplementedError(self.peek_lock_mode)
264
220
def abspath(self, name):
265
"""Return absolute filename for something in the branch"""
266
return os.path.join(self.base, name)
268
def relpath(self, path):
269
"""Return path relative to this branch of something inside it.
271
Raises an error if path is not in this branch."""
272
return _relpath(self.base, path)
274
def controlfilename(self, file_or_path):
275
"""Return location relative to branch."""
276
if isinstance(file_or_path, basestring):
277
file_or_path = [file_or_path]
278
return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
281
def controlfile(self, file_or_path, mode='r'):
282
"""Open a control file for this branch.
284
There are two classes of file in the control directory: text
285
and binary. binary files are untranslated byte streams. Text
286
control files are stored with Unix newlines and in UTF-8, even
287
if the platform or locale defaults are different.
289
Controlfiles should almost never be opened in write mode but
290
rather should be atomically copied and replaced using atomicfile.
293
fn = self.controlfilename(file_or_path)
295
if mode == 'rb' or mode == 'wb':
296
return file(fn, mode)
297
elif mode == 'r' or mode == 'w':
298
# open in binary mode anyhow so there's no newline translation;
299
# codecs uses line buffering by default; don't want that.
301
return codecs.open(fn, mode + 'b', 'utf-8',
304
raise BzrError("invalid controlfile mode %r" % mode)
306
def _make_control(self):
307
os.mkdir(self.controlfilename([]))
308
self.controlfile('README', 'w').write(
309
"This is a Bazaar-NG control directory.\n"
310
"Do not change any files in this directory.\n")
311
self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT_5)
312
for d in ('text-store', 'inventory-store', 'revision-store',
314
os.mkdir(self.controlfilename(d))
315
for f in ('revision-history', 'merged-patches',
316
'pending-merged-patches', 'branch-name',
319
self.controlfile(f, 'w').write('')
320
mutter('created control directory in ' + self.base)
322
# if we want per-tree root ids then this is the place to set
323
# them; they're not needed for now and so ommitted for
325
f = self.controlfile('inventory','w')
326
bzrlib.xml5.serializer_v5.write_inventory(Inventory(), f)
329
def _check_format(self):
330
"""Check this branch format is supported.
332
The format level is stored, as an integer, in
333
self._branch_format for code that needs to check it later.
335
In the future, we might need different in-memory Branch
336
classes to support downlevel branches. But not yet.
338
fmt = self.controlfile('branch-format', 'r').read()
339
if fmt == BZR_BRANCH_FORMAT_5:
340
self._branch_format = 5
342
raise BzrError('sorry, branch format "%s" not supported; '
343
'use a different bzr version, '
344
'or run "bzr upgrade", '
345
'or remove the .bzr directory and "bzr init" again'
346
% fmt.rstrip('\n\r'))
221
"""Return absolute filename for something in the branch
223
XXX: Robert Collins 20051017 what is this used for? why is it a branch
224
method and not a tree method.
226
raise NotImplementedError('abspath is abstract')
348
228
def get_root_id(self):
349
229
"""Return the id of this branches root"""
350
inv = self.read_working_inventory()
351
return inv.root.file_id
353
def set_root_id(self, file_id):
354
inv = self.read_working_inventory()
355
orig_root_id = inv.root.file_id
356
del inv._byid[inv.root.file_id]
357
inv.root.file_id = file_id
358
inv._byid[inv.root.file_id] = inv.root
361
if entry.parent_id in (None, orig_root_id):
362
entry.parent_id = inv.root.file_id
363
self._write_inventory(inv)
365
def read_working_inventory(self):
366
"""Read the working inventory."""
369
# ElementTree does its own conversion from UTF-8, so open in
371
f = self.controlfile('inventory', 'rb')
372
return bzrlib.xml5.serializer_v5.read_inventory(f)
377
def _write_inventory(self, inv):
378
"""Update the working inventory.
380
That is to say, the inventory describing changes underway, that
381
will be committed to the next revision.
383
from bzrlib.atomicfile import AtomicFile
387
f = AtomicFile(self.controlfilename('inventory'), 'wb')
389
bzrlib.xml5.serializer_v5.write_inventory(inv, f)
396
mutter('wrote working inventory')
399
inventory = property(read_working_inventory, _write_inventory, None,
400
"""Inventory for the working copy.""")
403
def add(self, files, ids=None):
404
"""Make files versioned.
406
Note that the command line normally calls smart_add instead,
407
which can automatically recurse.
409
This puts the files in the Added state, so that they will be
410
recorded by the next commit.
413
List of paths to add, relative to the base of the tree.
416
If set, use these instead of automatically generated ids.
417
Must be the same length as the list of files, but may
418
contain None for ids that are to be autogenerated.
420
TODO: Perhaps have an option to add the ids even if the files do
423
TODO: Perhaps yield the ids and paths as they're added.
425
# TODO: Re-adding a file that is removed in the working copy
426
# should probably put it back with the previous ID.
427
if isinstance(files, basestring):
428
assert(ids is None or isinstance(ids, basestring))
434
ids = [None] * len(files)
436
assert(len(ids) == len(files))
440
inv = self.read_working_inventory()
441
for f,file_id in zip(files, ids):
442
if is_control_file(f):
443
raise BzrError("cannot add control file %s" % quotefn(f))
448
raise BzrError("cannot add top-level %r" % f)
450
fullpath = os.path.normpath(self.abspath(f))
453
kind = file_kind(fullpath)
455
# maybe something better?
456
raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
458
if kind != 'file' and kind != 'directory':
459
raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
462
file_id = gen_file_id(f)
463
inv.add_path(f, kind=kind, file_id=file_id)
465
mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
467
self._write_inventory(inv)
472
def print_file(self, file, revno):
230
raise NotImplementedError('get_root_id is abstract')
232
def print_file(self, file, revision_id):
473
233
"""Print `file` to stdout."""
476
tree = self.revision_tree(self.lookup_revision(revno))
477
# use inventory as it was in that revision
478
file_id = tree.inventory.path2id(file)
480
raise BzrError("%r is not present in revision %s" % (file, revno))
481
tree.print_file(file_id)
486
def remove(self, files, verbose=False):
487
"""Mark nominated files for removal from the inventory.
489
This does not remove their text. This does not run on
491
TODO: Refuse to remove modified files unless --force is given?
493
TODO: Do something useful with directories.
495
TODO: Should this remove the text or not? Tough call; not
496
removing may be useful and the user can just use use rm, and
497
is the opposite of add. Removing it is consistent with most
498
other tools. Maybe an option.
500
## TODO: Normalize names
501
## TODO: Remove nested loops; better scalability
502
if isinstance(files, basestring):
508
tree = self.working_tree()
511
# do this before any modifications
515
raise BzrError("cannot remove unversioned file %s" % quotefn(f))
516
mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
518
# having remove it, it must be either ignored or unknown
519
if tree.is_ignored(f):
523
show_status(new_status, inv[fid].kind, quotefn(f))
526
self._write_inventory(inv)
531
# FIXME: this doesn't need to be a branch method
532
def set_inventory(self, new_inventory_list):
533
from bzrlib.inventory import Inventory, InventoryEntry
534
inv = Inventory(self.get_root_id())
535
for path, file_id, parent, kind in new_inventory_list:
536
name = os.path.basename(path)
539
inv.add(InventoryEntry(file_id, name, kind, parent))
540
self._write_inventory(inv)
544
"""Return all unknown files.
546
These are files in the working directory that are not versioned or
547
control files or ignored.
549
>>> b = ScratchBranch(files=['foo', 'foo~'])
550
>>> list(b.unknowns())
553
>>> list(b.unknowns())
556
>>> list(b.unknowns())
559
return self.working_tree().unknowns()
234
raise NotImplementedError('print_file is abstract')
562
236
def append_revision(self, *revision_ids):
563
from bzrlib.atomicfile import AtomicFile
565
for revision_id in revision_ids:
566
mutter("add {%s} to revision-history" % revision_id)
568
rev_history = self.revision_history()
569
rev_history.extend(revision_ids)
571
f = AtomicFile(self.controlfilename('revision-history'))
573
for rev_id in rev_history:
580
def get_revision_xml_file(self, revision_id):
581
"""Return XML file object for revision object."""
582
if not revision_id or not isinstance(revision_id, basestring):
583
raise InvalidRevisionId(revision_id)
588
return self.revision_store[revision_id]
590
raise bzrlib.errors.NoSuchRevision(self, revision_id)
596
get_revision_xml = get_revision_xml_file
599
def get_revision(self, revision_id):
600
"""Return the Revision object for a named revision"""
601
xml_file = self.get_revision_xml_file(revision_id)
604
r = bzrlib.xml5.serializer_v5.read_revision(xml_file)
605
except SyntaxError, e:
606
raise bzrlib.errors.BzrError('failed to unpack revision_xml',
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)
637
def get_revision_sha1(self, revision_id):
638
"""Hash the stored value of a revision, and return it."""
639
# In the future, revision entries will be signed. At that
640
# point, it is probably best *not* to include the signature
641
# in the revision hash. Because that lets you re-sign
642
# the revision, (add signatures/remove signatures) and still
643
# have all hash pointers stay consistent.
644
# But for now, just hash the contents.
645
return bzrlib.osutils.sha_file(self.get_revision_xml(revision_id))
648
def get_inventory(self, revision_id):
649
"""Get Inventory object by hash.
651
TODO: Perhaps for this and similar methods, take a revision
652
parameter which can be either an integer revno or a
654
f = self.get_inventory_xml_file(revision_id)
655
return bzrlib.xml5.serializer_v5.read_inventory(f)
658
def get_inventory_xml(self, revision_id):
659
"""Get inventory XML as a file object."""
661
assert isinstance(revision_id, basestring), type(revision_id)
662
return self.inventory_store[revision_id]
664
raise bzrlib.errors.HistoryMissing(self, 'inventory', revision_id)
666
get_inventory_xml_file = get_inventory_xml
669
def get_inventory_sha1(self, revision_id):
670
"""Return the sha1 hash of the inventory entry
672
return sha_file(self.get_inventory_xml_file(revision_id))
675
def get_revision_inventory(self, revision_id):
676
"""Return inventory of a past revision."""
677
# bzr 0.0.6 imposes the constraint that the inventory_id
678
# must be the same as its revision, so this is trivial.
679
if revision_id == None:
680
return Inventory(self.get_root_id())
682
return self.get_inventory(revision_id)
237
raise NotImplementedError('append_revision is abstract')
239
def set_revision_history(self, rev_history):
240
raise NotImplementedError('set_revision_history is abstract')
685
242
def revision_history(self):
686
"""Return sequence of revision hashes on to this branch.
688
>>> ScratchBranch().revision_history()
693
return [l.rstrip('\r\n') for l in
694
self.controlfile('revision-history', 'r').readlines()]
699
def common_ancestor(self, other, self_revno=None, other_revno=None):
702
>>> sb = ScratchBranch(files=['foo', 'foo~'])
703
>>> sb.common_ancestor(sb) == (None, None)
705
>>> commit.commit(sb, "Committing first revision", verbose=False)
706
>>> sb.common_ancestor(sb)[0]
708
>>> clone = sb.clone()
709
>>> commit.commit(sb, "Committing second revision", verbose=False)
710
>>> sb.common_ancestor(sb)[0]
712
>>> sb.common_ancestor(clone)[0]
714
>>> commit.commit(clone, "Committing divergent second revision",
716
>>> sb.common_ancestor(clone)[0]
718
>>> sb.common_ancestor(clone) == clone.common_ancestor(sb)
720
>>> sb.common_ancestor(sb) != clone.common_ancestor(clone)
722
>>> clone2 = sb.clone()
723
>>> sb.common_ancestor(clone2)[0]
725
>>> sb.common_ancestor(clone2, self_revno=1)[0]
727
>>> sb.common_ancestor(clone2, other_revno=1)[0]
730
my_history = self.revision_history()
731
other_history = other.revision_history()
732
if self_revno is None:
733
self_revno = len(my_history)
734
if other_revno is None:
735
other_revno = len(other_history)
736
indices = range(min((self_revno, other_revno)))
739
if my_history[r] == other_history[r]:
740
return r+1, my_history[r]
243
"""Return sequence of revision hashes on to this branch."""
244
raise NotImplementedError('revision_history is abstract')
745
247
"""Return current revision number for this branch.
915
330
raise bzrlib.errors.NoSuchRevision(self, revno)
916
331
return history[revno - 1]
918
def _get_revision_info(self, revision):
919
"""Return (revno, revision id) for revision specifier.
921
revision can be an integer, in which case it is assumed to be revno
922
(though this will translate negative values into positive ones)
923
revision can also be a string, in which case it is parsed for something
924
like 'date:' or 'revid:' etc.
926
A revid is always returned. If it is None, the specifier referred to
927
the null revision. If the revid does not occur in the revision
928
history, revno will be None.
934
try:# Convert to int if possible
935
revision = int(revision)
938
revs = self.revision_history()
939
if isinstance(revision, int):
941
revno = len(revs) + revision + 1
944
rev_id = self.get_rev_id(revno, revs)
945
elif isinstance(revision, basestring):
946
for prefix, func in Branch.REVISION_NAMESPACES.iteritems():
947
if revision.startswith(prefix):
948
result = func(self, revs, revision)
950
revno, rev_id = result
953
rev_id = self.get_rev_id(revno, revs)
956
raise BzrError('No namespace registered for string: %r' %
959
raise TypeError('Unhandled revision type %s' % revision)
963
raise bzrlib.errors.NoSuchRevision(self, revision)
966
def _namespace_revno(self, revs, revision):
967
"""Lookup a revision by revision number"""
968
assert revision.startswith('revno:')
970
return (int(revision[6:]),)
973
REVISION_NAMESPACES['revno:'] = _namespace_revno
975
def _namespace_revid(self, revs, revision):
976
assert revision.startswith('revid:')
977
rev_id = revision[len('revid:'):]
979
return revs.index(rev_id) + 1, rev_id
982
REVISION_NAMESPACES['revid:'] = _namespace_revid
984
def _namespace_last(self, revs, revision):
985
assert revision.startswith('last:')
987
offset = int(revision[5:])
992
raise BzrError('You must supply a positive value for --revision last:XXX')
993
return (len(revs) - offset + 1,)
994
REVISION_NAMESPACES['last:'] = _namespace_last
996
def _namespace_tag(self, revs, revision):
997
assert revision.startswith('tag:')
998
raise BzrError('tag: namespace registered, but not implemented.')
999
REVISION_NAMESPACES['tag:'] = _namespace_tag
1001
def _namespace_date(self, revs, revision):
1002
assert revision.startswith('date:')
1004
# Spec for date revisions:
1006
# value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
1007
# it can also start with a '+/-/='. '+' says match the first
1008
# entry after the given date. '-' is match the first entry before the date
1009
# '=' is match the first entry after, but still on the given date.
1011
# +2005-05-12 says find the first matching entry after May 12th, 2005 at 0:00
1012
# -2005-05-12 says find the first matching entry before May 12th, 2005 at 0:00
1013
# =2005-05-12 says find the first match after May 12th, 2005 at 0:00 but before
1014
# May 13th, 2005 at 0:00
1016
# So the proper way of saying 'give me all entries for today' is:
1017
# -r {date:+today}:{date:-tomorrow}
1018
# The default is '=' when not supplied
1021
if val[:1] in ('+', '-', '='):
1022
match_style = val[:1]
1025
today = datetime.datetime.today().replace(hour=0,minute=0,second=0,microsecond=0)
1026
if val.lower() == 'yesterday':
1027
dt = today - datetime.timedelta(days=1)
1028
elif val.lower() == 'today':
1030
elif val.lower() == 'tomorrow':
1031
dt = today + datetime.timedelta(days=1)
1034
# This should be done outside the function to avoid recompiling it.
1035
_date_re = re.compile(
1036
r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
1038
r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
1040
m = _date_re.match(val)
1041
if not m or (not m.group('date') and not m.group('time')):
1042
raise BzrError('Invalid revision date %r' % revision)
1045
year, month, day = int(m.group('year')), int(m.group('month')), int(m.group('day'))
1047
year, month, day = today.year, today.month, today.day
1049
hour = int(m.group('hour'))
1050
minute = int(m.group('minute'))
1051
if m.group('second'):
1052
second = int(m.group('second'))
1056
hour, minute, second = 0,0,0
1058
dt = datetime.datetime(year=year, month=month, day=day,
1059
hour=hour, minute=minute, second=second)
1063
if match_style == '-':
1065
elif match_style == '=':
1066
last = dt + datetime.timedelta(days=1)
1069
for i in range(len(revs)-1, -1, -1):
1070
r = self.get_revision(revs[i])
1071
# TODO: Handle timezone.
1072
dt = datetime.datetime.fromtimestamp(r.timestamp)
1073
if first >= dt and (last is None or dt >= last):
1076
for i in range(len(revs)):
1077
r = self.get_revision(revs[i])
1078
# TODO: Handle timezone.
1079
dt = datetime.datetime.fromtimestamp(r.timestamp)
1080
if first <= dt and (last is None or dt <= last):
1082
REVISION_NAMESPACES['date:'] = _namespace_date
1084
def revision_tree(self, revision_id):
1085
"""Return Tree for a revision on this branch.
1087
`revision_id` may be None for the null revision, in which case
1088
an `EmptyTree` is returned."""
1089
# TODO: refactor this to use an existing revision object
1090
# so we don't need to read it in twice.
1091
if revision_id == None:
1094
inv = self.get_revision_inventory(revision_id)
1095
return RevisionTree(self.weave_store, inv, revision_id)
1098
333
def working_tree(self):
1099
"""Return a `Tree` for the working copy."""
1100
from workingtree import WorkingTree
1101
return WorkingTree(self.base, self.read_working_inventory())
334
"""Return a `Tree` for the working copy if this is a local branch."""
335
raise NotImplementedError('working_tree is abstract')
337
def pull(self, source, overwrite=False):
338
raise NotImplementedError('pull is abstract')
1104
340
def basis_tree(self):
1105
341
"""Return `Tree` object for last revision.
1107
343
If there are no revisions yet, return an `EmptyTree`.
1109
return self.revision_tree(self.last_patch())
345
return self.repository.revision_tree(self.last_revision())
1112
347
def rename_one(self, from_rel, to_rel):
1113
348
"""Rename one file.
1115
350
This can change the directory or the filename or both.
1119
tree = self.working_tree()
1120
inv = tree.inventory
1121
if not tree.has_filename(from_rel):
1122
raise BzrError("can't rename: old working file %r does not exist" % from_rel)
1123
if tree.has_filename(to_rel):
1124
raise BzrError("can't rename: new working file %r already exists" % to_rel)
1126
file_id = inv.path2id(from_rel)
1128
raise BzrError("can't rename: old name %r is not versioned" % from_rel)
1130
if inv.path2id(to_rel):
1131
raise BzrError("can't rename: new name %r is already versioned" % to_rel)
1133
to_dir, to_tail = os.path.split(to_rel)
1134
to_dir_id = inv.path2id(to_dir)
1135
if to_dir_id == None and to_dir != '':
1136
raise BzrError("can't determine destination directory id for %r" % to_dir)
1138
mutter("rename_one:")
1139
mutter(" file_id {%s}" % file_id)
1140
mutter(" from_rel %r" % from_rel)
1141
mutter(" to_rel %r" % to_rel)
1142
mutter(" to_dir %r" % to_dir)
1143
mutter(" to_dir_id {%s}" % to_dir_id)
1145
inv.rename(file_id, to_dir_id, to_tail)
1147
from_abs = self.abspath(from_rel)
1148
to_abs = self.abspath(to_rel)
1150
os.rename(from_abs, to_abs)
1152
raise BzrError("failed to rename %r to %r: %s"
1153
% (from_abs, to_abs, e[1]),
1154
["rename rolled back"])
1156
self._write_inventory(inv)
352
raise NotImplementedError('rename_one is abstract')
1161
354
def move(self, from_paths, to_name):
1162
355
"""Rename files.
1362
403
if revno < 1 or revno > self.revno():
1363
404
raise InvalidRevisionNumber(revno)
1368
class ScratchBranch(Branch):
406
def clone(self, to_location, revision=None, basis_branch=None, to_branch_type=None):
407
"""Copy this branch into the existing directory to_location.
409
Returns the newly created branch object.
412
If not None, only revisions up to this point will be copied.
413
The head of the new branch will be that revision. Must be a
416
to_location -- The destination directory; must either exist and be
417
empty, or not exist, in which case it is created.
420
A local branch to copy revisions from, related to this branch.
421
This is used when branching from a remote (slow) branch, and we have
422
a local branch that might contain some relevant revisions.
425
Branch type of destination branch
427
from bzrlib.workingtree import WorkingTree
428
assert isinstance(to_location, basestring)
429
if not bzrlib.osutils.lexists(to_location):
430
os.mkdir(to_location)
431
if to_branch_type is None:
432
to_branch_type = BzrBranch
433
# FIXME use a branch format here
434
br_to = to_branch_type.initialize(to_location)
435
mutter("copy branch from %s to %s", self, br_to)
436
if basis_branch is not None:
437
basis_branch.push_stores(br_to)
439
revision = self.last_revision()
440
br_to.update_revisions(self, stop_revision=revision)
441
br_to.set_parent(self.base)
442
WorkingTree.create(br_to, to_location).set_root_id(self.get_root_id())
446
def fileid_involved_between_revs(self, from_revid, to_revid):
447
""" This function returns the file_id(s) involved in the
448
changes between the from_revid revision and the to_revid
451
raise NotImplementedError('fileid_involved_between_revs is abstract')
453
def fileid_involved(self, last_revid=None):
454
""" This function returns the file_id(s) involved in the
455
changes up to the revision last_revid
456
If no parametr is passed, then all file_id[s] present in the
457
repository are returned
459
raise NotImplementedError('fileid_involved is abstract')
461
def fileid_involved_by_set(self, changes):
462
""" This function returns the file_id(s) involved in the
463
changes present in the set 'changes'
465
raise NotImplementedError('fileid_involved_by_set is abstract')
467
def fileid_involved_between_revs(self, from_revid, to_revid):
468
""" This function returns the file_id(s) involved in the
469
changes between the from_revid revision and the to_revid
472
raise NotImplementedError('fileid_involved_between_revs is abstract')
474
def fileid_involved(self, last_revid=None):
475
""" This function returns the file_id(s) involved in the
476
changes up to the revision last_revid
477
If no parametr is passed, then all file_id[s] present in the
478
repository are returned
480
raise NotImplementedError('fileid_involved is abstract')
482
def fileid_involved_by_set(self, changes):
483
""" This function returns the file_id(s) involved in the
484
changes present in the set 'changes'
486
raise NotImplementedError('fileid_involved_by_set is abstract')
488
class BzrBranchFormat(object):
489
"""An encapsulation of the initialization and open routines for a format.
491
Formats provide three things:
492
* An initialization routine,
496
Formats are placed in an dict by their format string for reference
497
during branch opening. Its not required that these be instances, they
498
can be classes themselves with class methods - it simply depends on
499
whether state is needed for a given format or not.
501
Once a format is deprecated, just deprecate the initialize and open
502
methods on the format class. Do not deprecate the object, as the
503
object will be created every time regardless.
507
"""The known formats."""
510
def find_format(klass, transport):
511
"""Return the format registered for URL."""
513
format_string = transport.get(".bzr/branch-format").read()
514
return klass._formats[format_string]
516
raise NotBranchError(path=transport.base)
518
raise errors.UnknownFormatError(format_string)
520
def get_format_string(self):
521
"""Return the ASCII format string that identifies this format."""
522
raise NotImplementedError(self.get_format_string)
524
def _find_modes(self, t):
525
"""Determine the appropriate modes for files and directories.
527
FIXME: When this merges into, or from storage,
528
this code becomes delgatable to a LockableFiles instance.
530
For now its cribbed and returns (dir_mode, file_mode)
534
except errors.TransportNotPossible:
538
dir_mode = st.st_mode & 07777
539
# Remove the sticky and execute bits for files
540
file_mode = dir_mode & ~07111
541
if not BzrBranch._set_dir_mode:
543
if not BzrBranch._set_file_mode:
545
return dir_mode, file_mode
547
def initialize(self, url):
548
"""Create a branch of this format at url and return an open branch."""
549
t = get_transport(url)
550
from bzrlib.weavefile import write_weave_v5
551
from bzrlib.weave import Weave
553
# Create an empty weave
555
bzrlib.weavefile.write_weave_v5(Weave(), sio)
556
empty_weave = sio.getvalue()
558
# Since we don't have a .bzr directory, inherit the
559
# mode from the root directory
560
temp_control = LockableFiles(t, '')
561
temp_control._transport.mkdir('.bzr',
562
mode=temp_control._dir_mode)
563
file_mode = temp_control._file_mode
565
mutter('created control directory in ' + t.base)
566
control = t.clone('.bzr')
567
dirs = ['revision-store', 'weaves']
568
lock_file = 'branch-lock'
569
utf8_files = [('README',
570
"This is a Bazaar-NG control directory.\n"
571
"Do not change any files in this directory.\n"),
572
('branch-format', self.get_format_string()),
573
('revision-history', ''),
576
files = [('inventory.weave', StringIO(empty_weave)),
579
# FIXME: RBC 20060125 dont peek under the covers
580
# NB: no need to escape relative paths that are url safe.
581
control.put(lock_file, StringIO(), mode=file_mode)
582
control_files = LockableFiles(control, lock_file)
583
control_files.lock_write()
584
control_files._transport.mkdir_multi(dirs,
585
mode=control_files._dir_mode)
587
for file, content in utf8_files:
588
control_files.put_utf8(file, content)
589
for file, content in files:
590
control_files.put(file, content)
592
control_files.unlock()
593
return BzrBranch(t, _format=self, _control_files=control_files)
595
def is_supported(self):
596
"""Is this format supported?
598
Supported formats can be initialized and opened.
599
Unsupported formats may not support initialization or committing or
600
some other features depending on the reason for not being supported.
604
def open(self, transport):
605
"""Fill out the data in branch for the branch at url."""
606
return BzrBranch(transport, _format=self)
609
def register_format(klass, format):
610
klass._formats[format.get_format_string()] = format
613
def unregister_format(klass, format):
614
assert klass._formats[format.get_format_string()] is format
615
del klass._formats[format.get_format_string()]
618
class BzrBranchFormat4(BzrBranchFormat):
619
"""Bzr branch format 4.
623
- TextStores for texts, inventories,revisions.
625
This format is deprecated: it indexes texts using a text it which is
626
removed in format 5; write support for this format has been removed.
629
def get_format_string(self):
630
"""See BzrBranchFormat.get_format_string()."""
631
return BZR_BRANCH_FORMAT_4
633
def initialize(self, url):
634
"""Format 4 branches cannot be created."""
635
raise UninitializableFormat(self)
637
def is_supported(self):
638
"""Format 4 is not supported.
640
It is not supported because the model changed from 4 to 5 and the
641
conversion logic is expensive - so doing it on the fly was not
647
class BzrBranchFormat5(BzrBranchFormat):
648
"""Bzr branch format 5.
651
- weaves for file texts and inventory
653
- TextStores for revisions and signatures.
656
def get_format_string(self):
657
"""See BzrBranchFormat.get_format_string()."""
658
return BZR_BRANCH_FORMAT_5
661
class BzrBranchFormat6(BzrBranchFormat):
662
"""Bzr branch format 6.
665
- weaves for file texts and inventory
666
- hash subdirectory based stores.
667
- TextStores for revisions and signatures.
670
def get_format_string(self):
671
"""See BzrBranchFormat.get_format_string()."""
672
return BZR_BRANCH_FORMAT_6
675
BzrBranchFormat.register_format(BzrBranchFormat4())
676
BzrBranchFormat.register_format(BzrBranchFormat5())
677
BzrBranchFormat.register_format(BzrBranchFormat6())
679
# TODO: jam 20060108 Create a new branch format, and as part of upgrade
680
# make sure that ancestry.weave is deleted (it is never used, but
681
# used to be created)
684
class BzrBranch(Branch):
685
"""A branch stored in the actual filesystem.
687
Note that it's "local" in the context of the filesystem; it doesn't
688
really matter if it's on an nfs/smb/afs/coda/... share, as long as
689
it's writable, and can be accessed via the normal filesystem API.
692
# We actually expect this class to be somewhat short-lived; part of its
693
# purpose is to try to isolate what bits of the branch logic are tied to
694
# filesystem access, so that in a later step, we can extricate them to
695
# a separarte ("storage") class.
696
_inventory_weave = None
698
# Map some sort of prefix into a namespace
699
# stuff like "revno:10", "revid:", etc.
700
# This should match a prefix with a function which accepts
701
REVISION_NAMESPACES = {}
703
def push_stores(self, branch_to):
704
"""See Branch.push_stores."""
705
if (not isinstance(self._branch_format, BzrBranchFormat4) or
706
self._branch_format != branch_to._branch_format):
707
from bzrlib.fetch import greedy_fetch
708
mutter("Using fetch logic to push between %s(%s) and %s(%s)",
709
self, self._branch_format, branch_to, branch_to._branch_format)
710
greedy_fetch(to_branch=branch_to, from_branch=self,
711
revision=self.last_revision())
714
# format 4 to format 4 logic only.
715
store_pairs = ((self.text_store, branch_to.text_store),
716
(self.inventory_store, branch_to.inventory_store),
717
(self.revision_store, branch_to.revision_store))
719
for from_store, to_store in store_pairs:
720
copy_all(from_store, to_store)
721
except UnlistableStore:
722
raise UnlistableBranch(from_store)
724
def __init__(self, transport, init=DEPRECATED_PARAMETER,
725
relax_version_check=DEPRECATED_PARAMETER, _format=None,
726
_control_files=None):
727
"""Create new branch object at a particular location.
729
transport -- A Transport object, defining how to access files.
731
init -- If True, create new control files in a previously
732
unversioned directory. If False, the branch must already
735
relax_version_check -- If true, the usual check for the branch
736
version is not applied. This is intended only for
737
upgrade/recovery type use; it's not guaranteed that
738
all operations will work on old format branches.
740
In the test suite, creation of new trees is tested using the
741
`ScratchBranch` class.
743
assert isinstance(transport, Transport), \
744
"%r is not a Transport" % transport
745
self._transport = transport
746
self._base = self._transport.base
747
if _control_files is None:
748
_control_files = LockableFiles(self._transport.clone(bzrlib.BZRDIR),
750
self.control_files = _control_files
751
if deprecated_passed(init):
752
warn("BzrBranch.__init__(..., init=XXX): The init parameter is "
753
"deprecated as of bzr 0.8. Please use Branch.create().",
757
# this is slower than before deprecation, oh well never mind.
759
self._initialize(transport.base)
760
self._check_format(_format)
761
if deprecated_passed(relax_version_check):
762
warn("BzrBranch.__init__(..., relax_version_check=XXX_: The "
763
"relax_version_check parameter is deprecated as of bzr 0.8. "
764
"Please use Branch.open_downlevel, or a BzrBranchFormat's "
768
if (not relax_version_check
769
and not self._branch_format.is_supported()):
770
raise errors.UnsupportedFormatError(
771
'sorry, branch format %r not supported' % fmt,
772
['use a different bzr version',
773
'or remove the .bzr directory'
774
' and "bzr init" again'])
775
self.repository = Repository(transport, self._branch_format)
779
def _initialize(base):
780
"""Create a bzr branch in the latest format."""
781
return BzrBranchFormat6().initialize(base)
784
return '%s(%r)' % (self.__class__.__name__, self.base)
789
# TODO: It might be best to do this somewhere else,
790
# but it is nice for a Branch object to automatically
791
# cache it's information.
792
# Alternatively, we could have the Transport objects cache requests
793
# See the earlier discussion about how major objects (like Branch)
794
# should never expect their __del__ function to run.
795
# XXX: cache_root seems to be unused, 2006-01-13 mbp
796
if hasattr(self, 'cache_root') and self.cache_root is not None:
798
shutil.rmtree(self.cache_root)
801
self.cache_root = None
806
base = property(_get_base, doc="The URL for the root of this branch.")
808
def _finish_transaction(self):
809
"""Exit the current transaction."""
810
return self.control_files._finish_transaction()
812
def get_transaction(self):
813
"""Return the current active transaction.
815
If no transaction is active, this returns a passthrough object
816
for which all data is immediately flushed and no caching happens.
818
# this is an explicit function so that we can do tricky stuff
819
# when the storage in rev_storage is elsewhere.
820
# we probably need to hook the two 'lock a location' and
821
# 'have a transaction' together more delicately, so that
822
# we can have two locks (branch and storage) and one transaction
823
# ... and finishing the transaction unlocks both, but unlocking
824
# does not. - RBC 20051121
825
return self.control_files.get_transaction()
827
def _set_transaction(self, transaction):
828
"""Set a new active transaction."""
829
return self.control_files._set_transaction(transaction)
831
def abspath(self, name):
832
"""See Branch.abspath."""
833
return self.control_files._transport.abspath(name)
835
def _check_format(self, format):
836
"""Identify the branch format if needed.
838
The format is stored as a reference to the format object in
839
self._branch_format for code that needs to check it later.
841
The format parameter is either None or the branch format class
842
used to open this branch.
845
format = BzrBranchFormat.find_format(self._transport)
846
self._branch_format = format
847
mutter("got branch format %s", self._branch_format)
850
def get_root_id(self):
851
"""See Branch.get_root_id."""
852
tree = self.repository.revision_tree(self.last_revision())
853
return tree.inventory.root.file_id
855
def lock_write(self):
856
# TODO: test for failed two phase locks. This is known broken.
857
self.control_files.lock_write()
858
self.repository.lock_write()
861
# TODO: test for failed two phase locks. This is known broken.
862
self.control_files.lock_read()
863
self.repository.lock_read()
866
# TODO: test for failed two phase locks. This is known broken.
867
self.repository.unlock()
868
self.control_files.unlock()
870
def peek_lock_mode(self):
871
if self.control_files._lock_count == 0:
874
return self.control_files._lock_mode
877
def print_file(self, file, revision_id):
878
"""See Branch.print_file."""
879
return self.repository.print_file(file, revision_id)
882
def append_revision(self, *revision_ids):
883
"""See Branch.append_revision."""
884
for revision_id in revision_ids:
885
mutter("add {%s} to revision-history" % revision_id)
886
rev_history = self.revision_history()
887
rev_history.extend(revision_ids)
888
self.set_revision_history(rev_history)
891
def set_revision_history(self, rev_history):
892
"""See Branch.set_revision_history."""
893
self.control_files.put_utf8(
894
'revision-history', '\n'.join(rev_history))
896
def get_revision_delta(self, revno):
897
"""Return the delta for one revision.
899
The delta is relative to its mainline predecessor, or the
900
empty tree for revision 1.
902
assert isinstance(revno, int)
903
rh = self.revision_history()
904
if not (1 <= revno <= len(rh)):
905
raise InvalidRevisionNumber(revno)
907
# revno is 1-based; list is 0-based
909
new_tree = self.repository.revision_tree(rh[revno-1])
911
old_tree = EmptyTree()
913
old_tree = self.repository.revision_tree(rh[revno-2])
914
return compare_trees(old_tree, new_tree)
917
def revision_history(self):
918
"""See Branch.revision_history."""
919
# FIXME are transactions bound to control files ? RBC 20051121
920
transaction = self.get_transaction()
921
history = transaction.map.find_revision_history()
922
if history is not None:
923
mutter("cache hit for revision-history in %s", self)
925
history = [l.rstrip('\r\n') for l in
926
self.control_files.get_utf8('revision-history').readlines()]
927
transaction.map.add_revision_history(history)
928
# this call is disabled because revision_history is
929
# not really an object yet, and the transaction is for objects.
930
# transaction.register_clean(history, precious=True)
933
def update_revisions(self, other, stop_revision=None):
934
"""See Branch.update_revisions."""
935
from bzrlib.fetch import greedy_fetch
936
if stop_revision is None:
937
stop_revision = other.last_revision()
938
### Should this be checking is_ancestor instead of revision_history?
939
if (stop_revision is not None and
940
stop_revision in self.revision_history()):
942
greedy_fetch(to_branch=self, from_branch=other,
943
revision=stop_revision)
944
pullable_revs = self.pullable_revisions(other, stop_revision)
945
if len(pullable_revs) > 0:
946
self.append_revision(*pullable_revs)
948
def pullable_revisions(self, other, stop_revision):
949
"""See Branch.pullable_revisions."""
950
other_revno = other.revision_id_to_revno(stop_revision)
952
return self.missing_revisions(other, other_revno)
953
except DivergedBranches, e:
955
pullable_revs = get_intervening_revisions(self.last_revision(),
958
assert self.last_revision() not in pullable_revs
960
except bzrlib.errors.NotAncestor:
961
if is_ancestor(self.last_revision(), stop_revision, self):
966
def basis_tree(self):
967
"""See Branch.basis_tree."""
969
revision_id = self.revision_history()[-1]
970
# FIXME: This is an abstraction violation, the basis tree
971
# here as defined is on the working tree, the method should
972
# be too. The basis tree for a branch can be different than
973
# that for a working tree. RBC 20051207
974
xml = self.working_tree().read_basis_inventory(revision_id)
975
inv = bzrlib.xml5.serializer_v5.read_inventory_from_string(xml)
976
return RevisionTree(self.repository, inv, revision_id)
977
except (IndexError, NoSuchFile, NoWorkingTree), e:
978
return self.repository.revision_tree(self.last_revision())
980
def working_tree(self):
981
"""See Branch.working_tree."""
982
from bzrlib.workingtree import WorkingTree
983
from bzrlib.transport.local import LocalTransport
984
if (self.base.find('://') != -1 or
985
not isinstance(self._transport, LocalTransport)):
986
raise NoWorkingTree(self.base)
987
return WorkingTree(self.base, branch=self)
990
def pull(self, source, overwrite=False):
991
"""See Branch.pull."""
994
old_count = len(self.revision_history())
996
self.update_revisions(source)
997
except DivergedBranches:
1001
self.set_revision_history(source.revision_history())
1002
new_count = len(self.revision_history())
1003
return new_count - old_count
1007
def get_parent(self):
1008
"""See Branch.get_parent."""
1010
_locs = ['parent', 'pull', 'x-pull']
1013
return self.control_files.get_utf8(l).read().strip('\n')
1018
def get_push_location(self):
1019
"""See Branch.get_push_location."""
1020
config = bzrlib.config.BranchConfig(self)
1021
push_loc = config.get_user_option('push_location')
1024
def set_push_location(self, location):
1025
"""See Branch.set_push_location."""
1026
config = bzrlib.config.LocationConfig(self.base)
1027
config.set_user_option('push_location', location)
1030
def set_parent(self, url):
1031
"""See Branch.set_parent."""
1032
# TODO: Maybe delete old location files?
1033
# URLs should never be unicode, even on the local fs,
1034
# FIXUP this and get_parent in a future branch format bump:
1035
# read and rewrite the file, and have the new format code read
1036
# using .get not .get_utf8. RBC 20060125
1037
self.control_files.put_utf8('parent', url + '\n')
1039
def tree_config(self):
1040
return TreeConfig(self)
1042
def _get_truncated_history(self, revision_id):
1043
history = self.revision_history()
1044
if revision_id is None:
1047
idx = history.index(revision_id)
1049
raise InvalidRevisionId(revision_id=revision, branch=self)
1050
return history[:idx+1]
1053
def _clone_weave(self, to_location, revision=None, basis_branch=None):
1055
from bzrlib.workingtree import WorkingTree
1056
assert isinstance(to_location, basestring)
1057
if basis_branch is not None:
1058
note("basis_branch is not supported for fast weave copy yet.")
1060
history = self._get_truncated_history(revision)
1061
if not bzrlib.osutils.lexists(to_location):
1062
os.mkdir(to_location)
1063
branch_to = Branch.initialize(to_location)
1064
mutter("copy branch from %s to %s", self, branch_to)
1066
self.repository.copy(branch_to.repository)
1068
# must be done *after* history is copied across
1069
# FIXME duplicate code with base .clone().
1070
# .. would template method be useful here? RBC 20051207
1071
branch_to.set_parent(self.base)
1072
branch_to.append_revision(*history)
1073
# FIXME: this should be in workingtree.clone
1074
WorkingTree.create(branch_to, to_location).set_root_id(self.get_root_id())
1078
def clone(self, to_location, revision=None, basis_branch=None, to_branch_type=None):
1079
# FIXME: clone via create and fetch is probably faster when versioned
1081
if to_branch_type is None:
1082
to_branch_type = BzrBranch
1084
if to_branch_type == BzrBranch \
1085
and self.repository.weave_store.listable() \
1086
and self.repository.revision_store.listable():
1087
return self._clone_weave(to_location, revision, basis_branch)
1089
return Branch.clone(self, to_location, revision, basis_branch, to_branch_type)
1091
def fileid_involved_between_revs(self, from_revid, to_revid):
1092
"""Find file_id(s) which are involved in the changes between revisions.
1094
This determines the set of revisions which are involved, and then
1095
finds all file ids affected by those revisions.
1097
# TODO: jam 20060119 This code assumes that w.inclusions will
1098
# always be correct. But because of the presence of ghosts
1099
# it is possible to be wrong.
1100
# One specific example from Robert Collins:
1101
# Two branches, with revisions ABC, and AD
1102
# C is a ghost merge of D.
1103
# Inclusions doesn't recognize D as an ancestor.
1104
# If D is ever merged in the future, the weave
1105
# won't be fixed, because AD never saw revision C
1106
# to cause a conflict which would force a reweave.
1107
w = self.repository.get_inventory_weave()
1108
from_set = set(w.inclusions([w.lookup(from_revid)]))
1109
to_set = set(w.inclusions([w.lookup(to_revid)]))
1110
included = to_set.difference(from_set)
1111
changed = map(w.idx_to_name, included)
1112
return self._fileid_involved_by_set(changed)
1114
def fileid_involved(self, last_revid=None):
1115
"""Find all file_ids modified in the ancestry of last_revid.
1117
:param last_revid: If None, last_revision() will be used.
1119
w = self.repository.get_inventory_weave()
1121
changed = set(w._names)
1123
included = w.inclusions([w.lookup(last_revid)])
1124
changed = map(w.idx_to_name, included)
1125
return self._fileid_involved_by_set(changed)
1127
def fileid_involved_by_set(self, changes):
1128
"""Find all file_ids modified by the set of revisions passed in.
1130
:param changes: A set() of revision ids
1132
# TODO: jam 20060119 This line does *nothing*, remove it.
1133
# or better yet, change _fileid_involved_by_set so
1134
# that it takes the inventory weave, rather than
1135
# pulling it out by itself.
1136
w = self.repository.get_inventory_weave()
1137
return self._fileid_involved_by_set(changes)
1139
def _fileid_involved_by_set(self, changes):
1140
"""Find the set of file-ids affected by the set of revisions.
1142
:param changes: A set() of revision ids.
1143
:return: A set() of file ids.
1145
This peaks at the Weave, interpreting each line, looking to
1146
see if it mentions one of the revisions. And if so, includes
1147
the file id mentioned.
1148
This expects both the Weave format, and the serialization
1149
to have a single line per file/directory, and to have
1150
fileid="" and revision="" on that line.
1152
assert (isinstance(self._branch_format, BzrBranchFormat5) or
1153
isinstance(self._branch_format, BzrBranchFormat6)), \
1154
"fileid_involved only supported for branches which store inventory as xml"
1156
w = self.repository.get_inventory_weave()
1158
for line in w._weave:
1160
# it is ugly, but it is due to the weave structure
1161
if not isinstance(line, basestring): continue
1163
start = line.find('file_id="')+9
1164
if start < 9: continue
1165
end = line.find('"', start)
1167
file_id = xml.sax.saxutils.unescape(line[start:end])
1169
# check if file_id is already present
1170
if file_id in file_ids: continue
1172
start = line.find('revision="')+10
1173
if start < 10: continue
1174
end = line.find('"', start)
1176
revision_id = xml.sax.saxutils.unescape(line[start:end])
1178
if revision_id in changes:
1179
file_ids.add(file_id)
1184
Branch.set_default_initializer(BzrBranch._initialize)
1187
class BranchTestProviderAdapter(object):
1188
"""A tool to generate a suite testing multiple branch formats at once.
1190
This is done by copying the test once for each transport and injecting
1191
the transport_server, transport_readonly_server, and branch_format
1192
classes into each copy. Each copy is also given a new id() to make it
1196
def __init__(self, transport_server, transport_readonly_server, formats):
1197
self._transport_server = transport_server
1198
self._transport_readonly_server = transport_readonly_server
1199
self._formats = formats
1201
def adapt(self, test):
1202
result = TestSuite()
1203
for format in self._formats:
1204
new_test = deepcopy(test)
1205
new_test.transport_server = self._transport_server
1206
new_test.transport_readonly_server = self._transport_readonly_server
1207
new_test.branch_format = format
1208
def make_new_test_id():
1209
new_id = "%s(%s)" % (new_test.id(), format.__class__.__name__)
1210
return lambda: new_id
1211
new_test.id = make_new_test_id()
1212
result.addTest(new_test)
1216
class ScratchBranch(BzrBranch):
1369
1217
"""Special test class: a branch that cleans up after itself.
1371
1219
>>> b = ScratchBranch()
1372
1220
>>> isdir(b.base)
1374
1222
>>> bd = b.base
1223
>>> b._transport.__del__()
1379
def __init__(self, files=[], dirs=[], base=None):
1228
def __init__(self, files=[], dirs=[], transport=None):
1380
1229
"""Make a test branch.
1382
1231
This creates a temporary directory and runs init-tree in it.
1384
1233
If any files are listed, they are created in the working copy.
1386
from tempfile import mkdtemp
1391
Branch.__init__(self, base, init=init)
1235
if transport is None:
1236
transport = bzrlib.transport.local.ScratchTransport()
1237
# local import for scope restriction
1238
from bzrlib.workingtree import WorkingTree
1239
WorkingTree.create_standalone(transport.base)
1240
super(ScratchBranch, self).__init__(transport)
1242
super(ScratchBranch, self).__init__(transport)
1244
# BzrBranch creates a clone to .bzr and then forgets about the
1245
# original transport. A ScratchTransport() deletes itself and
1246
# everything underneath it when it goes away, so we need to
1247
# grab a local copy to prevent that from happening
1248
self._transport = transport
1393
os.mkdir(self.abspath(d))
1251
self._transport.mkdir(d)
1395
1253
for f in files:
1396
file(os.path.join(self.base, f), 'w').write('content of %s' % f)
1254
self._transport.put(f, 'content of %s' % f)
1399
1256
def clone(self):
1401
1258
>>> orig = ScratchBranch(files=["file1", "file2"])
1259
>>> os.listdir(orig.base)
1260
[u'.bzr', u'file1', u'file2']
1402
1261
>>> clone = orig.clone()
1403
>>> os.path.samefile(orig.base, clone.base)
1262
>>> if os.name != 'nt':
1263
... os.path.samefile(orig.base, clone.base)
1265
... orig.base == clone.base
1405
>>> os.path.isfile(os.path.join(clone.base, "file1"))
1268
>>> os.listdir(clone.base)
1269
[u'.bzr', u'file1', u'file2']
1408
1271
from shutil import copytree
1409
from tempfile import mkdtemp
1272
from bzrlib.osutils import mkdtemp
1410
1273
base = mkdtemp()
1412
1275
copytree(self.base, base, symlinks=True)
1413
return ScratchBranch(base=base)
1421
"""Destroy the test branch, removing the scratch directory."""
1422
from shutil import rmtree
1425
mutter("delete ScratchBranch %s" % self.base)
1428
# Work around for shutil.rmtree failing on Windows when
1429
# readonly files are encountered
1430
mutter("hit exception in destroying ScratchBranch: %s" % e)
1431
for root, dirs, files in os.walk(self.base, topdown=False):
1433
os.chmod(os.path.join(root, name), 0700)
1276
return ScratchBranch(
1277
transport=bzrlib.transport.local.ScratchTransport(base))
1439
1280
######################################################################