21
from warnings import warn
22
from cStringIO import StringIO
26
from bzrlib.inventory import InventoryEntry
27
import bzrlib.inventory as inventory
28
22
from bzrlib.trace import mutter, note
29
from bzrlib.osutils import (isdir, quotefn, compact_date, rand_bytes,
30
rename, splitpath, sha_file, appendpath,
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)
23
from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, \
25
sha_file, appendpath, file_kind
27
from bzrlib.errors import BzrError, InvalidRevisionNumber, InvalidRevisionId, \
28
DivergedBranches, NotBranchError
37
29
from bzrlib.textui import show_status
38
from bzrlib.revision import (Revision, is_ancestor, get_intervening_revisions,
30
from bzrlib.revision import Revision
41
31
from bzrlib.delta import compare_trees
42
32
from bzrlib.tree import EmptyTree, RevisionTree
43
from bzrlib.inventory import Inventory
44
from bzrlib.store import copy_all
45
from bzrlib.store.compressed_text import CompressedTextStore
46
from bzrlib.store.text import TextStore
47
from bzrlib.store.weave import WeaveStore
48
from bzrlib.testament import Testament
49
import bzrlib.transactions as transactions
50
from bzrlib.transport import Transport, get_transport
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"
38
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
58
39
## TODO: Maybe include checks for common corruption of newlines, etc?
61
42
# TODO: Some operations like log might retrieve the same revisions
62
43
# repeatedly to calculate deltas. We could perhaps have a weakref
63
# cache in memory to make this faster. In general anything can be
64
# cached in memory between lock and unlock operations.
66
def find_branch(*ignored, **ignored_too):
67
# XXX: leave this here for about one release, then remove it
68
raise NotImplementedError('find_branch() is not supported anymore, '
69
'please use one of the new branch constructors')
72
def needs_read_lock(unbound):
73
"""Decorate unbound to take out and release a read lock."""
74
def decorated(self, *args, **kwargs):
77
return unbound(self, *args, **kwargs)
83
def needs_write_lock(unbound):
84
"""Decorate unbound to take out and release a write lock."""
85
def decorated(self, *args, **kwargs):
88
return unbound(self, *args, **kwargs)
44
# cache in memory to make this faster.
46
# TODO: please move the revision-string syntax stuff out of the branch
47
# object; it's clutter
50
def find_branch(f, **args):
51
if f and (f.startswith('http://') or f.startswith('https://')):
52
from bzrlib.remotebranch import RemoteBranch
53
return RemoteBranch(f, **args)
55
return Branch(f, **args)
58
def find_cached_branch(f, cache_root, **args):
59
from bzrlib.remotebranch import RemoteBranch
60
br = find_branch(f, **args)
61
def cacheify(br, store_name):
62
from bzrlib.meta_store import CachedStore
63
cache_path = os.path.join(cache_root, store_name)
65
new_store = CachedStore(getattr(br, store_name), cache_path)
66
setattr(br, store_name, new_store)
68
if isinstance(br, RemoteBranch):
69
cacheify(br, 'inventory_store')
70
cacheify(br, 'text_store')
71
cacheify(br, 'revision_store')
75
def _relpath(base, path):
76
"""Return path relative to base, or raise exception.
78
The path may be either an absolute path or a path relative to the
79
current working directory.
81
Lifted out of Branch.relpath for ease of testing.
83
os.path.commonprefix (python2.4) has a bad bug that it works just
84
on string prefixes, assuming that '/u' is a prefix of '/u2'. This
85
avoids that problem."""
86
rp = os.path.abspath(path)
90
while len(head) >= len(base):
93
head, tail = os.path.split(head)
97
raise NotBranchError("path %r is not within branch %r" % (rp, base))
102
def find_branch_root(f=None):
103
"""Find the branch root enclosing f, or pwd.
105
f may be a filename or a URL.
107
It is not necessary that f exists.
109
Basically we keep looking up until we find the control directory or
110
run into the root. If there isn't one, raises NotBranchError.
114
elif hasattr(os.path, 'realpath'):
115
f = os.path.realpath(f)
117
f = os.path.abspath(f)
118
if not os.path.exists(f):
119
raise BzrError('%r does not exist' % f)
125
if os.path.exists(os.path.join(f, bzrlib.BZRDIR)):
127
head, tail = os.path.split(f)
129
# reached the root, whatever that may be
130
raise NotBranchError('%s is not in a branch' % orig_f)
93
136
######################################################################
97
140
"""Branch holding a history of revisions.
100
Base directory/url of the branch.
104
def __init__(self, *ignored, **ignored_too):
105
raise NotImplementedError('The Branch class is abstract')
108
def open_downlevel(base):
109
"""Open a branch which may be of an old format.
111
Only local branches are supported."""
112
return _Branch(get_transport(base), relax_version_check=True)
116
"""Open an existing branch, rooted at 'base' (url)"""
117
t = get_transport(base)
118
mutter("trying to open %r with transport %r", base, t)
122
def open_containing(url):
123
"""Open an existing branch which contains url.
125
This probes for a branch at url, and searches upwards from there.
127
Basically we keep looking up until we find the control directory or
128
run into the root. If there isn't one, raises NotBranchError.
129
If there is one, it is returned, along with the unused portion of url.
131
t = get_transport(url)
134
return _Branch(t), t.relpath(url)
135
except NotBranchError:
137
new_t = t.clone('..')
138
if new_t.base == t.base:
139
# reached the root, whatever that may be
140
raise NotBranchError(path=url)
144
def initialize(base):
145
"""Create a new branch, rooted at 'base' (url)"""
146
t = get_transport(base)
147
return _Branch(t, init=True)
149
def setup_caching(self, cache_root):
150
"""Subclasses that care about caching should override this, and set
151
up cached stores located under cache_root.
153
self.cache_root = cache_root
156
class _Branch(Branch):
157
"""A branch stored in the actual filesystem.
159
Note that it's "local" in the context of the filesystem; it doesn't
160
really matter if it's on an nfs/smb/afs/coda/... share, as long as
161
it's writable, and can be accessed via the normal filesystem API.
143
Base directory of the branch.
164
146
None, or 'r' or 'w'
171
153
Lock object from bzrlib.lock.
173
# We actually expect this class to be somewhat short-lived; part of its
174
# purpose is to try to isolate what bits of the branch logic are tied to
175
# filesystem access, so that in a later step, we can extricate them to
176
# a separarte ("storage") class.
177
156
_lock_mode = None
178
157
_lock_count = None
180
_inventory_weave = None
182
160
# Map some sort of prefix into a namespace
183
161
# stuff like "revno:10", "revid:", etc.
184
162
# This should match a prefix with a function which accepts
185
163
REVISION_NAMESPACES = {}
187
def push_stores(self, branch_to):
188
"""Copy the content of this branches store to branch_to."""
189
if (self._branch_format != branch_to._branch_format
190
or self._branch_format != 4):
191
from bzrlib.fetch import greedy_fetch
192
mutter("falling back to fetch logic to push between %s(%s) and %s(%s)",
193
self, self._branch_format, branch_to, branch_to._branch_format)
194
greedy_fetch(to_branch=branch_to, from_branch=self,
195
revision=self.last_revision())
198
store_pairs = ((self.text_store, branch_to.text_store),
199
(self.inventory_store, branch_to.inventory_store),
200
(self.revision_store, branch_to.revision_store))
202
for from_store, to_store in store_pairs:
203
copy_all(from_store, to_store)
204
except UnlistableStore:
205
raise UnlistableBranch(from_store)
207
def __init__(self, transport, init=False,
208
relax_version_check=False):
165
def __init__(self, base, init=False, find_root=True):
209
166
"""Create new branch object at a particular location.
211
transport -- A Transport object, defining how to access files.
168
base -- Base directory for the branch.
213
170
init -- If True, create new control files in a previously
214
171
unversioned directory. If False, the branch must already
217
relax_version_check -- If true, the usual check for the branch
218
version is not applied. This is intended only for
219
upgrade/recovery type use; it's not guaranteed that
220
all operations will work on old format branches.
174
find_root -- If true and init is false, find the root of the
175
existing branch containing base.
222
177
In the test suite, creation of new trees is tested using the
223
178
`ScratchBranch` class.
225
assert isinstance(transport, Transport), \
226
"%r is not a Transport" % transport
227
self._transport = transport
180
from bzrlib.store import ImmutableStore
182
self.base = os.path.realpath(base)
229
183
self._make_control()
230
self._check_format(relax_version_check)
232
def get_store(name, compressed=True, prefixed=False):
233
# FIXME: This approach of assuming stores are all entirely compressed
234
# or entirely uncompressed is tidy, but breaks upgrade from
235
# some existing branches where there's a mixture; we probably
236
# still want the option to look for both.
237
relpath = self._rel_controlfilename(name)
239
store = CompressedTextStore(self._transport.clone(relpath),
242
store = TextStore(self._transport.clone(relpath),
244
#if self._transport.should_cache():
245
# cache_path = os.path.join(self.cache_root, name)
246
# os.mkdir(cache_path)
247
# store = bzrlib.store.CachedStore(store, cache_path)
249
def get_weave(name, prefixed=False):
250
relpath = self._rel_controlfilename(name)
251
ws = WeaveStore(self._transport.clone(relpath), prefixed=prefixed)
252
if self._transport.should_cache():
253
ws.enable_cache = True
256
if self._branch_format == 4:
257
self.inventory_store = get_store('inventory-store')
258
self.text_store = get_store('text-store')
259
self.revision_store = get_store('revision-store')
260
elif self._branch_format == 5:
261
self.control_weaves = get_weave('')
262
self.weave_store = get_weave('weaves')
263
self.revision_store = get_store('revision-store', compressed=False)
264
elif self._branch_format == 6:
265
self.control_weaves = get_weave('')
266
self.weave_store = get_weave('weaves', prefixed=True)
267
self.revision_store = get_store('revision-store', compressed=False,
269
self.revision_store.register_suffix('sig')
270
self._transaction = None
185
self.base = find_branch_root(base)
187
self.base = os.path.realpath(base)
188
if not isdir(self.controlfilename('.')):
189
raise NotBranchError("not a bzr branch: %s" % quotefn(base),
190
['use "bzr init" to initialize a new working tree',
191
'current bzr can only operate from top-of-tree'])
194
self.text_store = ImmutableStore(self.controlfilename('text-store'))
195
self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
196
self.inventory_store = ImmutableStore(self.controlfilename('inventory-store'))
272
199
def __str__(self):
273
return '%s(%r)' % (self.__class__.__name__, self._transport.base)
200
return '%s(%r)' % (self.__class__.__name__, self.base)
276
203
__repr__ = __str__
279
206
def __del__(self):
280
207
if self._lock_mode or self._lock:
281
# XXX: This should show something every time, and be suitable for
282
# headless operation and embedding
208
from bzrlib.warnings import warn
283
209
warn("branch %r was not explicitly unlocked" % self)
284
210
self._lock.unlock()
286
# TODO: It might be best to do this somewhere else,
287
# but it is nice for a Branch object to automatically
288
# cache it's information.
289
# Alternatively, we could have the Transport objects cache requests
290
# See the earlier discussion about how major objects (like Branch)
291
# should never expect their __del__ function to run.
292
if hasattr(self, 'cache_root') and self.cache_root is not None:
295
shutil.rmtree(self.cache_root)
298
self.cache_root = None
302
return self._transport.base
305
base = property(_get_base, doc="The URL for the root of this branch.")
307
def _finish_transaction(self):
308
"""Exit the current transaction."""
309
if self._transaction is None:
310
raise errors.LockError('Branch %s is not in a transaction' %
312
transaction = self._transaction
313
self._transaction = None
316
def get_transaction(self):
317
"""Return the current active transaction.
319
If no transaction is active, this returns a passthrough object
320
for which all data is immediately flushed and no caching happens.
322
if self._transaction is None:
323
return transactions.PassThroughTransaction()
325
return self._transaction
327
def _set_transaction(self, new_transaction):
328
"""Set a new active transaction."""
329
if self._transaction is not None:
330
raise errors.LockError('Branch %s is in a transaction already.' %
332
self._transaction = new_transaction
334
213
def lock_write(self):
335
mutter("lock write: %s (%s)", self, self._lock_count)
336
# TODO: Upgrade locking to support using a Transport,
337
# and potentially a remote locking protocol
338
214
if self._lock_mode:
339
215
if self._lock_mode != 'w':
216
from bzrlib.errors import LockError
340
217
raise LockError("can't upgrade to a write lock from %r" %
342
219
self._lock_count += 1
344
self._lock = self._transport.lock_write(
345
self._rel_controlfilename('branch-lock'))
221
from bzrlib.lock import WriteLock
223
self._lock = WriteLock(self.controlfilename('branch-lock'))
346
224
self._lock_mode = 'w'
347
225
self._lock_count = 1
348
self._set_transaction(transactions.PassThroughTransaction())
350
228
def lock_read(self):
351
mutter("lock read: %s (%s)", self, self._lock_count)
352
229
if self._lock_mode:
353
230
assert self._lock_mode in ('r', 'w'), \
354
231
"invalid lock mode %r" % self._lock_mode
355
232
self._lock_count += 1
357
self._lock = self._transport.lock_read(
358
self._rel_controlfilename('branch-lock'))
234
from bzrlib.lock import ReadLock
236
self._lock = ReadLock(self.controlfilename('branch-lock'))
359
237
self._lock_mode = 'r'
360
238
self._lock_count = 1
361
self._set_transaction(transactions.ReadOnlyTransaction())
362
# 5K may be excessive, but hey, its a knob.
363
self.get_transaction().set_cache_size(5000)
365
240
def unlock(self):
366
mutter("unlock: %s (%s)", self, self._lock_count)
367
241
if not self._lock_mode:
242
from bzrlib.errors import LockError
368
243
raise LockError('branch %r is not locked' % (self))
370
245
if self._lock_count > 1:
371
246
self._lock_count -= 1
373
self._finish_transaction()
374
248
self._lock.unlock()
375
249
self._lock = None
376
250
self._lock_mode = self._lock_count = None
378
252
def abspath(self, name):
379
"""Return absolute filename for something in the branch
381
XXX: Robert Collins 20051017 what is this used for? why is it a branch
382
method and not a tree method.
384
return self._transport.abspath(name)
386
def _rel_controlfilename(self, file_or_path):
387
if not isinstance(file_or_path, basestring):
388
file_or_path = '/'.join(file_or_path)
389
if file_or_path == '':
391
return bzrlib.transport.urlescape(bzrlib.BZRDIR + '/' + file_or_path)
253
"""Return absolute filename for something in the branch"""
254
return os.path.join(self.base, name)
256
def relpath(self, path):
257
"""Return path relative to this branch of something inside it.
259
Raises an error if path is not in this branch."""
260
return _relpath(self.base, path)
393
262
def controlfilename(self, file_or_path):
394
263
"""Return location relative to branch."""
395
return self._transport.abspath(self._rel_controlfilename(file_or_path))
264
if isinstance(file_or_path, basestring):
265
file_or_path = [file_or_path]
266
return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
397
269
def controlfile(self, file_or_path, mode='r'):
398
270
"""Open a control file for this branch.
405
277
Controlfiles should almost never be opened in write mode but
406
278
rather should be atomically copied and replaced using atomicfile.
410
relpath = self._rel_controlfilename(file_or_path)
411
#TODO: codecs.open() buffers linewise, so it was overloaded with
412
# a much larger buffer, do we need to do the same for getreader/getwriter?
414
return self._transport.get(relpath)
416
raise BzrError("Branch.controlfile(mode='wb') is not supported, use put_controlfiles")
418
return codecs.getreader('utf-8')(self._transport.get(relpath), errors='replace')
420
raise BzrError("Branch.controlfile(mode='w') is not supported, use put_controlfiles")
281
fn = self.controlfilename(file_or_path)
283
if mode == 'rb' or mode == 'wb':
284
return file(fn, mode)
285
elif mode == 'r' or mode == 'w':
286
# open in binary mode anyhow so there's no newline translation;
287
# codecs uses line buffering by default; don't want that.
289
return codecs.open(fn, mode + 'b', 'utf-8',
422
292
raise BzrError("invalid controlfile mode %r" % mode)
424
def put_controlfile(self, path, f, encode=True):
425
"""Write an entry as a controlfile.
427
:param path: The path to put the file, relative to the .bzr control
429
:param f: A file-like or string object whose contents should be copied.
430
:param encode: If true, encode the contents as utf-8
432
self.put_controlfiles([(path, f)], encode=encode)
434
def put_controlfiles(self, files, encode=True):
435
"""Write several entries as controlfiles.
437
:param files: A list of [(path, file)] pairs, where the path is the directory
438
underneath the bzr control directory
439
:param encode: If true, encode the contents as utf-8
443
for path, f in files:
445
if isinstance(f, basestring):
446
f = f.encode('utf-8', 'replace')
448
f = codecs.getwriter('utf-8')(f, errors='replace')
449
path = self._rel_controlfilename(path)
450
ctrl_files.append((path, f))
451
self._transport.put_multi(ctrl_files)
453
294
def _make_control(self):
454
295
from bzrlib.inventory import Inventory
455
from bzrlib.weavefile import write_weave_v5
456
from bzrlib.weave import Weave
458
# Create an empty inventory
297
os.mkdir(self.controlfilename([]))
298
self.controlfile('README', 'w').write(
299
"This is a Bazaar-NG control directory.\n"
300
"Do not change any files in this directory.\n")
301
self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT)
302
for d in ('text-store', 'inventory-store', 'revision-store'):
303
os.mkdir(self.controlfilename(d))
304
for f in ('revision-history', 'merged-patches',
305
'pending-merged-patches', 'branch-name',
308
self.controlfile(f, 'w').write('')
309
mutter('created control directory in ' + self.base)
460
311
# if we want per-tree root ids then this is the place to set
461
312
# them; they're not needed for now and so ommitted for
463
bzrlib.xml5.serializer_v5.write_inventory(Inventory(), sio)
464
empty_inv = sio.getvalue()
466
bzrlib.weavefile.write_weave_v5(Weave(), sio)
467
empty_weave = sio.getvalue()
469
dirs = [[], 'revision-store', 'weaves']
471
"This is a Bazaar-NG control directory.\n"
472
"Do not change any files in this directory.\n"),
473
('branch-format', BZR_BRANCH_FORMAT_6),
474
('revision-history', ''),
477
('pending-merges', ''),
478
('inventory', empty_inv),
479
('inventory.weave', empty_weave),
480
('ancestry.weave', empty_weave)
482
cfn = self._rel_controlfilename
483
self._transport.mkdir_multi([cfn(d) for d in dirs])
484
self.put_controlfiles(files)
485
mutter('created control directory in ' + self._transport.base)
487
def _check_format(self, relax_version_check):
314
f = self.controlfile('inventory','w')
315
bzrlib.xml.serializer_v4.write_inventory(Inventory(), f)
318
def _check_format(self):
488
319
"""Check this branch format is supported.
490
The format level is stored, as an integer, in
491
self._branch_format for code that needs to check it later.
321
The current tool only supports the current unstable format.
493
323
In the future, we might need different in-memory Branch
494
324
classes to support downlevel branches. But not yet.
497
fmt = self.controlfile('branch-format', 'r').read()
499
raise NotBranchError(path=self.base)
500
mutter("got branch format %r", fmt)
501
if fmt == BZR_BRANCH_FORMAT_6:
502
self._branch_format = 6
503
elif fmt == BZR_BRANCH_FORMAT_5:
504
self._branch_format = 5
505
elif fmt == BZR_BRANCH_FORMAT_4:
506
self._branch_format = 4
508
if (not relax_version_check
509
and self._branch_format not in (5, 6)):
510
raise errors.UnsupportedFormatError(
511
'sorry, branch format %r not supported' % fmt,
326
# This ignores newlines so that we can open branches created
327
# on Windows from Linux and so on. I think it might be better
328
# to always make all internal files in unix format.
329
fmt = self.controlfile('branch-format', 'r').read()
330
fmt = fmt.replace('\r\n', '\n')
331
if fmt != BZR_BRANCH_FORMAT:
332
raise BzrError('sorry, branch format %r not supported' % fmt,
512
333
['use a different bzr version',
513
'or remove the .bzr directory'
514
' and "bzr init" again'])
334
'or remove the .bzr directory and "bzr init" again'])
516
336
def get_root_id(self):
517
337
"""Return the id of this branches root"""
530
350
entry.parent_id = inv.root.file_id
531
351
self._write_inventory(inv)
534
353
def read_working_inventory(self):
535
354
"""Read the working inventory."""
536
# ElementTree does its own conversion from UTF-8, so open in
538
f = self.controlfile('inventory', 'rb')
539
return bzrlib.xml5.serializer_v5.read_inventory(f)
355
from bzrlib.inventory import Inventory
358
# ElementTree does its own conversion from UTF-8, so open in
360
f = self.controlfile('inventory', 'rb')
361
return bzrlib.xml.serializer_v4.read_inventory(f)
542
366
def _write_inventory(self, inv):
543
367
"""Update the working inventory.
545
369
That is to say, the inventory describing changes underway, that
546
370
will be committed to the next revision.
548
from cStringIO import StringIO
550
bzrlib.xml5.serializer_v5.write_inventory(inv, sio)
552
# Transport handles atomicity
553
self.put_controlfile('inventory', sio)
372
from bzrlib.atomicfile import AtomicFile
376
f = AtomicFile(self.controlfilename('inventory'), 'wb')
378
bzrlib.xml.serializer_v4.write_inventory(inv, f)
555
385
mutter('wrote working inventory')
557
388
inventory = property(read_working_inventory, _write_inventory, None,
558
389
"""Inventory for the working copy.""")
561
392
def add(self, files, ids=None):
562
393
"""Make files versioned.
594
425
assert(len(ids) == len(files))
596
inv = self.read_working_inventory()
597
for f,file_id in zip(files, ids):
598
if is_control_file(f):
599
raise BzrError("cannot add control file %s" % quotefn(f))
604
raise BzrError("cannot add top-level %r" % f)
606
fullpath = os.path.normpath(self.abspath(f))
609
kind = file_kind(fullpath)
611
# maybe something better?
612
raise BzrError('cannot add: not a regular file, symlink or directory: %s' % quotefn(f))
614
if not InventoryEntry.versionable_kind(kind):
615
raise BzrError('cannot add: not a versionable file ('
616
'i.e. regular file, symlink or directory): %s' % quotefn(f))
619
file_id = gen_file_id(f)
620
inv.add_path(f, kind=kind, file_id=file_id)
622
mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
624
self._write_inventory(inv)
429
inv = self.read_working_inventory()
430
for f,file_id in zip(files, ids):
431
if is_control_file(f):
432
raise BzrError("cannot add control file %s" % quotefn(f))
437
raise BzrError("cannot add top-level %r" % f)
439
fullpath = os.path.normpath(self.abspath(f))
442
kind = file_kind(fullpath)
444
# maybe something better?
445
raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
447
if kind != 'file' and kind != 'directory':
448
raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
451
file_id = gen_file_id(f)
452
inv.add_path(f, kind=kind, file_id=file_id)
454
mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
456
self._write_inventory(inv)
627
461
def print_file(self, file, revno):
628
462
"""Print `file` to stdout."""
629
tree = self.revision_tree(self.get_rev_id(revno))
630
# use inventory as it was in that revision
631
file_id = tree.inventory.path2id(file)
633
raise BzrError("%r is not present in revision %s" % (file, revno))
634
tree.print_file(file_id)
465
tree = self.revision_tree(self.lookup_revision(revno))
466
# use inventory as it was in that revision
467
file_id = tree.inventory.path2id(file)
469
raise BzrError("%r is not present in revision %s" % (file, revno))
470
tree.print_file(file_id)
475
def remove(self, files, verbose=False):
476
"""Mark nominated files for removal from the inventory.
478
This does not remove their text. This does not run on
480
TODO: Refuse to remove modified files unless --force is given?
482
TODO: Do something useful with directories.
484
TODO: Should this remove the text or not? Tough call; not
485
removing may be useful and the user can just use use rm, and
486
is the opposite of add. Removing it is consistent with most
487
other tools. Maybe an option.
489
## TODO: Normalize names
490
## TODO: Remove nested loops; better scalability
491
if isinstance(files, basestring):
497
tree = self.working_tree()
500
# do this before any modifications
504
raise BzrError("cannot remove unversioned file %s" % quotefn(f))
505
mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
507
# having remove it, it must be either ignored or unknown
508
if tree.is_ignored(f):
512
show_status(new_status, inv[fid].kind, quotefn(f))
515
self._write_inventory(inv)
636
520
# FIXME: this doesn't need to be a branch method
637
521
def set_inventory(self, new_inventory_list):
641
525
name = os.path.basename(path)
644
# fixme, there should be a factory function inv,add_??
645
if kind == 'directory':
646
inv.add(inventory.InventoryDirectory(file_id, name, parent))
648
inv.add(inventory.InventoryFile(file_id, name, parent))
649
elif kind == 'symlink':
650
inv.add(inventory.InventoryLink(file_id, name, parent))
652
raise BzrError("unknown kind %r" % kind)
528
inv.add(InventoryEntry(file_id, name, kind, parent))
653
529
self._write_inventory(inv)
655
532
def unknowns(self):
656
533
"""Return all unknown files.
658
535
These are files in the working directory that are not versioned or
659
536
control files or ignored.
661
>>> from bzrlib.workingtree import WorkingTree
662
538
>>> b = ScratchBranch(files=['foo', 'foo~'])
663
>>> map(str, b.unknowns())
539
>>> list(b.unknowns())
666
542
>>> list(b.unknowns())
668
>>> WorkingTree(b.base, b).remove('foo')
669
545
>>> list(b.unknowns())
672
548
return self.working_tree().unknowns()
675
551
def append_revision(self, *revision_ids):
552
from bzrlib.atomicfile import AtomicFile
676
554
for revision_id in revision_ids:
677
555
mutter("add {%s} to revision-history" % revision_id)
678
557
rev_history = self.revision_history()
679
558
rev_history.extend(revision_ids)
680
self.set_revision_history(rev_history)
683
def set_revision_history(self, rev_history):
684
self.put_controlfile('revision-history', '\n'.join(rev_history))
686
def has_revision(self, revision_id):
687
"""True if this branch has a copy of the revision.
689
This does not necessarily imply the revision is merge
690
or on the mainline."""
691
return (revision_id is None
692
or self.revision_store.has_id(revision_id))
560
f = AtomicFile(self.controlfilename('revision-history'))
562
for rev_id in rev_history:
695
569
def get_revision_xml_file(self, revision_id):
696
570
"""Return XML file object for revision object."""
697
571
if not revision_id or not isinstance(revision_id, basestring):
698
raise InvalidRevisionId(revision_id=revision_id, branch=self)
572
raise InvalidRevisionId(revision_id)
700
return self.revision_store.get(revision_id)
701
except (IndexError, KeyError):
702
raise bzrlib.errors.NoSuchRevision(self, revision_id)
577
return self.revision_store[revision_id]
579
raise bzrlib.errors.NoSuchRevision(self, revision_id)
705
585
get_revision_xml = get_revision_xml_file
707
def get_revision_xml(self, revision_id):
708
return self.get_revision_xml_file(revision_id).read()
711
588
def get_revision(self, revision_id):
712
589
"""Return the Revision object for a named revision"""
713
590
xml_file = self.get_revision_xml_file(revision_id)
716
r = bzrlib.xml5.serializer_v5.read_revision(xml_file)
593
r = bzrlib.xml.serializer_v4.read_revision(xml_file)
717
594
except SyntaxError, e:
718
595
raise bzrlib.errors.BzrError('failed to unpack revision_xml',
751
631
# the revision, (add signatures/remove signatures) and still
752
632
# have all hash pointers stay consistent.
753
633
# But for now, just hash the contents.
754
return bzrlib.osutils.sha_file(self.get_revision_xml_file(revision_id))
756
def get_ancestry(self, revision_id):
757
"""Return a list of revision-ids integrated by a revision.
759
This currently returns a list, but the ordering is not guaranteed:
762
if revision_id is None:
764
w = self.get_inventory_weave()
765
return [None] + map(w.idx_to_name,
766
w.inclusions([w.lookup(revision_id)]))
768
def get_inventory_weave(self):
769
return self.control_weaves.get_weave('inventory',
770
self.get_transaction())
772
def get_inventory(self, revision_id):
773
"""Get Inventory object by hash."""
774
xml = self.get_inventory_xml(revision_id)
775
return bzrlib.xml5.serializer_v5.read_inventory_from_string(xml)
777
def get_inventory_xml(self, revision_id):
634
return bzrlib.osutils.sha_file(self.get_revision_xml(revision_id))
637
def get_inventory(self, inventory_id):
638
"""Get Inventory object by hash.
640
TODO: Perhaps for this and similar methods, take a revision
641
parameter which can be either an integer revno or a
643
from bzrlib.inventory import Inventory
645
f = self.get_inventory_xml_file(inventory_id)
646
return bzrlib.xml.serializer_v4.read_inventory(f)
649
def get_inventory_xml(self, inventory_id):
778
650
"""Get inventory XML as a file object."""
780
assert isinstance(revision_id, basestring), type(revision_id)
781
iw = self.get_inventory_weave()
782
return iw.get_text(iw.lookup(revision_id))
784
raise bzrlib.errors.HistoryMissing(self, 'inventory', revision_id)
786
def get_inventory_sha1(self, revision_id):
651
return self.inventory_store[inventory_id]
653
get_inventory_xml_file = get_inventory_xml
656
def get_inventory_sha1(self, inventory_id):
787
657
"""Return the sha1 hash of the inventory entry
789
return self.get_revision(revision_id).inventory_sha1
659
return sha_file(self.get_inventory_xml(inventory_id))
791
662
def get_revision_inventory(self, revision_id):
792
663
"""Return inventory of a past revision."""
793
# TODO: Unify this with get_inventory()
794
# bzr 0.0.6 and later imposes the constraint that the inventory_id
664
# bzr 0.0.6 imposes the constraint that the inventory_id
795
665
# must be the same as its revision, so this is trivial.
796
666
if revision_id == None:
667
from bzrlib.inventory import Inventory
797
668
return Inventory(self.get_root_id())
799
670
return self.get_inventory(revision_id)
802
673
def revision_history(self):
803
"""Return sequence of revision hashes on to this branch."""
804
transaction = self.get_transaction()
805
history = transaction.map.find_revision_history()
806
if history is not None:
807
mutter("cache hit for revision-history in %s", self)
809
history = [l.rstrip('\r\n') for l in
810
self.controlfile('revision-history', 'r').readlines()]
811
transaction.map.add_revision_history(history)
812
# this call is disabled because revision_history is
813
# not really an object yet, and the transaction is for objects.
814
# transaction.register_clean(history, precious=True)
674
"""Return sequence of revision hashes on to this branch.
676
>>> ScratchBranch().revision_history()
681
return [l.rstrip('\r\n') for l in
682
self.controlfile('revision-history', 'r').readlines()]
687
def common_ancestor(self, other, self_revno=None, other_revno=None):
689
>>> from bzrlib.commit import commit
690
>>> sb = ScratchBranch(files=['foo', 'foo~'])
691
>>> sb.common_ancestor(sb) == (None, None)
693
>>> commit(sb, "Committing first revision", verbose=False)
694
>>> sb.common_ancestor(sb)[0]
696
>>> clone = sb.clone()
697
>>> commit(sb, "Committing second revision", verbose=False)
698
>>> sb.common_ancestor(sb)[0]
700
>>> sb.common_ancestor(clone)[0]
702
>>> commit(clone, "Committing divergent second revision",
704
>>> sb.common_ancestor(clone)[0]
706
>>> sb.common_ancestor(clone) == clone.common_ancestor(sb)
708
>>> sb.common_ancestor(sb) != clone.common_ancestor(clone)
710
>>> clone2 = sb.clone()
711
>>> sb.common_ancestor(clone2)[0]
713
>>> sb.common_ancestor(clone2, self_revno=1)[0]
715
>>> sb.common_ancestor(clone2, other_revno=1)[0]
718
my_history = self.revision_history()
719
other_history = other.revision_history()
720
if self_revno is None:
721
self_revno = len(my_history)
722
if other_revno is None:
723
other_revno = len(other_history)
724
indices = range(min((self_revno, other_revno)))
727
if my_history[r] == other_history[r]:
728
return r+1, my_history[r]
818
733
"""Return current revision number for this branch.
871
787
if stop_revision is None:
872
788
stop_revision = other_len
874
assert isinstance(stop_revision, int)
875
if stop_revision > other_len:
876
raise bzrlib.errors.NoSuchRevision(self, stop_revision)
789
elif stop_revision > other_len:
790
raise bzrlib.errors.NoSuchRevision(self, stop_revision)
877
792
return other_history[self_len:stop_revision]
879
795
def update_revisions(self, other, stop_revision=None):
880
"""Pull in new perfect-fit revisions."""
796
"""Pull in all new revisions from other branch.
881
798
from bzrlib.fetch import greedy_fetch
882
if stop_revision is None:
883
stop_revision = other.last_revision()
884
### Should this be checking is_ancestor instead of revision_history?
885
if (stop_revision is not None and
886
stop_revision in self.revision_history()):
888
greedy_fetch(to_branch=self, from_branch=other,
889
revision=stop_revision)
890
pullable_revs = self.pullable_revisions(other, stop_revision)
891
if len(pullable_revs) > 0:
892
self.append_revision(*pullable_revs)
894
def pullable_revisions(self, other, stop_revision):
895
other_revno = other.revision_id_to_revno(stop_revision)
897
return self.missing_revisions(other, other_revno)
898
except DivergedBranches, e:
800
pb = bzrlib.ui.ui_factory.progress_bar()
801
pb.update('comparing histories')
803
revision_ids = self.missing_revisions(other, stop_revision)
805
if len(revision_ids) > 0:
806
count = greedy_fetch(self, other, revision_ids[-1], pb)[0]
809
self.append_revision(*revision_ids)
810
## note("Added %d revisions." % count)
813
def install_revisions(self, other, revision_ids, pb):
814
if hasattr(other.revision_store, "prefetch"):
815
other.revision_store.prefetch(revision_ids)
816
if hasattr(other.inventory_store, "prefetch"):
817
inventory_ids = [other.get_revision(r).inventory_id
818
for r in revision_ids]
819
other.inventory_store.prefetch(inventory_ids)
822
pb = bzrlib.ui.ui_factory.progress_bar()
829
for i, rev_id in enumerate(revision_ids):
830
pb.update('fetching revision', i+1, len(revision_ids))
900
pullable_revs = get_intervening_revisions(self.last_revision(),
902
assert self.last_revision() not in pullable_revs
904
except bzrlib.errors.NotAncestor:
905
if is_ancestor(self.last_revision(), stop_revision, self):
832
rev = other.get_revision(rev_id)
833
except bzrlib.errors.NoSuchRevision:
837
revisions.append(rev)
838
inv = other.get_inventory(str(rev.inventory_id))
839
for key, entry in inv.iter_entries():
840
if entry.text_id is None:
842
if entry.text_id not in self.text_store:
843
needed_texts.add(entry.text_id)
847
count, cp_fail = self.text_store.copy_multi(other.text_store,
849
#print "Added %d texts." % count
850
inventory_ids = [ f.inventory_id for f in revisions ]
851
count, cp_fail = self.inventory_store.copy_multi(other.inventory_store,
853
#print "Added %d inventories." % count
854
revision_ids = [ f.revision_id for f in revisions]
856
count, cp_fail = self.revision_store.copy_multi(other.revision_store,
859
assert len(cp_fail) == 0
860
return count, failures
910
863
def commit(self, *args, **kw):
911
from bzrlib.commit import Commit
912
Commit().commit(self, *args, **kw)
864
from bzrlib.commit import commit
865
commit(self, *args, **kw)
868
def lookup_revision(self, revision):
869
"""Return the revision identifier for a given revision information."""
870
revno, info = self._get_revision_info(revision)
914
874
def revision_id_to_revno(self, revision_id):
915
875
"""Given a revision id, return its revno"""
916
if revision_id is None:
918
876
history = self.revision_history()
920
878
return history.index(revision_id) + 1
921
879
except ValueError:
922
880
raise bzrlib.errors.NoSuchRevision(self, revision_id)
883
def get_revision_info(self, revision):
884
"""Return (revno, revision id) for revision identifier.
886
revision can be an integer, in which case it is assumed to be revno (though
887
this will translate negative values into positive ones)
888
revision can also be a string, in which case it is parsed for something like
889
'date:' or 'revid:' etc.
891
revno, rev_id = self._get_revision_info(revision)
893
raise bzrlib.errors.NoSuchRevision(self, revision)
924
896
def get_rev_id(self, revno, history=None):
925
897
"""Find the revision id of the specified revno."""
931
903
raise bzrlib.errors.NoSuchRevision(self, revno)
932
904
return history[revno - 1]
906
def _get_revision_info(self, revision):
907
"""Return (revno, revision id) for revision specifier.
909
revision can be an integer, in which case it is assumed to be revno
910
(though this will translate negative values into positive ones)
911
revision can also be a string, in which case it is parsed for something
912
like 'date:' or 'revid:' etc.
914
A revid is always returned. If it is None, the specifier referred to
915
the null revision. If the revid does not occur in the revision
916
history, revno will be None.
922
try:# Convert to int if possible
923
revision = int(revision)
926
revs = self.revision_history()
927
if isinstance(revision, int):
929
revno = len(revs) + revision + 1
932
rev_id = self.get_rev_id(revno, revs)
933
elif isinstance(revision, basestring):
934
for prefix, func in Branch.REVISION_NAMESPACES.iteritems():
935
if revision.startswith(prefix):
936
result = func(self, revs, revision)
938
revno, rev_id = result
941
rev_id = self.get_rev_id(revno, revs)
944
raise BzrError('No namespace registered for string: %r' %
947
raise TypeError('Unhandled revision type %s' % revision)
951
raise bzrlib.errors.NoSuchRevision(self, revision)
954
def _namespace_revno(self, revs, revision):
955
"""Lookup a revision by revision number"""
956
assert revision.startswith('revno:')
958
return (int(revision[6:]),)
961
REVISION_NAMESPACES['revno:'] = _namespace_revno
963
def _namespace_revid(self, revs, revision):
964
assert revision.startswith('revid:')
965
rev_id = revision[len('revid:'):]
967
return revs.index(rev_id) + 1, rev_id
970
REVISION_NAMESPACES['revid:'] = _namespace_revid
972
def _namespace_last(self, revs, revision):
973
assert revision.startswith('last:')
975
offset = int(revision[5:])
980
raise BzrError('You must supply a positive value for --revision last:XXX')
981
return (len(revs) - offset + 1,)
982
REVISION_NAMESPACES['last:'] = _namespace_last
984
def _namespace_tag(self, revs, revision):
985
assert revision.startswith('tag:')
986
raise BzrError('tag: namespace registered, but not implemented.')
987
REVISION_NAMESPACES['tag:'] = _namespace_tag
989
def _namespace_date(self, revs, revision):
990
assert revision.startswith('date:')
992
# Spec for date revisions:
994
# value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
995
# it can also start with a '+/-/='. '+' says match the first
996
# entry after the given date. '-' is match the first entry before the date
997
# '=' is match the first entry after, but still on the given date.
999
# +2005-05-12 says find the first matching entry after May 12th, 2005 at 0:00
1000
# -2005-05-12 says find the first matching entry before May 12th, 2005 at 0:00
1001
# =2005-05-12 says find the first match after May 12th, 2005 at 0:00 but before
1002
# May 13th, 2005 at 0:00
1004
# So the proper way of saying 'give me all entries for today' is:
1005
# -r {date:+today}:{date:-tomorrow}
1006
# The default is '=' when not supplied
1009
if val[:1] in ('+', '-', '='):
1010
match_style = val[:1]
1013
today = datetime.datetime.today().replace(hour=0,minute=0,second=0,microsecond=0)
1014
if val.lower() == 'yesterday':
1015
dt = today - datetime.timedelta(days=1)
1016
elif val.lower() == 'today':
1018
elif val.lower() == 'tomorrow':
1019
dt = today + datetime.timedelta(days=1)
1022
# This should be done outside the function to avoid recompiling it.
1023
_date_re = re.compile(
1024
r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
1026
r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
1028
m = _date_re.match(val)
1029
if not m or (not m.group('date') and not m.group('time')):
1030
raise BzrError('Invalid revision date %r' % revision)
1033
year, month, day = int(m.group('year')), int(m.group('month')), int(m.group('day'))
1035
year, month, day = today.year, today.month, today.day
1037
hour = int(m.group('hour'))
1038
minute = int(m.group('minute'))
1039
if m.group('second'):
1040
second = int(m.group('second'))
1044
hour, minute, second = 0,0,0
1046
dt = datetime.datetime(year=year, month=month, day=day,
1047
hour=hour, minute=minute, second=second)
1051
if match_style == '-':
1053
elif match_style == '=':
1054
last = dt + datetime.timedelta(days=1)
1057
for i in range(len(revs)-1, -1, -1):
1058
r = self.get_revision(revs[i])
1059
# TODO: Handle timezone.
1060
dt = datetime.datetime.fromtimestamp(r.timestamp)
1061
if first >= dt and (last is None or dt >= last):
1064
for i in range(len(revs)):
1065
r = self.get_revision(revs[i])
1066
# TODO: Handle timezone.
1067
dt = datetime.datetime.fromtimestamp(r.timestamp)
1068
if first <= dt and (last is None or dt <= last):
1070
REVISION_NAMESPACES['date:'] = _namespace_date
934
1072
def revision_tree(self, revision_id):
935
1073
"""Return Tree for a revision on this branch.
938
1076
an `EmptyTree` is returned."""
939
1077
# TODO: refactor this to use an existing revision object
940
1078
# so we don't need to read it in twice.
941
if revision_id == None or revision_id == NULL_REVISION:
1079
if revision_id == None:
942
1080
return EmptyTree()
944
1082
inv = self.get_revision_inventory(revision_id)
945
return RevisionTree(self.weave_store, inv, revision_id)
1083
return RevisionTree(self.text_store, inv)
947
1086
def working_tree(self):
948
1087
"""Return a `Tree` for the working copy."""
949
1088
from bzrlib.workingtree import WorkingTree
950
# TODO: In the future, perhaps WorkingTree should utilize Transport
951
# RobertCollins 20051003 - I don't think it should - working trees are
952
# much more complex to keep consistent than our careful .bzr subset.
953
# instead, we should say that working trees are local only, and optimise
955
return WorkingTree(self.base, branch=self)
1089
return WorkingTree(self.base, self.read_working_inventory())
958
def pull(self, source, overwrite=False):
962
self.update_revisions(source)
963
except DivergedBranches:
966
self.set_revision_history(source.revision_history())
970
1092
def basis_tree(self):
971
1093
"""Return `Tree` object for last revision.
973
1095
If there are no revisions yet, return an `EmptyTree`.
975
return self.revision_tree(self.last_revision())
1097
r = self.last_patch()
1101
return RevisionTree(self.text_store, self.get_revision_inventory(r))
978
1105
def rename_one(self, from_rel, to_rel):
979
1106
"""Rename one file.
981
1108
This can change the directory or the filename or both.
983
tree = self.working_tree()
985
if not tree.has_filename(from_rel):
986
raise BzrError("can't rename: old working file %r does not exist" % from_rel)
987
if tree.has_filename(to_rel):
988
raise BzrError("can't rename: new working file %r already exists" % to_rel)
990
file_id = inv.path2id(from_rel)
992
raise BzrError("can't rename: old name %r is not versioned" % from_rel)
994
if inv.path2id(to_rel):
995
raise BzrError("can't rename: new name %r is already versioned" % to_rel)
997
to_dir, to_tail = os.path.split(to_rel)
998
to_dir_id = inv.path2id(to_dir)
999
if to_dir_id == None and to_dir != '':
1000
raise BzrError("can't determine destination directory id for %r" % to_dir)
1002
mutter("rename_one:")
1003
mutter(" file_id {%s}" % file_id)
1004
mutter(" from_rel %r" % from_rel)
1005
mutter(" to_rel %r" % to_rel)
1006
mutter(" to_dir %r" % to_dir)
1007
mutter(" to_dir_id {%s}" % to_dir_id)
1009
inv.rename(file_id, to_dir_id, to_tail)
1011
from_abs = self.abspath(from_rel)
1012
to_abs = self.abspath(to_rel)
1014
rename(from_abs, to_abs)
1016
raise BzrError("failed to rename %r to %r: %s"
1017
% (from_abs, to_abs, e[1]),
1018
["rename rolled back"])
1020
self._write_inventory(inv)
1112
tree = self.working_tree()
1113
inv = tree.inventory
1114
if not tree.has_filename(from_rel):
1115
raise BzrError("can't rename: old working file %r does not exist" % from_rel)
1116
if tree.has_filename(to_rel):
1117
raise BzrError("can't rename: new working file %r already exists" % to_rel)
1119
file_id = inv.path2id(from_rel)
1121
raise BzrError("can't rename: old name %r is not versioned" % from_rel)
1123
if inv.path2id(to_rel):
1124
raise BzrError("can't rename: new name %r is already versioned" % to_rel)
1126
to_dir, to_tail = os.path.split(to_rel)
1127
to_dir_id = inv.path2id(to_dir)
1128
if to_dir_id == None and to_dir != '':
1129
raise BzrError("can't determine destination directory id for %r" % to_dir)
1131
mutter("rename_one:")
1132
mutter(" file_id {%s}" % file_id)
1133
mutter(" from_rel %r" % from_rel)
1134
mutter(" to_rel %r" % to_rel)
1135
mutter(" to_dir %r" % to_dir)
1136
mutter(" to_dir_id {%s}" % to_dir_id)
1138
inv.rename(file_id, to_dir_id, to_tail)
1140
from_abs = self.abspath(from_rel)
1141
to_abs = self.abspath(to_rel)
1143
os.rename(from_abs, to_abs)
1145
raise BzrError("failed to rename %r to %r: %s"
1146
% (from_abs, to_abs, e[1]),
1147
["rename rolled back"])
1149
self._write_inventory(inv)
1023
1154
def move(self, from_paths, to_name):
1024
1155
"""Rename files.
1035
1166
entry that is moved.
1038
## TODO: Option to move IDs only
1039
assert not isinstance(from_paths, basestring)
1040
tree = self.working_tree()
1041
inv = tree.inventory
1042
to_abs = self.abspath(to_name)
1043
if not isdir(to_abs):
1044
raise BzrError("destination %r is not a directory" % to_abs)
1045
if not tree.has_filename(to_name):
1046
raise BzrError("destination %r not in working directory" % to_abs)
1047
to_dir_id = inv.path2id(to_name)
1048
if to_dir_id == None and to_name != '':
1049
raise BzrError("destination %r is not a versioned directory" % to_name)
1050
to_dir_ie = inv[to_dir_id]
1051
if to_dir_ie.kind not in ('directory', 'root_directory'):
1052
raise BzrError("destination %r is not a directory" % to_abs)
1054
to_idpath = inv.get_idpath(to_dir_id)
1056
for f in from_paths:
1057
if not tree.has_filename(f):
1058
raise BzrError("%r does not exist in working tree" % f)
1059
f_id = inv.path2id(f)
1061
raise BzrError("%r is not versioned" % f)
1062
name_tail = splitpath(f)[-1]
1063
dest_path = appendpath(to_name, name_tail)
1064
if tree.has_filename(dest_path):
1065
raise BzrError("destination %r already exists" % dest_path)
1066
if f_id in to_idpath:
1067
raise BzrError("can't move %r to a subdirectory of itself" % f)
1069
# OK, so there's a race here, it's possible that someone will
1070
# create a file in this interval and then the rename might be
1071
# left half-done. But we should have caught most problems.
1073
for f in from_paths:
1074
name_tail = splitpath(f)[-1]
1075
dest_path = appendpath(to_name, name_tail)
1076
result.append((f, dest_path))
1077
inv.rename(inv.path2id(f), to_dir_id, name_tail)
1079
rename(self.abspath(f), self.abspath(dest_path))
1081
raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
1082
["rename rolled back"])
1084
self._write_inventory(inv)
1171
## TODO: Option to move IDs only
1172
assert not isinstance(from_paths, basestring)
1173
tree = self.working_tree()
1174
inv = tree.inventory
1175
to_abs = self.abspath(to_name)
1176
if not isdir(to_abs):
1177
raise BzrError("destination %r is not a directory" % to_abs)
1178
if not tree.has_filename(to_name):
1179
raise BzrError("destination %r not in working directory" % to_abs)
1180
to_dir_id = inv.path2id(to_name)
1181
if to_dir_id == None and to_name != '':
1182
raise BzrError("destination %r is not a versioned directory" % to_name)
1183
to_dir_ie = inv[to_dir_id]
1184
if to_dir_ie.kind not in ('directory', 'root_directory'):
1185
raise BzrError("destination %r is not a directory" % to_abs)
1187
to_idpath = inv.get_idpath(to_dir_id)
1189
for f in from_paths:
1190
if not tree.has_filename(f):
1191
raise BzrError("%r does not exist in working tree" % f)
1192
f_id = inv.path2id(f)
1194
raise BzrError("%r is not versioned" % f)
1195
name_tail = splitpath(f)[-1]
1196
dest_path = appendpath(to_name, name_tail)
1197
if tree.has_filename(dest_path):
1198
raise BzrError("destination %r already exists" % dest_path)
1199
if f_id in to_idpath:
1200
raise BzrError("can't move %r to a subdirectory of itself" % f)
1202
# OK, so there's a race here, it's possible that someone will
1203
# create a file in this interval and then the rename might be
1204
# left half-done. But we should have caught most problems.
1206
for f in from_paths:
1207
name_tail = splitpath(f)[-1]
1208
dest_path = appendpath(to_name, name_tail)
1209
result.append((f, dest_path))
1210
inv.rename(inv.path2id(f), to_dir_id, name_tail)
1212
os.rename(self.abspath(f), self.abspath(dest_path))
1214
raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
1215
["rename rolled back"])
1217
self._write_inventory(inv)
1215
1355
if revno < 1 or revno > self.revno():
1216
1356
raise InvalidRevisionNumber(revno)
1218
def sign_revision(self, revision_id, gpg_strategy):
1219
plaintext = Testament.from_revision(self, revision_id).as_short_text()
1220
self.store_revision_signature(gpg_strategy, plaintext, revision_id)
1223
def store_revision_signature(self, gpg_strategy, plaintext, revision_id):
1224
self.revision_store.add(StringIO(gpg_strategy.sign(plaintext)),
1228
class ScratchBranch(_Branch):
1361
class ScratchBranch(Branch):
1229
1362
"""Special test class: a branch that cleans up after itself.
1231
1364
>>> b = ScratchBranch()
1232
1365
>>> isdir(b.base)
1234
1367
>>> bd = b.base
1235
>>> b._transport.__del__()
1240
def __init__(self, files=[], dirs=[], transport=None):
1372
def __init__(self, files=[], dirs=[], base=None):
1241
1373
"""Make a test branch.
1243
1375
This creates a temporary directory and runs init-tree in it.
1245
1377
If any files are listed, they are created in the working copy.
1247
if transport is None:
1248
transport = bzrlib.transport.local.ScratchTransport()
1249
super(ScratchBranch, self).__init__(transport, init=True)
1251
super(ScratchBranch, self).__init__(transport)
1379
from tempfile import mkdtemp
1384
Branch.__init__(self, base, init=init)
1254
self._transport.mkdir(d)
1386
os.mkdir(self.abspath(d))
1256
1388
for f in files:
1257
self._transport.put(f, 'content of %s' % f)
1389
file(os.path.join(self.base, f), 'w').write('content of %s' % f)
1260
1392
def clone(self):
1262
1394
>>> orig = ScratchBranch(files=["file1", "file2"])
1263
1395
>>> clone = orig.clone()
1264
>>> if os.name != 'nt':
1265
... os.path.samefile(orig.base, clone.base)
1267
... orig.base == clone.base
1396
>>> os.path.samefile(orig.base, clone.base)
1270
1398
>>> os.path.isfile(os.path.join(clone.base, "file1"))
1332
1482
return gen_file_id('TREE_ROOT')
1485
def copy_branch(branch_from, to_location, revision=None):
1486
"""Copy branch_from into the existing directory to_location.
1489
If not None, only revisions up to this point will be copied.
1490
The head of the new branch will be that revision.
1493
The name of a local directory that exists but is empty.
1495
from bzrlib.merge import merge
1497
assert isinstance(branch_from, Branch)
1498
assert isinstance(to_location, basestring)
1500
br_to = Branch(to_location, init=True)
1501
br_to.set_root_id(branch_from.get_root_id())
1502
if revision is None:
1503
revno = branch_from.revno()
1505
revno, rev_id = branch_from.get_revision_info(revision)
1506
br_to.update_revisions(branch_from, stop_revision=revno)
1507
merge((to_location, -1), (to_location, 0), this_dir=to_location,
1508
check_clean=False, ignore_zero=True)
1510
from_location = branch_from.base
1511
br_to.set_parent(branch_from.base)