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
26
import xml.sax.saxutils
20
import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile
21
import traceback, socket, fnmatch, difflib, time
22
from binascii import hexlify
25
from inventory import Inventory
26
from trace import mutter, note
27
from tree import Tree, EmptyTree, RevisionTree, WorkingTree
28
from inventory import InventoryEntry, Inventory
29
from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \
30
format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \
31
joinpath, sha_string, file_kind, local_time_offset, appendpath
32
from store import ImmutableStore
33
from revision import Revision
34
from errors import bailout, BzrError
35
from textui import show_status
36
from diff import diff_trees
38
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
30
from bzrlib.config import TreeConfig
31
from bzrlib.delta import compare_trees
32
import bzrlib.errors as errors
33
from bzrlib.errors import (BzrError, InvalidRevisionNumber, InvalidRevisionId,
34
NoSuchRevision, HistoryMissing, NotBranchError,
35
DivergedBranches, LockError,
36
UninitializableFormat,
38
UnlistableBranch, NoSuchFile, NotVersionedError,
40
import bzrlib.inventory as inventory
41
from bzrlib.inventory import Inventory
42
from bzrlib.osutils import (isdir, quotefn,
43
rename, splitpath, sha_file,
44
file_kind, abspath, normpath, pathjoin,
47
from bzrlib.textui import show_status
48
from bzrlib.trace import mutter, note
49
from bzrlib.tree import EmptyTree, RevisionTree
50
from bzrlib.revision import (Revision, is_ancestor, get_intervening_revisions,
52
from bzrlib.store import copy_all
53
from bzrlib.store.text import TextStore
54
from bzrlib.store.weave import WeaveStore
55
from bzrlib.symbol_versioning import *
56
from bzrlib.testament import Testament
57
import bzrlib.transactions as transactions
58
from bzrlib.transport import Transport, get_transport
63
BZR_BRANCH_FORMAT_4 = "Bazaar-NG branch, format 0.0.4\n"
64
BZR_BRANCH_FORMAT_5 = "Bazaar-NG branch, format 5\n"
65
BZR_BRANCH_FORMAT_6 = "Bazaar-NG branch, format 6\n"
39
66
## TODO: Maybe include checks for common corruption of newlines, etc?
43
def find_branch_root(f=None):
44
"""Find the branch root enclosing f, or pwd.
46
It is not necessary that f exists.
48
Basically we keep looking up until we find the control directory or
52
elif hasattr(os.path, 'realpath'):
53
f = os.path.realpath(f)
55
f = os.path.abspath(f)
60
if os.path.exists(os.path.join(f, bzrlib.BZRDIR)):
62
head, tail = os.path.split(f)
64
# reached the root, whatever that may be
65
raise BzrError('%r is not in a branch' % orig_f)
69
# TODO: Some operations like log might retrieve the same revisions
70
# repeatedly to calculate deltas. We could perhaps have a weakref
71
# cache in memory to make this faster. In general anything can be
72
# cached in memory between lock and unlock operations.
74
def find_branch(*ignored, **ignored_too):
75
# XXX: leave this here for about one release, then remove it
76
raise NotImplementedError('find_branch() is not supported anymore, '
77
'please use one of the new branch constructors')
80
def needs_read_lock(unbound):
81
"""Decorate unbound to take out and release a read lock."""
82
def decorated(self, *args, **kwargs):
85
return unbound(self, *args, **kwargs)
91
def needs_write_lock(unbound):
92
"""Decorate unbound to take out and release a write lock."""
93
def decorated(self, *args, **kwargs):
96
return unbound(self, *args, **kwargs)
70
101
######################################################################
104
class Branch(object):
74
105
"""Branch holding a history of revisions.
76
TODO: Perhaps use different stores for different classes of object,
77
so that we can keep track of how much space each one uses,
78
or garbage-collect them.
80
TODO: Add a RemoteBranch subclass. For the basic case of read-only
81
HTTP access this should be very easy by,
82
just redirecting controlfile access into HTTP requests.
83
We would need a RemoteStore working similarly.
85
TODO: Keep the on-disk branch locked while the object exists.
108
Base directory/url of the branch.
89
def __init__(self, base, init=False, find_root=True):
90
"""Create new branch object at a particular location.
92
base -- Base directory for the branch.
94
init -- If True, create new control files in a previously
95
unversioned directory. If False, the branch must already
98
find_root -- If true and init is false, find the root of the
99
existing branch containing base.
101
In the test suite, creation of new trees is tested using the
102
`ScratchBranch` class.
105
self.base = os.path.realpath(base)
108
self.base = find_branch_root(base)
110
self.base = os.path.realpath(base)
111
if not isdir(self.controlfilename('.')):
112
bailout("not a bzr branch: %s" % quotefn(base),
113
['use "bzr init" to initialize a new working tree',
114
'current bzr can only operate from top-of-tree'])
117
self.text_store = ImmutableStore(self.controlfilename('text-store'))
118
self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
119
self.inventory_store = ImmutableStore(self.controlfilename('inventory-store'))
123
return '%s(%r)' % (self.__class__.__name__, self.base)
110
# this is really an instance variable - FIXME move it there
114
_default_initializer = None
115
"""The default initializer for making new branches."""
117
def __init__(self, *ignored, **ignored_too):
118
raise NotImplementedError('The Branch class is abstract')
121
def open_downlevel(base):
122
"""Open a branch which may be of an old format."""
123
return Branch.open(base, _unsupported=True)
126
def open(base, _unsupported=False):
127
"""Open an existing branch, rooted at 'base' (url)
129
_unsupported is a private parameter to the Branch class.
131
t = get_transport(base)
132
mutter("trying to open %r with transport %r", base, t)
133
format = BzrBranchFormat.find_format(t)
134
if not _unsupported and not format.is_supported():
135
# see open_downlevel to open legacy branches.
136
raise errors.UnsupportedFormatError(
137
'sorry, branch format %s not supported' % format,
138
['use a different bzr version',
139
'or remove the .bzr directory'
140
' and "bzr init" again'])
141
return format.open(t)
144
def open_containing(url):
145
"""Open an existing branch which contains url.
147
This probes for a branch at url, and searches upwards from there.
149
Basically we keep looking up until we find the control directory or
150
run into the root. If there isn't one, raises NotBranchError.
151
If there is one and it is either an unrecognised format or an unsupported
152
format, UnknownFormatError or UnsupportedFormatError are raised.
153
If there is one, it is returned, along with the unused portion of url.
155
t = get_transport(url)
156
# this gets the normalised url back. I.e. '.' -> the full path.
160
format = BzrBranchFormat.find_format(t)
161
return format.open(t), t.relpath(url)
162
except NotBranchError, e:
163
mutter('not a branch in: %r %s', t.base, e)
164
new_t = t.clone('..')
165
if new_t.base == t.base:
166
# reached the root, whatever that may be
167
raise NotBranchError(path=url)
172
"""Create a new Branch at the url 'bzr'.
174
This will call the current default initializer with base
175
as the only parameter.
177
return Branch._default_initializer(safe_unicode(base))
180
@deprecated_function(zero_eight)
181
def initialize(base):
182
"""Create a new working tree and branch, rooted at 'base' (url)
184
# imported here to prevent scope creep as this is going.
185
from bzrlib.workingtree import WorkingTree
186
return WorkingTree.create_standalone(safe_unicode(base)).branch
189
def get_default_initializer():
190
"""Return the initializer being used for new branches."""
191
return Branch._default_initializer
194
def set_default_initializer(initializer):
195
"""Set the initializer to be used for new branches."""
196
Branch._default_initializer = staticmethod(initializer)
198
def setup_caching(self, cache_root):
199
"""Subclasses that care about caching should override this, and set
200
up cached stores located under cache_root.
202
self.cache_root = cache_root
205
cfg = self.tree_config()
206
return cfg.get_option(u"nickname", default=self.base.split('/')[-2])
208
def _set_nick(self, nick):
209
cfg = self.tree_config()
210
cfg.set_option(nick, "nickname")
211
assert cfg.get_option("nickname") == nick
213
nick = property(_get_nick, _set_nick)
215
def push_stores(self, branch_to):
216
"""Copy the content of this branches store to branch_to."""
217
raise NotImplementedError('push_stores is abstract')
219
def get_transaction(self):
220
"""Return the current active transaction.
222
If no transaction is active, this returns a passthrough object
223
for which all data is immediately flushed and no caching happens.
225
raise NotImplementedError('get_transaction is abstract')
227
def lock_write(self):
228
raise NotImplementedError('lock_write is abstract')
231
raise NotImplementedError('lock_read is abstract')
234
raise NotImplementedError('unlock is abstract')
129
236
def abspath(self, name):
130
"""Return absolute filename for something in the branch"""
131
return os.path.join(self.base, name)
134
def relpath(self, path):
135
"""Return path relative to this branch of something inside it.
137
Raises an error if path is not in this branch."""
138
rp = os.path.realpath(path)
140
if not rp.startswith(self.base):
141
bailout("path %r is not within branch %r" % (rp, self.base))
142
rp = rp[len(self.base):]
143
rp = rp.lstrip(os.sep)
237
"""Return absolute filename for something in the branch
239
XXX: Robert Collins 20051017 what is this used for? why is it a branch
240
method and not a tree method.
242
raise NotImplementedError('abspath is abstract')
147
244
def controlfilename(self, file_or_path):
148
245
"""Return location relative to branch."""
149
if isinstance(file_or_path, types.StringTypes):
150
file_or_path = [file_or_path]
151
return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
246
raise NotImplementedError('controlfilename is abstract')
154
248
def controlfile(self, file_or_path, mode='r'):
155
249
"""Open a control file for this branch.
158
252
and binary. binary files are untranslated byte streams. Text
159
253
control files are stored with Unix newlines and in UTF-8, even
160
254
if the platform or locale defaults are different.
163
fn = self.controlfilename(file_or_path)
165
if mode == 'rb' or mode == 'wb':
166
return file(fn, mode)
167
elif mode == 'r' or mode == 'w':
168
# open in binary mode anyhow so there's no newline translation;
169
# codecs uses line buffering by default; don't want that.
171
return codecs.open(fn, mode + 'b', 'utf-8',
174
raise BzrError("invalid controlfile mode %r" % mode)
178
def _make_control(self):
179
os.mkdir(self.controlfilename([]))
180
self.controlfile('README', 'w').write(
181
"This is a Bazaar-NG control directory.\n"
182
"Do not change any files in this directory.")
183
self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT)
184
for d in ('text-store', 'inventory-store', 'revision-store'):
185
os.mkdir(self.controlfilename(d))
186
for f in ('revision-history', 'merged-patches',
187
'pending-merged-patches', 'branch-name'):
188
self.controlfile(f, 'w').write('')
189
mutter('created control directory in ' + self.base)
190
Inventory().write_xml(self.controlfile('inventory','w'))
193
def _check_format(self):
194
"""Check this branch format is supported.
196
The current tool only supports the current unstable format.
198
In the future, we might need different in-memory Branch
199
classes to support downlevel branches. But not yet.
201
# This ignores newlines so that we can open branches created
202
# on Windows from Linux and so on. I think it might be better
203
# to always make all internal files in unix format.
204
fmt = self.controlfile('branch-format', 'r').read()
205
fmt.replace('\r\n', '')
206
if fmt != BZR_BRANCH_FORMAT:
207
bailout('sorry, branch format %r not supported' % fmt,
208
['use a different bzr version',
209
'or remove the .bzr directory and "bzr init" again'])
212
def read_working_inventory(self):
213
"""Read the working inventory."""
215
# ElementTree does its own conversion from UTF-8, so open in
217
inv = Inventory.read_xml(self.controlfile('inventory', 'rb'))
218
mutter("loaded inventory of %d items in %f"
219
% (len(inv), time.time() - before))
223
def _write_inventory(self, inv):
224
"""Update the working inventory.
226
That is to say, the inventory describing changes underway, that
227
will be committed to the next revision.
229
## TODO: factor out to atomicfile? is rename safe on windows?
230
## TODO: Maybe some kind of clean/dirty marker on inventory?
231
tmpfname = self.controlfilename('inventory.tmp')
232
tmpf = file(tmpfname, 'wb')
235
inv_fname = self.controlfilename('inventory')
236
if sys.platform == 'win32':
238
os.rename(tmpfname, inv_fname)
239
mutter('wrote working inventory')
242
inventory = property(read_working_inventory, _write_inventory, None,
243
"""Inventory for the working copy.""")
246
def add(self, files, verbose=False):
247
"""Make files versioned.
249
Note that the command line normally calls smart_add instead.
251
This puts the files in the Added state, so that they will be
252
recorded by the next commit.
254
TODO: Perhaps have an option to add the ids even if the files do
257
TODO: Perhaps return the ids of the files? But then again it
258
is easy to retrieve them if they're needed.
260
TODO: Option to specify file id.
262
TODO: Adding a directory should optionally recurse down and
263
add all non-ignored children. Perhaps do that in a
266
>>> b = ScratchBranch(files=['foo'])
267
>>> 'foo' in b.unknowns()
272
>>> 'foo' in b.unknowns()
274
>>> bool(b.inventory.path2id('foo'))
280
Traceback (most recent call last):
282
BzrError: ('foo is already versioned', [])
284
>>> b.add(['nothere'])
285
Traceback (most recent call last):
286
BzrError: ('cannot add: not a regular file or directory: nothere', [])
289
# TODO: Re-adding a file that is removed in the working copy
290
# should probably put it back with the previous ID.
291
if isinstance(files, types.StringTypes):
294
inv = self.read_working_inventory()
296
if is_control_file(f):
297
bailout("cannot add control file %s" % quotefn(f))
302
bailout("cannot add top-level %r" % f)
304
fullpath = os.path.normpath(self.abspath(f))
307
kind = file_kind(fullpath)
309
# maybe something better?
310
bailout('cannot add: not a regular file or directory: %s' % quotefn(f))
312
if kind != 'file' and kind != 'directory':
313
bailout('cannot add: not a regular file or directory: %s' % quotefn(f))
315
file_id = gen_file_id(f)
316
inv.add_path(f, kind=kind, file_id=file_id)
319
show_status('A', kind, quotefn(f))
321
mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
323
self._write_inventory(inv)
326
def print_file(self, file, revno):
256
Controlfiles should almost never be opened in write mode but
257
rather should be atomically copied and replaced using atomicfile.
259
raise NotImplementedError('controlfile is abstract')
261
def put_controlfile(self, path, f, encode=True):
262
"""Write an entry as a controlfile.
264
:param path: The path to put the file, relative to the .bzr control
266
:param f: A file-like or string object whose contents should be copied.
267
:param encode: If true, encode the contents as utf-8
269
raise NotImplementedError('put_controlfile is abstract')
271
def put_controlfiles(self, files, encode=True):
272
"""Write several entries as controlfiles.
274
:param files: A list of [(path, file)] pairs, where the path is the directory
275
underneath the bzr control directory
276
:param encode: If true, encode the contents as utf-8
278
raise NotImplementedError('put_controlfiles is abstract')
280
def get_root_id(self):
281
"""Return the id of this branches root"""
282
raise NotImplementedError('get_root_id is abstract')
284
def set_root_id(self, file_id):
285
raise NotImplementedError('set_root_id is abstract')
287
def print_file(self, file, revision_id):
327
288
"""Print `file` to stdout."""
328
tree = self.revision_tree(self.lookup_revision(revno))
329
# use inventory as it was in that revision
330
file_id = tree.inventory.path2id(file)
332
bailout("%r is not present in revision %d" % (file, revno))
333
tree.print_file(file_id)
336
def remove(self, files, verbose=False):
337
"""Mark nominated files for removal from the inventory.
339
This does not remove their text. This does not run on
341
TODO: Refuse to remove modified files unless --force is given?
343
>>> b = ScratchBranch(files=['foo'])
345
>>> b.inventory.has_filename('foo')
348
>>> b.working_tree().has_filename('foo')
350
>>> b.inventory.has_filename('foo')
353
>>> b = ScratchBranch(files=['foo'])
358
>>> b.inventory.has_filename('foo')
360
>>> b.basis_tree().has_filename('foo')
362
>>> b.working_tree().has_filename('foo')
365
TODO: Do something useful with directories.
367
TODO: Should this remove the text or not? Tough call; not
368
removing may be useful and the user can just use use rm, and
369
is the opposite of add. Removing it is consistent with most
370
other tools. Maybe an option.
372
## TODO: Normalize names
373
## TODO: Remove nested loops; better scalability
375
if isinstance(files, types.StringTypes):
378
tree = self.working_tree()
381
# do this before any modifications
385
bailout("cannot remove unversioned file %s" % quotefn(f))
386
mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
388
# having remove it, it must be either ignored or unknown
389
if tree.is_ignored(f):
393
show_status(new_status, inv[fid].kind, quotefn(f))
396
self._write_inventory(inv)
400
"""Return all unknown files.
402
These are files in the working directory that are not versioned or
403
control files or ignored.
405
>>> b = ScratchBranch(files=['foo', 'foo~'])
406
>>> list(b.unknowns())
409
>>> list(b.unknowns())
412
>>> list(b.unknowns())
415
return self.working_tree().unknowns()
418
def commit(self, message, timestamp=None, timezone=None,
421
"""Commit working copy as a new revision.
423
The basic approach is to add all the file texts into the
424
store, then the inventory, then make a new revision pointing
425
to that inventory and store that.
427
This is not quite safe if the working copy changes during the
428
commit; for the moment that is simply not allowed. A better
429
approach is to make a temporary copy of the files before
430
computing their hashes, and then add those hashes in turn to
431
the inventory. This should mean at least that there are no
432
broken hash pointers. There is no way we can get a snapshot
433
of the whole directory at an instant. This would also have to
434
be robust against files disappearing, moving, etc. So the
435
whole thing is a bit hard.
437
timestamp -- if not None, seconds-since-epoch for a
438
postdated/predated commit.
441
## TODO: Show branch names
443
# TODO: Don't commit if there are no changes, unless forced?
445
# First walk over the working inventory; and both update that
446
# and also build a new revision inventory. The revision
447
# inventory needs to hold the text-id, sha1 and size of the
448
# actual file versions committed in the revision. (These are
449
# not present in the working inventory.) We also need to
450
# detect missing/deleted files, and remove them from the
453
work_inv = self.read_working_inventory()
455
basis = self.basis_tree()
456
basis_inv = basis.inventory
458
for path, entry in work_inv.iter_entries():
459
## TODO: Cope with files that have gone missing.
461
## TODO: Check that the file kind has not changed from the previous
462
## revision of this file (if any).
466
p = self.abspath(path)
467
file_id = entry.file_id
468
mutter('commit prep file %s, id %r ' % (p, file_id))
470
if not os.path.exists(p):
471
mutter(" file is missing, removing from inventory")
473
show_status('D', entry.kind, quotefn(path))
474
missing_ids.append(file_id)
477
# TODO: Handle files that have been deleted
479
# TODO: Maybe a special case for empty files? Seems a
480
# waste to store them many times.
484
if basis_inv.has_id(file_id):
485
old_kind = basis_inv[file_id].kind
486
if old_kind != entry.kind:
487
bailout("entry %r changed kind from %r to %r"
488
% (file_id, old_kind, entry.kind))
490
if entry.kind == 'directory':
492
bailout("%s is entered as directory but not a directory" % quotefn(p))
493
elif entry.kind == 'file':
495
bailout("%s is entered as file but is not a file" % quotefn(p))
497
content = file(p, 'rb').read()
499
entry.text_sha1 = sha_string(content)
500
entry.text_size = len(content)
502
old_ie = basis_inv.has_id(file_id) and basis_inv[file_id]
504
and (old_ie.text_size == entry.text_size)
505
and (old_ie.text_sha1 == entry.text_sha1)):
506
## assert content == basis.get_file(file_id).read()
507
entry.text_id = basis_inv[file_id].text_id
508
mutter(' unchanged from previous text_id {%s}' %
512
entry.text_id = gen_file_id(entry.name)
513
self.text_store.add(content, entry.text_id)
514
mutter(' stored with text_id {%s}' % entry.text_id)
518
elif (old_ie.name == entry.name
519
and old_ie.parent_id == entry.parent_id):
524
show_status(state, entry.kind, quotefn(path))
526
for file_id in missing_ids:
527
# have to do this later so we don't mess up the iterator.
528
# since parents may be removed before their children we
531
# FIXME: There's probably a better way to do this; perhaps
532
# the workingtree should know how to filter itself.
533
if work_inv.has_id(file_id):
534
del work_inv[file_id]
537
inv_id = rev_id = _gen_revision_id(time.time())
539
inv_tmp = tempfile.TemporaryFile()
540
inv.write_xml(inv_tmp)
542
self.inventory_store.add(inv_tmp, inv_id)
543
mutter('new inventory_id is {%s}' % inv_id)
545
self._write_inventory(work_inv)
547
if timestamp == None:
548
timestamp = time.time()
550
if committer == None:
551
committer = username()
554
timezone = local_time_offset()
556
mutter("building commit log message")
557
rev = Revision(timestamp=timestamp,
560
precursor = self.last_patch(),
565
rev_tmp = tempfile.TemporaryFile()
566
rev.write_xml(rev_tmp)
568
self.revision_store.add(rev_tmp, rev_id)
569
mutter("new revision_id is {%s}" % rev_id)
571
## XXX: Everything up to here can simply be orphaned if we abort
572
## the commit; it will leave junk files behind but that doesn't
575
## TODO: Read back the just-generated changeset, and make sure it
576
## applies and recreates the right state.
578
## TODO: Also calculate and store the inventory SHA1
579
mutter("committing patch r%d" % (self.revno() + 1))
582
self.append_revision(rev_id)
585
note("commited r%d" % self.revno())
588
def append_revision(self, revision_id):
589
mutter("add {%s} to revision-history" % revision_id)
590
rev_history = self.revision_history()
592
tmprhname = self.controlfilename('revision-history.tmp')
593
rhname = self.controlfilename('revision-history')
595
f = file(tmprhname, 'wt')
596
rev_history.append(revision_id)
597
f.write('\n'.join(rev_history))
601
if sys.platform == 'win32':
603
os.rename(tmprhname, rhname)
289
raise NotImplementedError('print_file is abstract')
291
def append_revision(self, *revision_ids):
292
raise NotImplementedError('append_revision is abstract')
294
def set_revision_history(self, rev_history):
295
raise NotImplementedError('set_revision_history is abstract')
297
def has_revision(self, revision_id):
298
"""True if this branch has a copy of the revision.
300
This does not necessarily imply the revision is merge
301
or on the mainline."""
302
raise NotImplementedError('has_revision is abstract')
304
def get_revision_xml(self, revision_id):
305
raise NotImplementedError('get_revision_xml is abstract')
607
307
def get_revision(self, revision_id):
608
308
"""Return the Revision object for a named revision"""
609
r = Revision.read_xml(self.revision_store[revision_id])
610
assert r.revision_id == revision_id
614
def get_inventory(self, inventory_id):
615
"""Get Inventory object by hash.
617
TODO: Perhaps for this and similar methods, take a revision
618
parameter which can be either an integer revno or a
620
i = Inventory.read_xml(self.inventory_store[inventory_id])
309
raise NotImplementedError('get_revision is abstract')
311
def get_revision_delta(self, revno):
312
"""Return the delta for one revision.
314
The delta is relative to its mainline predecessor, or the
315
empty tree for revision 1.
317
assert isinstance(revno, int)
318
rh = self.revision_history()
319
if not (1 <= revno <= len(rh)):
320
raise InvalidRevisionNumber(revno)
322
# revno is 1-based; list is 0-based
324
new_tree = self.revision_tree(rh[revno-1])
326
old_tree = EmptyTree()
328
old_tree = self.revision_tree(rh[revno-2])
330
return compare_trees(old_tree, new_tree)
332
def get_revision_sha1(self, revision_id):
333
"""Hash the stored value of a revision, and return it."""
334
raise NotImplementedError('get_revision_sha1 is abstract')
336
def get_ancestry(self, revision_id):
337
"""Return a list of revision-ids integrated by a revision.
339
This currently returns a list, but the ordering is not guaranteed:
342
raise NotImplementedError('get_ancestry is abstract')
344
def get_inventory(self, revision_id):
345
"""Get Inventory object by hash."""
346
raise NotImplementedError('get_inventory is abstract')
348
def get_inventory_xml(self, revision_id):
349
"""Get inventory XML as a file object."""
350
raise NotImplementedError('get_inventory_xml is abstract')
352
def get_inventory_sha1(self, revision_id):
353
"""Return the sha1 hash of the inventory entry."""
354
raise NotImplementedError('get_inventory_sha1 is abstract')
624
356
def get_revision_inventory(self, revision_id):
625
357
"""Return inventory of a past revision."""
626
if revision_id == None:
629
return self.get_inventory(self.get_revision(revision_id).inventory_id)
358
raise NotImplementedError('get_revision_inventory is abstract')
632
360
def revision_history(self):
633
"""Return sequence of revision hashes on to this branch.
635
>>> ScratchBranch().revision_history()
638
return [chomp(l) for l in self.controlfile('revision-history', 'r').readlines()]
361
"""Return sequence of revision hashes on to this branch."""
362
raise NotImplementedError('revision_history is abstract')
642
365
"""Return current revision number for this branch.
644
367
That is equivalent to the number of revisions committed to
647
>>> b = ScratchBranch()
650
>>> b.commit('no foo')
654
370
return len(self.revision_history())
657
def last_patch(self):
658
"""Return last patch hash, or None if no history.
660
>>> ScratchBranch().last_patch() == None
372
def last_revision(self):
373
"""Return last patch hash, or None if no history."""
663
374
ph = self.revision_history()
670
def lookup_revision(self, revno):
671
"""Return revision hash for revision number."""
380
def missing_revisions(self, other, stop_revision=None, diverged_ok=False):
381
"""Return a list of new revisions that would perfectly fit.
383
If self and other have not diverged, return a list of the revisions
384
present in other, but missing from self.
386
>>> bzrlib.trace.silent = True
387
>>> br1 = ScratchBranch()
388
>>> br2 = ScratchBranch()
389
>>> br1.missing_revisions(br2)
391
>>> br2.working_tree().commit("lala!", rev_id="REVISION-ID-1")
392
>>> br1.missing_revisions(br2)
394
>>> br2.missing_revisions(br1)
396
>>> br1.working_tree().commit("lala!", rev_id="REVISION-ID-1")
397
>>> br1.missing_revisions(br2)
399
>>> br2.working_tree().commit("lala!", rev_id="REVISION-ID-2A")
400
>>> br1.missing_revisions(br2)
402
>>> br1.working_tree().commit("lala!", rev_id="REVISION-ID-2B")
403
>>> br1.missing_revisions(br2)
404
Traceback (most recent call last):
405
DivergedBranches: These branches have diverged. Try merge.
407
self_history = self.revision_history()
408
self_len = len(self_history)
409
other_history = other.revision_history()
410
other_len = len(other_history)
411
common_index = min(self_len, other_len) -1
412
if common_index >= 0 and \
413
self_history[common_index] != other_history[common_index]:
414
raise DivergedBranches(self, other)
416
if stop_revision is None:
417
stop_revision = other_len
419
assert isinstance(stop_revision, int)
420
if stop_revision > other_len:
421
raise bzrlib.errors.NoSuchRevision(self, stop_revision)
422
return other_history[self_len:stop_revision]
424
def update_revisions(self, other, stop_revision=None):
425
"""Pull in new perfect-fit revisions."""
426
raise NotImplementedError('update_revisions is abstract')
428
def pullable_revisions(self, other, stop_revision):
429
raise NotImplementedError('pullable_revisions is abstract')
431
def revision_id_to_revno(self, revision_id):
432
"""Given a revision id, return its revno"""
433
if revision_id is None:
435
history = self.revision_history()
437
return history.index(revision_id) + 1
439
raise bzrlib.errors.NoSuchRevision(self, revision_id)
441
def get_rev_id(self, revno, history=None):
442
"""Find the revision id of the specified revno."""
676
# list is 0-based; revisions are 1-based
677
return self.revision_history()[revno-1]
679
raise BzrError("no such revision %s" % revno)
446
history = self.revision_history()
447
elif revno <= 0 or revno > len(history):
448
raise bzrlib.errors.NoSuchRevision(self, revno)
449
return history[revno - 1]
682
451
def revision_tree(self, revision_id):
683
452
"""Return Tree for a revision on this branch.
685
454
`revision_id` may be None for the null revision, in which case
686
455
an `EmptyTree` is returned."""
688
if revision_id == None:
691
inv = self.get_revision_inventory(revision_id)
692
return RevisionTree(self.text_store, inv)
456
raise NotImplementedError('revision_tree is abstract')
695
458
def working_tree(self):
696
"""Return a `Tree` for the working copy."""
697
return WorkingTree(self.base, self.read_working_inventory())
459
"""Return a `Tree` for the working copy if this is a local branch."""
460
raise NotImplementedError('working_tree is abstract')
462
def pull(self, source, overwrite=False):
463
raise NotImplementedError('pull is abstract')
700
465
def basis_tree(self):
701
466
"""Return `Tree` object for last revision.
703
468
If there are no revisions yet, return an `EmptyTree`.
705
>>> b = ScratchBranch(files=['foo'])
706
>>> b.basis_tree().has_filename('foo')
708
>>> b.working_tree().has_filename('foo')
711
>>> b.commit('add foo')
712
>>> b.basis_tree().has_filename('foo')
715
r = self.last_patch()
719
return RevisionTree(self.text_store, self.get_revision_inventory(r))
723
def write_log(self, show_timezone='original', verbose=False):
724
"""Write out human-readable log of commits to this branch
726
utc -- If true, show dates in universal time, not local time."""
727
## TODO: Option to choose either original, utc or local timezone
730
for p in self.revision_history():
732
print 'revno:', revno
733
## TODO: Show hash if --id is given.
734
##print 'revision-hash:', p
735
rev = self.get_revision(p)
736
print 'committer:', rev.committer
737
print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0,
740
## opportunistic consistency check, same as check_patch_chaining
741
if rev.precursor != precursor:
742
bailout("mismatched precursor!")
746
print ' (no message)'
748
for l in rev.message.split('\n'):
751
if verbose == True and precursor != None:
752
print 'changed files:'
753
tree = self.revision_tree(p)
754
prevtree = self.revision_tree(precursor)
756
for file_state, fid, old_name, new_name, kind in \
757
diff_trees(prevtree, tree, ):
758
if file_state == 'A' or file_state == 'M':
759
show_status(file_state, kind, new_name)
760
elif file_state == 'D':
761
show_status(file_state, kind, old_name)
762
elif file_state == 'R':
763
show_status(file_state, kind,
764
old_name + ' => ' + new_name)
470
return self.revision_tree(self.last_revision())
770
472
def rename_one(self, from_rel, to_rel):
771
tree = self.working_tree()
773
if not tree.has_filename(from_rel):
774
bailout("can't rename: old working file %r does not exist" % from_rel)
775
if tree.has_filename(to_rel):
776
bailout("can't rename: new working file %r already exists" % to_rel)
778
file_id = inv.path2id(from_rel)
780
bailout("can't rename: old name %r is not versioned" % from_rel)
782
if inv.path2id(to_rel):
783
bailout("can't rename: new name %r is already versioned" % to_rel)
785
to_dir, to_tail = os.path.split(to_rel)
786
to_dir_id = inv.path2id(to_dir)
787
if to_dir_id == None and to_dir != '':
788
bailout("can't determine destination directory id for %r" % to_dir)
790
mutter("rename_one:")
791
mutter(" file_id {%s}" % file_id)
792
mutter(" from_rel %r" % from_rel)
793
mutter(" to_rel %r" % to_rel)
794
mutter(" to_dir %r" % to_dir)
795
mutter(" to_dir_id {%s}" % to_dir_id)
797
inv.rename(file_id, to_dir_id, to_tail)
799
print "%s => %s" % (from_rel, to_rel)
801
from_abs = self.abspath(from_rel)
802
to_abs = self.abspath(to_rel)
804
os.rename(from_abs, to_abs)
806
bailout("failed to rename %r to %r: %s"
807
% (from_abs, to_abs, e[1]),
808
["rename rolled back"])
810
self._write_inventory(inv)
475
This can change the directory or the filename or both.
477
raise NotImplementedError('rename_one is abstract')
814
479
def move(self, from_paths, to_name):
822
487
Note that to_name is only the last component of the new name;
823
488
this doesn't change the directory.
825
## TODO: Option to move IDs only
826
assert not isinstance(from_paths, basestring)
827
tree = self.working_tree()
829
to_abs = self.abspath(to_name)
830
if not isdir(to_abs):
831
bailout("destination %r is not a directory" % to_abs)
832
if not tree.has_filename(to_name):
833
bailout("destination %r not in working directory" % to_abs)
834
to_dir_id = inv.path2id(to_name)
835
if to_dir_id == None and to_name != '':
836
bailout("destination %r is not a versioned directory" % to_name)
837
to_dir_ie = inv[to_dir_id]
838
if to_dir_ie.kind not in ('directory', 'root_directory'):
839
bailout("destination %r is not a directory" % to_abs)
841
to_idpath = Set(inv.get_idpath(to_dir_id))
844
if not tree.has_filename(f):
845
bailout("%r does not exist in working tree" % f)
846
f_id = inv.path2id(f)
848
bailout("%r is not versioned" % f)
849
name_tail = splitpath(f)[-1]
850
dest_path = appendpath(to_name, name_tail)
851
if tree.has_filename(dest_path):
852
bailout("destination %r already exists" % dest_path)
853
if f_id in to_idpath:
854
bailout("can't move %r to a subdirectory of itself" % f)
856
# OK, so there's a race here, it's possible that someone will
857
# create a file in this interval and then the rename might be
858
# left half-done. But we should have caught most problems.
861
name_tail = splitpath(f)[-1]
862
dest_path = appendpath(to_name, name_tail)
863
print "%s => %s" % (f, dest_path)
864
inv.rename(inv.path2id(f), to_dir_id, name_tail)
866
os.rename(self.abspath(f), self.abspath(dest_path))
868
bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
869
["rename rolled back"])
871
self._write_inventory(inv)
875
def show_status(self, show_all=False):
876
"""Display single-line status for non-ignored working files.
878
The list is show sorted in order by file name.
880
>>> b = ScratchBranch(files=['foo', 'foo~'])
886
>>> b.commit("add foo")
888
>>> os.unlink(b.abspath('foo'))
892
TODO: Get state for single files.
895
# We have to build everything into a list first so that it can
896
# sorted by name, incorporating all the different sources.
898
# FIXME: Rather than getting things in random order and then sorting,
899
# just step through in order.
901
# Interesting case: the old ID for a file has been removed,
902
# but a new file has been created under that name.
904
old = self.basis_tree()
905
new = self.working_tree()
907
for fs, fid, oldname, newname, kind in diff_trees(old, new):
909
show_status(fs, kind,
910
oldname + ' => ' + newname)
911
elif fs == 'A' or fs == 'M':
912
show_status(fs, kind, newname)
914
show_status(fs, kind, oldname)
917
show_status(fs, kind, newname)
920
show_status(fs, kind, newname)
922
show_status(fs, kind, newname)
490
This returns a list of (from_path, to_path) pairs for each
493
raise NotImplementedError('move is abstract')
495
def get_parent(self):
496
"""Return the parent location of the branch.
498
This is the default location for push/pull/missing. The usual
499
pattern is that the user can override it by specifying a
502
raise NotImplementedError('get_parent is abstract')
504
def get_push_location(self):
505
"""Return the None or the location to push this branch to."""
506
raise NotImplementedError('get_push_location is abstract')
508
def set_push_location(self, location):
509
"""Set a new push location for this branch."""
510
raise NotImplementedError('set_push_location is abstract')
512
def set_parent(self, url):
513
raise NotImplementedError('set_parent is abstract')
515
def check_revno(self, revno):
517
Check whether a revno corresponds to any revision.
518
Zero (the NULL revision) is considered valid.
521
self.check_real_revno(revno)
523
def check_real_revno(self, revno):
525
Check whether a revno corresponds to a real revision.
526
Zero (the NULL revision) is considered invalid
528
if revno < 1 or revno > self.revno():
529
raise InvalidRevisionNumber(revno)
531
def sign_revision(self, revision_id, gpg_strategy):
532
raise NotImplementedError('sign_revision is abstract')
534
def store_revision_signature(self, gpg_strategy, plaintext, revision_id):
535
raise NotImplementedError('store_revision_signature is abstract')
537
def fileid_involved_between_revs(self, from_revid, to_revid):
538
""" This function returns the file_id(s) involved in the
539
changes between the from_revid revision and the to_revid
542
raise NotImplementedError('fileid_involved_between_revs is abstract')
544
def fileid_involved(self, last_revid=None):
545
""" This function returns the file_id(s) involved in the
546
changes up to the revision last_revid
547
If no parametr is passed, then all file_id[s] present in the
548
repository are returned
550
raise NotImplementedError('fileid_involved is abstract')
552
def fileid_involved_by_set(self, changes):
553
""" This function returns the file_id(s) involved in the
554
changes present in the set 'changes'
556
raise NotImplementedError('fileid_involved_by_set is abstract')
558
def fileid_involved_between_revs(self, from_revid, to_revid):
559
""" This function returns the file_id(s) involved in the
560
changes between the from_revid revision and the to_revid
563
raise NotImplementedError('fileid_involved_between_revs is abstract')
565
def fileid_involved(self, last_revid=None):
566
""" This function returns the file_id(s) involved in the
567
changes up to the revision last_revid
568
If no parametr is passed, then all file_id[s] present in the
569
repository are returned
571
raise NotImplementedError('fileid_involved is abstract')
573
def fileid_involved_by_set(self, changes):
574
""" This function returns the file_id(s) involved in the
575
changes present in the set 'changes'
577
raise NotImplementedError('fileid_involved_by_set is abstract')
579
class BzrBranchFormat(object):
580
"""An encapsulation of the initialization and open routines for a format.
582
Formats provide three things:
583
* An initialization routine,
587
Formats are placed in an dict by their format string for reference
588
during branch opening. Its not required that these be instances, they
589
can be classes themselves with class methods - it simply depends on
590
whether state is needed for a given format or not.
592
Once a format is deprecated, just deprecate the initialize and open
593
methods on the format class. Do not deprecate the object, as the
594
object will be created every time regardless.
598
"""The known formats."""
601
def find_format(klass, transport):
602
"""Return the format registered for URL."""
604
format_string = transport.get(".bzr/branch-format").read()
605
return klass._formats[format_string]
607
raise NotBranchError(path=transport.base)
609
raise errors.UnknownFormatError(format_string)
611
def get_format_string(self):
612
"""Return the ASCII format string that identifies this format."""
613
raise NotImplementedError(self.get_format_string)
615
def _find_modes(self, t):
616
"""Determine the appropriate modes for files and directories.
618
FIXME: When this merges into, or from storage,
619
this code becomes delgatable to a LockableFiles instance.
621
For now its cribbed and returns (dir_mode, file_mode)
625
except errors.TransportNotPossible:
629
dir_mode = st.st_mode & 07777
630
# Remove the sticky and execute bits for files
631
file_mode = dir_mode & ~07111
632
if not BzrBranch._set_dir_mode:
634
if not BzrBranch._set_file_mode:
636
return dir_mode, file_mode
638
def initialize(self, url):
639
"""Create a branch of this format at url and return an open branch."""
640
t = get_transport(url)
641
from bzrlib.weavefile import write_weave_v5
642
from bzrlib.weave import Weave
644
# Create an empty weave
646
bzrlib.weavefile.write_weave_v5(Weave(), sio)
647
empty_weave = sio.getvalue()
649
# Since we don't have a .bzr directory, inherit the
650
# mode from the root directory
651
dir_mode, file_mode = self._find_modes(t)
653
t.mkdir('.bzr', mode=dir_mode)
654
control = t.clone('.bzr')
655
dirs = ['revision-store', 'weaves']
657
StringIO("This is a Bazaar-NG control directory.\n"
658
"Do not change any files in this directory.\n")),
659
('branch-format', StringIO(self.get_format_string())),
660
('revision-history', StringIO('')),
661
('branch-name', StringIO('')),
662
('branch-lock', StringIO('')),
663
('inventory.weave', StringIO(empty_weave)),
665
control.mkdir_multi(dirs, mode=dir_mode)
666
control.put_multi(files, mode=file_mode)
667
mutter('created control directory in ' + t.base)
668
return BzrBranch(t, format=self)
670
def is_supported(self):
671
"""Is this format supported?
673
Supported formats can be initialized and opened.
674
Unsupported formats may not support initialization or committing or
675
some other features depending on the reason for not being supported.
679
def open(self, transport):
680
"""Fill out the data in branch for the branch at url."""
681
return BzrBranch(transport, format=self)
684
def register_format(klass, format):
685
klass._formats[format.get_format_string()] = format
688
def unregister_format(klass, format):
689
assert klass._formats[format.get_format_string()] is format
690
del klass._formats[format.get_format_string()]
693
class BzrBranchFormat4(BzrBranchFormat):
694
"""Bzr branch format 4.
698
- TextStores for texts, inventories,revisions.
700
This format is deprecated: it indexes texts using a text it which is
701
removed in format 5; write support for this format has been removed.
704
def get_format_string(self):
705
"""See BzrBranchFormat.get_format_string()."""
706
return BZR_BRANCH_FORMAT_4
708
def initialize(self, url):
709
"""Format 4 branches cannot be created."""
710
raise UninitializableFormat(self)
712
def is_supported(self):
713
"""Format 4 is not supported.
715
It is not supported because the model changed from 4 to 5 and the
716
conversion logic is expensive - so doing it on the fly was not
722
class BzrBranchFormat5(BzrBranchFormat):
723
"""Bzr branch format 5.
726
- weaves for file texts and inventory
728
- TextStores for revisions and signatures.
731
def get_format_string(self):
732
"""See BzrBranchFormat.get_format_string()."""
733
return BZR_BRANCH_FORMAT_5
736
class BzrBranchFormat6(BzrBranchFormat):
737
"""Bzr branch format 6.
740
- weaves for file texts and inventory
741
- hash subdirectory based stores.
742
- TextStores for revisions and signatures.
745
def get_format_string(self):
746
"""See BzrBranchFormat.get_format_string()."""
747
return BZR_BRANCH_FORMAT_6
750
BzrBranchFormat.register_format(BzrBranchFormat4())
751
BzrBranchFormat.register_format(BzrBranchFormat5())
752
BzrBranchFormat.register_format(BzrBranchFormat6())
754
# TODO: jam 20060108 Create a new branch format, and as part of upgrade
755
# make sure that ancestry.weave is deleted (it is never used, but
756
# used to be created)
759
class BzrBranch(Branch):
760
"""A branch stored in the actual filesystem.
762
Note that it's "local" in the context of the filesystem; it doesn't
763
really matter if it's on an nfs/smb/afs/coda/... share, as long as
764
it's writable, and can be accessed via the normal filesystem API.
770
If _lock_mode is true, a positive count of the number of times the
774
Lock object from bzrlib.lock.
776
# We actually expect this class to be somewhat short-lived; part of its
777
# purpose is to try to isolate what bits of the branch logic are tied to
778
# filesystem access, so that in a later step, we can extricate them to
779
# a separarte ("storage") class.
783
_inventory_weave = None
784
# If set to False (by a plugin, etc) BzrBranch will not set the
785
# mode on created files or directories
786
_set_file_mode = True
789
# Map some sort of prefix into a namespace
790
# stuff like "revno:10", "revid:", etc.
791
# This should match a prefix with a function which accepts
792
REVISION_NAMESPACES = {}
794
def push_stores(self, branch_to):
795
"""See Branch.push_stores."""
796
if (not isinstance(self._branch_format, BzrBranchFormat4) or
797
self._branch_format != branch_to._branch_format):
798
from bzrlib.fetch import greedy_fetch
799
mutter("Using fetch logic to push between %s(%s) and %s(%s)",
800
self, self._branch_format, branch_to, branch_to._branch_format)
801
greedy_fetch(to_branch=branch_to, from_branch=self,
802
revision=self.last_revision())
805
# format 4 to format 4 logic only.
806
store_pairs = ((self.text_store, branch_to.text_store),
807
(self.inventory_store, branch_to.inventory_store),
808
(self.revision_store, branch_to.revision_store))
810
for from_store, to_store in store_pairs:
811
copy_all(from_store, to_store)
812
except UnlistableStore:
813
raise UnlistableBranch(from_store)
815
def __init__(self, transport, init=deprecated_nonce,
816
relax_version_check=deprecated_nonce, format=None):
817
"""Create new branch object at a particular location.
819
transport -- A Transport object, defining how to access files.
821
init -- If True, create new control files in a previously
822
unversioned directory. If False, the branch must already
825
relax_version_check -- If true, the usual check for the branch
826
version is not applied. This is intended only for
827
upgrade/recovery type use; it's not guaranteed that
828
all operations will work on old format branches.
830
In the test suite, creation of new trees is tested using the
831
`ScratchBranch` class.
833
assert isinstance(transport, Transport), \
834
"%r is not a Transport" % transport
835
self._transport = transport
836
if deprecated_passed(init):
837
warn("BzrBranch.__init__(..., init=XXX): The init parameter is "
838
"deprecated as of bzr 0.8. Please use Branch.create().",
842
# this is slower than before deprecation, oh well never mind.
844
self._initialize(transport.base)
846
self._check_format(format)
847
if deprecated_passed(relax_version_check):
848
warn("BzrBranch.__init__(..., relax_version_check=XXX_: The "
849
"relax_version_check parameter is deprecated as of bzr 0.8. "
850
"Please use Branch.open_downlevel, or a BzrBranchFormat's "
854
if (not relax_version_check
855
and not self._branch_format.is_supported()):
856
raise errors.UnsupportedFormatError(
857
'sorry, branch format %r not supported' % fmt,
858
['use a different bzr version',
859
'or remove the .bzr directory'
860
' and "bzr init" again'])
862
def get_store(name, compressed=True, prefixed=False):
863
relpath = self._rel_controlfilename(safe_unicode(name))
864
store = TextStore(self._transport.clone(relpath),
865
dir_mode=self._dir_mode,
866
file_mode=self._file_mode,
868
compressed=compressed)
871
def get_weave(name, prefixed=False):
872
relpath = self._rel_controlfilename(unicode(name))
873
ws = WeaveStore(self._transport.clone(relpath),
875
dir_mode=self._dir_mode,
876
file_mode=self._file_mode)
877
if self._transport.should_cache():
878
ws.enable_cache = True
881
if isinstance(self._branch_format, BzrBranchFormat4):
882
self.inventory_store = get_store('inventory-store')
883
self.text_store = get_store('text-store')
884
self.revision_store = get_store('revision-store')
885
elif isinstance(self._branch_format, BzrBranchFormat5):
886
self.control_weaves = get_weave(u'')
887
self.weave_store = get_weave(u'weaves')
888
self.revision_store = get_store(u'revision-store', compressed=False)
889
elif isinstance(self._branch_format, BzrBranchFormat6):
890
self.control_weaves = get_weave(u'')
891
self.weave_store = get_weave(u'weaves', prefixed=True)
892
self.revision_store = get_store(u'revision-store', compressed=False,
894
self.revision_store.register_suffix('sig')
895
self._transaction = None
898
def _initialize(base):
899
"""Create a bzr branch in the latest format."""
900
return BzrBranchFormat6().initialize(base)
903
return '%s(%r)' % (self.__class__.__name__, self._transport.base)
908
if self._lock_mode or self._lock:
909
# XXX: This should show something every time, and be suitable for
910
# headless operation and embedding
911
warn("branch %r was not explicitly unlocked" % self)
914
# TODO: It might be best to do this somewhere else,
915
# but it is nice for a Branch object to automatically
916
# cache it's information.
917
# Alternatively, we could have the Transport objects cache requests
918
# See the earlier discussion about how major objects (like Branch)
919
# should never expect their __del__ function to run.
920
if hasattr(self, 'cache_root') and self.cache_root is not None:
922
shutil.rmtree(self.cache_root)
925
self.cache_root = None
929
return self._transport.base
932
base = property(_get_base, doc="The URL for the root of this branch.")
934
def _finish_transaction(self):
935
"""Exit the current transaction."""
936
if self._transaction is None:
937
raise errors.LockError('Branch %s is not in a transaction' %
939
transaction = self._transaction
940
self._transaction = None
943
def get_transaction(self):
944
"""See Branch.get_transaction."""
945
if self._transaction is None:
946
return transactions.PassThroughTransaction()
948
return self._transaction
950
def _set_transaction(self, new_transaction):
951
"""Set a new active transaction."""
952
if self._transaction is not None:
953
raise errors.LockError('Branch %s is in a transaction already.' %
955
self._transaction = new_transaction
957
def lock_write(self):
958
#mutter("lock write: %s (%s)", self, self._lock_count)
959
# TODO: Upgrade locking to support using a Transport,
960
# and potentially a remote locking protocol
962
if self._lock_mode != 'w':
963
raise LockError("can't upgrade to a write lock from %r" %
965
self._lock_count += 1
967
self._lock = self._transport.lock_write(
968
self._rel_controlfilename('branch-lock'))
969
self._lock_mode = 'w'
971
self._set_transaction(transactions.PassThroughTransaction())
974
#mutter("lock read: %s (%s)", self, self._lock_count)
976
assert self._lock_mode in ('r', 'w'), \
977
"invalid lock mode %r" % self._lock_mode
978
self._lock_count += 1
980
self._lock = self._transport.lock_read(
981
self._rel_controlfilename('branch-lock'))
982
self._lock_mode = 'r'
984
self._set_transaction(transactions.ReadOnlyTransaction())
985
# 5K may be excessive, but hey, its a knob.
986
self.get_transaction().set_cache_size(5000)
989
#mutter("unlock: %s (%s)", self, self._lock_count)
990
if not self._lock_mode:
991
raise LockError('branch %r is not locked' % (self))
993
if self._lock_count > 1:
994
self._lock_count -= 1
996
self._finish_transaction()
999
self._lock_mode = self._lock_count = None
1001
def abspath(self, name):
1002
"""See Branch.abspath."""
1003
return self._transport.abspath(name)
1005
def _rel_controlfilename(self, file_or_path):
1006
if not isinstance(file_or_path, basestring):
1007
file_or_path = u'/'.join(file_or_path)
1008
if file_or_path == '':
1009
return bzrlib.BZRDIR
1010
return bzrlib.transport.urlescape(bzrlib.BZRDIR + u'/' + file_or_path)
1012
def controlfilename(self, file_or_path):
1013
"""See Branch.controlfilename."""
1014
return self._transport.abspath(self._rel_controlfilename(file_or_path))
1016
def controlfile(self, file_or_path, mode='r'):
1017
"""See Branch.controlfile."""
1020
relpath = self._rel_controlfilename(file_or_path)
1021
#TODO: codecs.open() buffers linewise, so it was overloaded with
1022
# a much larger buffer, do we need to do the same for getreader/getwriter?
1024
return self._transport.get(relpath)
1026
raise BzrError("Branch.controlfile(mode='wb') is not supported, use put_controlfiles")
1028
# XXX: Do we really want errors='replace'? Perhaps it should be
1029
# an error, or at least reported, if there's incorrectly-encoded
1030
# data inside a file.
1031
# <https://launchpad.net/products/bzr/+bug/3823>
1032
return codecs.getreader('utf-8')(self._transport.get(relpath), errors='replace')
1034
raise BzrError("Branch.controlfile(mode='w') is not supported, use put_controlfiles")
1036
raise BzrError("invalid controlfile mode %r" % mode)
1038
def put_controlfile(self, path, f, encode=True):
1039
"""See Branch.put_controlfile."""
1040
self.put_controlfiles([(path, f)], encode=encode)
1042
def put_controlfiles(self, files, encode=True):
1043
"""See Branch.put_controlfiles."""
1046
for path, f in files:
1048
if isinstance(f, basestring):
1049
f = StringIO(f.encode('utf-8', 'replace'))
1051
f = codecs.getwriter('utf-8')(f, errors='replace')
1052
path = self._rel_controlfilename(path)
1053
ctrl_files.append((path, f))
1054
self._transport.put_multi(ctrl_files, mode=self._file_mode)
1056
def _find_modes(self, path=None):
1057
"""Determine the appropriate modes for files and directories."""
1060
path = self._rel_controlfilename('')
1061
st = self._transport.stat(path)
1062
except errors.TransportNotPossible:
1063
self._dir_mode = 0755
1064
self._file_mode = 0644
1066
self._dir_mode = st.st_mode & 07777
1067
# Remove the sticky and execute bits for files
1068
self._file_mode = self._dir_mode & ~07111
1069
if not self._set_dir_mode:
1070
self._dir_mode = None
1071
if not self._set_file_mode:
1072
self._file_mode = None
1074
def _check_format(self, format):
1075
"""Identify the branch format if needed.
1077
The format is stored as a reference to the format object in
1078
self._branch_format for code that needs to check it later.
1080
The format parameter is either None or the branch format class
1081
used to open this branch.
1084
format = BzrBranchFormat.find_format(self._transport)
1085
self._branch_format = format
1086
mutter("got branch format %s", self._branch_format)
1089
def get_root_id(self):
1090
"""See Branch.get_root_id."""
1091
inv = self.get_inventory(self.last_revision())
1092
return inv.root.file_id
1095
def print_file(self, file, revision_id):
1096
"""See Branch.print_file."""
1097
tree = self.revision_tree(revision_id)
1098
# use inventory as it was in that revision
1099
file_id = tree.inventory.path2id(file)
1102
revno = self.revision_id_to_revno(revision_id)
1103
except errors.NoSuchRevision:
1104
# TODO: This should not be BzrError,
1105
# but NoSuchFile doesn't fit either
1106
raise BzrError('%r is not present in revision %s'
1107
% (file, revision_id))
924
bailout("weird file state %r" % ((fs, fid),))
928
class ScratchBranch(Branch):
1109
raise BzrError('%r is not present in revision %s'
1111
tree.print_file(file_id)
1114
def append_revision(self, *revision_ids):
1115
"""See Branch.append_revision."""
1116
for revision_id in revision_ids:
1117
mutter("add {%s} to revision-history" % revision_id)
1118
rev_history = self.revision_history()
1119
rev_history.extend(revision_ids)
1120
self.set_revision_history(rev_history)
1123
def set_revision_history(self, rev_history):
1124
"""See Branch.set_revision_history."""
1125
old_revision = self.last_revision()
1126
new_revision = rev_history[-1]
1127
self.put_controlfile('revision-history', '\n'.join(rev_history))
1129
def has_revision(self, revision_id):
1130
"""See Branch.has_revision."""
1131
return (revision_id is None
1132
or self.revision_store.has_id(revision_id))
1135
def _get_revision_xml_file(self, revision_id):
1136
if not revision_id or not isinstance(revision_id, basestring):
1137
raise InvalidRevisionId(revision_id=revision_id, branch=self)
1139
return self.revision_store.get(revision_id)
1140
except (IndexError, KeyError):
1141
raise bzrlib.errors.NoSuchRevision(self, revision_id)
1143
def get_revision_xml(self, revision_id):
1144
"""See Branch.get_revision_xml."""
1145
return self._get_revision_xml_file(revision_id).read()
1147
def get_revision(self, revision_id):
1148
"""See Branch.get_revision."""
1149
xml_file = self._get_revision_xml_file(revision_id)
1152
r = bzrlib.xml5.serializer_v5.read_revision(xml_file)
1153
except SyntaxError, e:
1154
raise bzrlib.errors.BzrError('failed to unpack revision_xml',
1158
assert r.revision_id == revision_id
1161
def get_revision_sha1(self, revision_id):
1162
"""See Branch.get_revision_sha1."""
1163
# In the future, revision entries will be signed. At that
1164
# point, it is probably best *not* to include the signature
1165
# in the revision hash. Because that lets you re-sign
1166
# the revision, (add signatures/remove signatures) and still
1167
# have all hash pointers stay consistent.
1168
# But for now, just hash the contents.
1169
return bzrlib.osutils.sha_file(self.get_revision_xml_file(revision_id))
1171
def get_ancestry(self, revision_id):
1172
"""See Branch.get_ancestry."""
1173
if revision_id is None:
1175
w = self._get_inventory_weave()
1176
return [None] + map(w.idx_to_name,
1177
w.inclusions([w.lookup(revision_id)]))
1179
def _get_inventory_weave(self):
1180
return self.control_weaves.get_weave('inventory',
1181
self.get_transaction())
1183
def get_inventory(self, revision_id):
1184
"""See Branch.get_inventory."""
1185
xml = self.get_inventory_xml(revision_id)
1186
return bzrlib.xml5.serializer_v5.read_inventory_from_string(xml)
1188
def get_inventory_xml(self, revision_id):
1189
"""See Branch.get_inventory_xml."""
1191
assert isinstance(revision_id, basestring), type(revision_id)
1192
iw = self._get_inventory_weave()
1193
return iw.get_text(iw.lookup(revision_id))
1195
raise bzrlib.errors.HistoryMissing(self, 'inventory', revision_id)
1197
def get_inventory_sha1(self, revision_id):
1198
"""See Branch.get_inventory_sha1."""
1199
return self.get_revision(revision_id).inventory_sha1
1201
def get_revision_inventory(self, revision_id):
1202
"""See Branch.get_revision_inventory."""
1203
# TODO: Unify this with get_inventory()
1204
# bzr 0.0.6 and later imposes the constraint that the inventory_id
1205
# must be the same as its revision, so this is trivial.
1206
if revision_id == None:
1207
# This does not make sense: if there is no revision,
1208
# then it is the current tree inventory surely ?!
1209
# and thus get_root_id() is something that looks at the last
1210
# commit on the branch, and the get_root_id is an inventory check.
1211
raise NotImplementedError
1212
# return Inventory(self.get_root_id())
1214
return self.get_inventory(revision_id)
1217
def revision_history(self):
1218
"""See Branch.revision_history."""
1219
transaction = self.get_transaction()
1220
history = transaction.map.find_revision_history()
1221
if history is not None:
1222
mutter("cache hit for revision-history in %s", self)
1223
return list(history)
1224
history = [l.rstrip('\r\n') for l in
1225
self.controlfile('revision-history', 'r').readlines()]
1226
transaction.map.add_revision_history(history)
1227
# this call is disabled because revision_history is
1228
# not really an object yet, and the transaction is for objects.
1229
# transaction.register_clean(history, precious=True)
1230
return list(history)
1232
def update_revisions(self, other, stop_revision=None):
1233
"""See Branch.update_revisions."""
1234
from bzrlib.fetch import greedy_fetch
1235
if stop_revision is None:
1236
stop_revision = other.last_revision()
1237
### Should this be checking is_ancestor instead of revision_history?
1238
if (stop_revision is not None and
1239
stop_revision in self.revision_history()):
1241
greedy_fetch(to_branch=self, from_branch=other,
1242
revision=stop_revision)
1243
pullable_revs = self.pullable_revisions(other, stop_revision)
1244
if len(pullable_revs) > 0:
1245
self.append_revision(*pullable_revs)
1247
def pullable_revisions(self, other, stop_revision):
1248
"""See Branch.pullable_revisions."""
1249
other_revno = other.revision_id_to_revno(stop_revision)
1251
return self.missing_revisions(other, other_revno)
1252
except DivergedBranches, e:
1254
pullable_revs = get_intervening_revisions(self.last_revision(),
1255
stop_revision, self)
1256
assert self.last_revision() not in pullable_revs
1257
return pullable_revs
1258
except bzrlib.errors.NotAncestor:
1259
if is_ancestor(self.last_revision(), stop_revision, self):
1264
def revision_tree(self, revision_id):
1265
"""See Branch.revision_tree."""
1266
# TODO: refactor this to use an existing revision object
1267
# so we don't need to read it in twice.
1268
if revision_id == None or revision_id == NULL_REVISION:
1271
inv = self.get_revision_inventory(revision_id)
1272
return RevisionTree(self, inv, revision_id)
1274
def basis_tree(self):
1275
"""See Branch.basis_tree."""
1277
revision_id = self.revision_history()[-1]
1278
xml = self.working_tree().read_basis_inventory(revision_id)
1279
inv = bzrlib.xml5.serializer_v5.read_inventory_from_string(xml)
1280
return RevisionTree(self, inv, revision_id)
1281
except (IndexError, NoSuchFile, NoWorkingTree), e:
1282
return self.revision_tree(self.last_revision())
1284
def working_tree(self):
1285
"""See Branch.working_tree."""
1286
from bzrlib.workingtree import WorkingTree
1287
from bzrlib.transport.local import LocalTransport
1288
if (self._transport.base.find('://') != -1 or
1289
not isinstance(self._transport, LocalTransport)):
1290
raise NoWorkingTree(self.base)
1291
return WorkingTree(self.base, branch=self)
1294
def pull(self, source, overwrite=False):
1295
"""See Branch.pull."""
1298
old_count = len(self.revision_history())
1300
self.update_revisions(source)
1301
except DivergedBranches:
1305
self.set_revision_history(source.revision_history())
1306
new_count = len(self.revision_history())
1307
return new_count - old_count
1311
def get_parent(self):
1312
"""See Branch.get_parent."""
1314
_locs = ['parent', 'pull', 'x-pull']
1317
return self.controlfile(l, 'r').read().strip('\n')
1322
def get_push_location(self):
1323
"""See Branch.get_push_location."""
1324
config = bzrlib.config.BranchConfig(self)
1325
push_loc = config.get_user_option('push_location')
1328
def set_push_location(self, location):
1329
"""See Branch.set_push_location."""
1330
config = bzrlib.config.LocationConfig(self.base)
1331
config.set_user_option('push_location', location)
1334
def set_parent(self, url):
1335
"""See Branch.set_parent."""
1336
# TODO: Maybe delete old location files?
1337
from bzrlib.atomicfile import AtomicFile
1338
f = AtomicFile(self.controlfilename('parent'))
1345
def tree_config(self):
1346
return TreeConfig(self)
1348
def sign_revision(self, revision_id, gpg_strategy):
1349
"""See Branch.sign_revision."""
1350
plaintext = Testament.from_revision(self, revision_id).as_short_text()
1351
self.store_revision_signature(gpg_strategy, plaintext, revision_id)
1354
def store_revision_signature(self, gpg_strategy, plaintext, revision_id):
1355
"""See Branch.store_revision_signature."""
1356
self.revision_store.add(StringIO(gpg_strategy.sign(plaintext)),
1359
def fileid_involved_between_revs(self, from_revid, to_revid):
1360
"""Find file_id(s) which are involved in the changes between revisions.
1362
This determines the set of revisions which are involved, and then
1363
finds all file ids affected by those revisions.
1365
# TODO: jam 20060119 This code assumes that w.inclusions will
1366
# always be correct. But because of the presence of ghosts
1367
# it is possible to be wrong.
1368
# One specific example from Robert Collins:
1369
# Two branches, with revisions ABC, and AD
1370
# C is a ghost merge of D.
1371
# Inclusions doesn't recognize D as an ancestor.
1372
# If D is ever merged in the future, the weave
1373
# won't be fixed, because AD never saw revision C
1374
# to cause a conflict which would force a reweave.
1375
w = self._get_inventory_weave()
1376
from_set = set(w.inclusions([w.lookup(from_revid)]))
1377
to_set = set(w.inclusions([w.lookup(to_revid)]))
1378
included = to_set.difference(from_set)
1379
changed = map(w.idx_to_name, included)
1380
return self._fileid_involved_by_set(changed)
1382
def fileid_involved(self, last_revid=None):
1383
"""Find all file_ids modified in the ancestry of last_revid.
1385
:param last_revid: If None, last_revision() will be used.
1387
w = self._get_inventory_weave()
1389
changed = set(w._names)
1391
included = w.inclusions([w.lookup(last_revid)])
1392
changed = map(w.idx_to_name, included)
1393
return self._fileid_involved_by_set(changed)
1395
def fileid_involved_by_set(self, changes):
1396
"""Find all file_ids modified by the set of revisions passed in.
1398
:param changes: A set() of revision ids
1400
# TODO: jam 20060119 This line does *nothing*, remove it.
1401
# or better yet, change _fileid_involved_by_set so
1402
# that it takes the inventory weave, rather than
1403
# pulling it out by itself.
1404
w = self._get_inventory_weave()
1405
return self._fileid_involved_by_set(changes)
1407
def _fileid_involved_by_set(self, changes):
1408
"""Find the set of file-ids affected by the set of revisions.
1410
:param changes: A set() of revision ids.
1411
:return: A set() of file ids.
1413
This peaks at the Weave, interpreting each line, looking to
1414
see if it mentions one of the revisions. And if so, includes
1415
the file id mentioned.
1416
This expects both the Weave format, and the serialization
1417
to have a single line per file/directory, and to have
1418
fileid="" and revision="" on that line.
1420
assert (isinstance(self._branch_format, BzrBranchFormat5) or
1421
isinstance(self._branch_format, BzrBranchFormat6)), \
1422
"fileid_involved only supported for branches which store inventory as xml"
1424
w = self._get_inventory_weave()
1426
for line in w._weave:
1428
# it is ugly, but it is due to the weave structure
1429
if not isinstance(line, basestring): continue
1431
start = line.find('file_id="')+9
1432
if start < 9: continue
1433
end = line.find('"', start)
1435
file_id = xml.sax.saxutils.unescape(line[start:end])
1437
# check if file_id is already present
1438
if file_id in file_ids: continue
1440
start = line.find('revision="')+10
1441
if start < 10: continue
1442
end = line.find('"', start)
1444
revision_id = xml.sax.saxutils.unescape(line[start:end])
1446
if revision_id in changes:
1447
file_ids.add(file_id)
1452
Branch.set_default_initializer(BzrBranch._initialize)
1455
class BranchTestProviderAdapter(object):
1456
"""A tool to generate a suite testing multiple branch formats at once.
1458
This is done by copying the test once for each transport and injecting
1459
the transport_server, transport_readonly_server, and branch_format
1460
classes into each copy. Each copy is also given a new id() to make it
1464
def __init__(self, transport_server, transport_readonly_server, formats):
1465
self._transport_server = transport_server
1466
self._transport_readonly_server = transport_readonly_server
1467
self._formats = formats
1469
def adapt(self, test):
1470
result = TestSuite()
1471
for format in self._formats:
1472
new_test = deepcopy(test)
1473
new_test.transport_server = self._transport_server
1474
new_test.transport_readonly_server = self._transport_readonly_server
1475
new_test.branch_format = format
1476
def make_new_test_id():
1477
new_id = "%s(%s)" % (new_test.id(), format.__class__.__name__)
1478
return lambda: new_id
1479
new_test.id = make_new_test_id()
1480
result.addTest(new_test)
1484
class ScratchBranch(BzrBranch):
929
1485
"""Special test class: a branch that cleans up after itself.
931
1487
>>> b = ScratchBranch()
932
1488
>>> isdir(b.base)
1491
>>> b._transport.__del__()
939
def __init__(self, files=[], dirs=[]):
1496
def __init__(self, files=[], dirs=[], transport=None):
940
1497
"""Make a test branch.
942
1499
This creates a temporary directory and runs init-tree in it.
944
1501
If any files are listed, they are created in the working copy.
946
Branch.__init__(self, tempfile.mkdtemp(), init=True)
1503
if transport is None:
1504
transport = bzrlib.transport.local.ScratchTransport()
1505
# local import for scope restriction
1506
from bzrlib.workingtree import WorkingTree
1507
WorkingTree.create_standalone(transport.base)
1508
super(ScratchBranch, self).__init__(transport)
1510
super(ScratchBranch, self).__init__(transport)
948
os.mkdir(self.abspath(d))
1513
self._transport.mkdir(d)
951
file(os.path.join(self.base, f), 'w').write('content of %s' % f)
955
"""Destroy the test branch, removing the scratch directory."""
957
shutil.rmtree(self.base)
959
# Work around for shutil.rmtree failing on Windows when
960
# readonly files are encountered
961
for root, dirs, files in os.walk(self.base, topdown=False):
963
os.chmod(os.path.join(root, name), 0700)
964
shutil.rmtree(self.base)
1516
self._transport.put(f, 'content of %s' % f)
1521
>>> orig = ScratchBranch(files=["file1", "file2"])
1522
>>> clone = orig.clone()
1523
>>> if os.name != 'nt':
1524
... os.path.samefile(orig.base, clone.base)
1526
... orig.base == clone.base
1529
>>> os.path.isfile(pathjoin(clone.base, "file1"))
1532
from shutil import copytree
1533
from bzrlib.osutils import mkdtemp
1536
copytree(self.base, base, symlinks=True)
1537
return ScratchBranch(
1538
transport=bzrlib.transport.local.ScratchTransport(base))
968
1541
######################################################################