15
16
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18
from copy import deepcopy
19
from cStringIO import StringIO
24
from unittest import TestSuite
25
from warnings import warn
27
import xml.sax.saxutils
29
raise ImportError("We were unable to import 'xml.sax.saxutils',"
30
" most likely you have an xml.pyc or xml.pyo file"
31
" lying around in your bzrlib directory."
21
import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile
22
import traceback, socket, fnmatch, difflib, time
23
from binascii import hexlify
36
import bzrlib.bzrdir as bzrdir
37
from bzrlib.config import TreeConfig
38
from bzrlib.decorators import needs_read_lock, needs_write_lock
39
from bzrlib.delta import compare_trees
40
import bzrlib.errors as errors
41
from bzrlib.errors import (BzrError, InvalidRevisionNumber, InvalidRevisionId,
42
NoSuchRevision, HistoryMissing, NotBranchError,
43
DivergedBranches, LockError,
44
UninitializableFormat,
46
UnlistableBranch, NoSuchFile, NotVersionedError,
48
import bzrlib.inventory as inventory
49
from bzrlib.inventory import Inventory
50
from bzrlib.lockable_files import LockableFiles
51
from bzrlib.osutils import (isdir, quotefn,
52
rename, splitpath, sha_file,
53
file_kind, abspath, normpath, pathjoin,
56
from bzrlib.textui import show_status
57
from bzrlib.trace import mutter, note
58
from bzrlib.tree import EmptyTree, RevisionTree
59
from bzrlib.repository import Repository
60
from bzrlib.revision import (Revision, is_ancestor, get_intervening_revisions)
61
from bzrlib.store import copy_all
62
from bzrlib.symbol_versioning import *
63
import bzrlib.transactions as transactions
64
from bzrlib.transport import Transport, get_transport
65
from bzrlib.tree import EmptyTree, RevisionTree
70
BZR_BRANCH_FORMAT_4 = "Bazaar-NG branch, format 0.0.4\n"
71
BZR_BRANCH_FORMAT_5 = "Bazaar-NG branch, format 5\n"
72
BZR_BRANCH_FORMAT_6 = "Bazaar-NG branch, format 6\n"
75
# TODO: Maybe include checks for common corruption of newlines, etc?
77
# TODO: Some operations like log might retrieve the same revisions
78
# repeatedly to calculate deltas. We could perhaps have a weakref
79
# cache in memory to make this faster. In general anything can be
80
# cached in memory between lock and unlock operations. .. nb thats
81
# what the transaction identity map provides
26
from inventory import Inventory
27
from trace import mutter, note
28
from tree import Tree, EmptyTree, RevisionTree, WorkingTree
29
from inventory import InventoryEntry, Inventory
30
from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \
31
format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \
32
joinpath, sha_string, file_kind, local_time_offset
33
from store import ImmutableStore
34
from revision import Revision
35
from errors import bailout
36
from textui import show_status
37
from diff import diff_trees
39
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
40
## TODO: Maybe include checks for common corruption of newlines, etc?
84
46
######################################################################
88
50
"""Branch holding a history of revisions.
91
Base directory/url of the branch.
52
:todo: Perhaps use different stores for different classes of object,
53
so that we can keep track of how much space each one uses,
54
or garbage-collect them.
56
:todo: Add a RemoteBranch subclass. For the basic case of read-only
57
HTTP access this should be very easy by,
58
just redirecting controlfile access into HTTP requests.
59
We would need a RemoteStore working similarly.
61
:todo: Keep the on-disk branch locked while the object exists.
63
:todo: mkdir() method.
93
# this is really an instance variable - FIXME move it there
99
"""Construct the current default format branch in a_bzrdir.
101
This creates the current default BzrDir format, and if that
102
supports multiple Branch formats, then the default Branch format
105
print "not usable until we have repositories"
106
raise NotImplementedError("not usable right now")
107
return bzrdir.BzrDir.create(base)
109
def __init__(self, *ignored, **ignored_too):
110
raise NotImplementedError('The Branch class is abstract')
113
@deprecated_method(zero_eight)
114
def open_downlevel(base):
115
"""Open a branch which may be of an old format."""
116
return Branch.open(base, _unsupported=True)
119
def open(base, _unsupported=False):
120
"""Open the repository rooted at base.
122
For instance, if the repository is at URL/.bzr/repository,
123
Repository.open(URL) -> a Repository instance.
125
control = bzrdir.BzrDir.open(base, _unsupported)
126
return control.open_branch(_unsupported)
129
def open_containing(url):
130
"""Open an existing branch which contains url.
132
This probes for a branch at url, and searches upwards from there.
134
Basically we keep looking up until we find the control directory or
135
run into the root. If there isn't one, raises NotBranchError.
136
If there is one and it is either an unrecognised format or an unsupported
137
format, UnknownFormatError or UnsupportedFormatError are raised.
138
If there is one, it is returned, along with the unused portion of url.
140
control, relpath = bzrdir.BzrDir.open_containing(url)
141
return control.open_branch(), relpath
144
@deprecated_function(zero_eight)
145
def initialize(base):
146
"""Create a new working tree and branch, rooted at 'base' (url)
148
NOTE: This will soon be deprecated in favour of creation
151
return bzrdir.BzrDir.create_standalone_workingtree(base).branch
153
def setup_caching(self, cache_root):
154
"""Subclasses that care about caching should override this, and set
155
up cached stores located under cache_root.
157
# seems to be unused, 2006-01-13 mbp
158
warn('%s is deprecated' % self.setup_caching)
159
self.cache_root = cache_root
162
cfg = self.tree_config()
163
return cfg.get_option(u"nickname", default=self.base.split('/')[-2])
165
def _set_nick(self, nick):
166
cfg = self.tree_config()
167
cfg.set_option(nick, "nickname")
168
assert cfg.get_option("nickname") == nick
170
nick = property(_get_nick, _set_nick)
172
def lock_write(self):
173
raise NotImplementedError('lock_write is abstract')
176
raise NotImplementedError('lock_read is abstract')
179
raise NotImplementedError('unlock is abstract')
181
def peek_lock_mode(self):
182
"""Return lock mode for the Branch: 'r', 'w' or None"""
183
raise NotImplementedError(self.peek_lock_mode)
185
def abspath(self, name):
186
"""Return absolute filename for something in the branch
188
XXX: Robert Collins 20051017 what is this used for? why is it a branch
189
method and not a tree method.
191
raise NotImplementedError('abspath is abstract')
193
def get_root_id(self):
194
"""Return the id of this branches root"""
195
raise NotImplementedError('get_root_id is abstract')
197
def print_file(self, file, revision_id):
198
"""Print `file` to stdout."""
199
raise NotImplementedError('print_file is abstract')
201
def append_revision(self, *revision_ids):
202
raise NotImplementedError('append_revision is abstract')
204
def set_revision_history(self, rev_history):
205
raise NotImplementedError('set_revision_history is abstract')
65
def __init__(self, base, init=False):
66
"""Create new branch object at a particular location.
68
:param base: Base directory for the branch.
70
:param init: If True, create new control files in a previously
71
unversioned directory. If False, the branch must already
74
In the test suite, creation of new trees is tested using the
75
`ScratchBranch` class.
77
self.base = os.path.realpath(base)
81
if not isdir(self.controlfilename('.')):
82
bailout("not a bzr branch: %s" % quotefn(base),
83
['use "bzr init" to initialize a new working tree',
84
'current bzr can only operate from top-of-tree'])
87
self.text_store = ImmutableStore(self.controlfilename('text-store'))
88
self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
89
self.inventory_store = ImmutableStore(self.controlfilename('inventory-store'))
93
return '%s(%r)' % (self.__class__.__name__, self.base)
100
"""Return filename relative to branch top"""
101
return os.path.join(self.base, name)
104
def controlfilename(self, file_or_path):
105
"""Return location relative to branch."""
106
if isinstance(file_or_path, types.StringTypes):
107
file_or_path = [file_or_path]
108
return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
111
def controlfile(self, file_or_path, mode='r'):
112
"""Open a control file for this branch"""
113
return file(self.controlfilename(file_or_path), mode)
116
def _make_control(self):
117
os.mkdir(self.controlfilename([]))
118
self.controlfile('README', 'w').write(
119
"This is a Bazaar-NG control directory.\n"
120
"Do not change any files in this directory.")
121
self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT)
122
for d in ('text-store', 'inventory-store', 'revision-store'):
123
os.mkdir(self.controlfilename(d))
124
for f in ('revision-history', 'merged-patches',
125
'pending-merged-patches', 'branch-name'):
126
self.controlfile(f, 'w').write('')
127
mutter('created control directory in ' + self.base)
128
Inventory().write_xml(self.controlfile('inventory','w'))
131
def _check_format(self):
132
"""Check this branch format is supported.
134
The current tool only supports the current unstable format.
136
In the future, we might need different in-memory Branch
137
classes to support downlevel branches. But not yet.
139
# read in binary mode to detect newline wierdness.
140
fmt = self.controlfile('branch-format', 'rb').read()
141
if fmt != BZR_BRANCH_FORMAT:
142
bailout('sorry, branch format %r not supported' % fmt,
143
['use a different bzr version',
144
'or remove the .bzr directory and "bzr init" again'])
147
def read_working_inventory(self):
148
"""Read the working inventory."""
150
inv = Inventory.read_xml(self.controlfile('inventory', 'r'))
151
mutter("loaded inventory of %d items in %f"
152
% (len(inv), time.time() - before))
156
def _write_inventory(self, inv):
157
"""Update the working inventory.
159
That is to say, the inventory describing changes underway, that
160
will be committed to the next revision.
162
inv.write_xml(self.controlfile('inventory', 'w'))
163
mutter('wrote inventory to %s' % quotefn(self.controlfilename('inventory')))
166
inventory = property(read_working_inventory, _write_inventory, None,
167
"""Inventory for the working copy.""")
170
def add(self, files, verbose=False):
171
"""Make files versioned.
173
This puts the files in the Added state, so that they will be
174
recorded by the next commit.
176
:todo: Perhaps have an option to add the ids even if the files do
179
:todo: Perhaps return the ids of the files? But then again it
180
is easy to retrieve them if they're needed.
182
:todo: Option to specify file id.
184
:todo: Adding a directory should optionally recurse down and
185
add all non-ignored children. Perhaps do that in a
188
>>> b = ScratchBranch(files=['foo'])
189
>>> 'foo' in b.unknowns()
194
>>> 'foo' in b.unknowns()
196
>>> bool(b.inventory.path2id('foo'))
202
Traceback (most recent call last):
204
BzrError: ('foo is already versioned', [])
206
>>> b.add(['nothere'])
207
Traceback (most recent call last):
208
BzrError: ('cannot add: not a regular file or directory: nothere', [])
211
# TODO: Re-adding a file that is removed in the working copy
212
# should probably put it back with the previous ID.
213
if isinstance(files, types.StringTypes):
216
inv = self.read_working_inventory()
218
if is_control_file(f):
219
bailout("cannot add control file %s" % quotefn(f))
224
bailout("cannot add top-level %r" % f)
226
fullpath = os.path.normpath(self._rel(f))
230
elif isdir(fullpath):
233
bailout('cannot add: not a regular file or directory: %s' % quotefn(f))
236
parent_name = joinpath(fp[:-1])
237
mutter("lookup parent %r" % parent_name)
238
parent_id = inv.path2id(parent_name)
239
if parent_id == None:
240
bailout("cannot add: parent %r is not versioned"
245
file_id = _gen_file_id(fp[-1])
246
inv.add(InventoryEntry(file_id, fp[-1], kind=kind, parent_id=parent_id))
248
show_status('A', kind, quotefn(f))
250
mutter("add file %s file_id:{%s} kind=%r parent_id={%s}"
251
% (f, file_id, kind, parent_id))
252
self._write_inventory(inv)
256
def remove(self, files, verbose=False):
257
"""Mark nominated files for removal from the inventory.
259
This does not remove their text. This does not run on
261
:todo: Refuse to remove modified files unless --force is given?
263
>>> b = ScratchBranch(files=['foo'])
265
>>> b.inventory.has_filename('foo')
268
>>> b.working_tree().has_filename('foo')
270
>>> b.inventory.has_filename('foo')
273
>>> b = ScratchBranch(files=['foo'])
278
>>> b.inventory.has_filename('foo')
280
>>> b.basis_tree().has_filename('foo')
282
>>> b.working_tree().has_filename('foo')
285
:todo: Do something useful with directories.
287
:todo: Should this remove the text or not? Tough call; not
288
removing may be useful and the user can just use use rm, and
289
is the opposite of add. Removing it is consistent with most
290
other tools. Maybe an option.
292
## TODO: Normalize names
293
## TODO: Remove nested loops; better scalability
295
if isinstance(files, types.StringTypes):
298
inv = self.read_working_inventory()
300
# do this before any modifications
304
bailout("cannot remove unversioned file %s" % quotefn(f))
305
mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
307
show_status('D', inv[fid].kind, quotefn(f))
310
self._write_inventory(inv)
314
"""Return all unknown files.
316
These are files in the working directory that are not versioned or
317
control files or ignored.
319
>>> b = ScratchBranch(files=['foo', 'foo~'])
320
>>> list(b.unknowns())
323
>>> list(b.unknowns())
326
>>> list(b.unknowns())
329
return self.working_tree().unknowns()
332
def commit(self, message, timestamp=None, timezone=None,
335
"""Commit working copy as a new revision.
337
The basic approach is to add all the file texts into the
338
store, then the inventory, then make a new revision pointing
339
to that inventory and store that.
341
This is not quite safe if the working copy changes during the
342
commit; for the moment that is simply not allowed. A better
343
approach is to make a temporary copy of the files before
344
computing their hashes, and then add those hashes in turn to
345
the inventory. This should mean at least that there are no
346
broken hash pointers. There is no way we can get a snapshot
347
of the whole directory at an instant. This would also have to
348
be robust against files disappearing, moving, etc. So the
349
whole thing is a bit hard.
351
:param timestamp: if not None, seconds-since-epoch for a
352
postdated/predated commit.
355
## TODO: Show branch names
357
# TODO: Don't commit if there are no changes, unless forced?
359
# First walk over the working inventory; and both update that
360
# and also build a new revision inventory. The revision
361
# inventory needs to hold the text-id, sha1 and size of the
362
# actual file versions committed in the revision. (These are
363
# not present in the working inventory.) We also need to
364
# detect missing/deleted files, and remove them from the
367
work_inv = self.read_working_inventory()
369
basis = self.basis_tree()
370
basis_inv = basis.inventory
372
for path, entry in work_inv.iter_entries():
373
## TODO: Cope with files that have gone missing.
375
## TODO: Check that the file kind has not changed from the previous
376
## revision of this file (if any).
381
file_id = entry.file_id
382
mutter('commit prep file %s, id %r ' % (p, file_id))
384
if not os.path.exists(p):
385
mutter(" file is missing, removing from inventory")
387
show_status('D', entry.kind, quotefn(path))
388
missing_ids.append(file_id)
391
# TODO: Handle files that have been deleted
393
# TODO: Maybe a special case for empty files? Seems a
394
# waste to store them many times.
398
if basis_inv.has_id(file_id):
399
old_kind = basis_inv[file_id].kind
400
if old_kind != entry.kind:
401
bailout("entry %r changed kind from %r to %r"
402
% (file_id, old_kind, entry.kind))
404
if entry.kind == 'directory':
406
bailout("%s is entered as directory but not a directory" % quotefn(p))
407
elif entry.kind == 'file':
409
bailout("%s is entered as file but is not a file" % quotefn(p))
411
content = file(p, 'rb').read()
413
entry.text_sha1 = sha_string(content)
414
entry.text_size = len(content)
416
old_ie = basis_inv.has_id(file_id) and basis_inv[file_id]
418
and (old_ie.text_size == entry.text_size)
419
and (old_ie.text_sha1 == entry.text_sha1)):
420
## assert content == basis.get_file(file_id).read()
421
entry.text_id = basis_inv[file_id].text_id
422
mutter(' unchanged from previous text_id {%s}' %
426
entry.text_id = _gen_file_id(entry.name)
427
self.text_store.add(content, entry.text_id)
428
mutter(' stored with text_id {%s}' % entry.text_id)
432
elif (old_ie.name == entry.name
433
and old_ie.parent_id == entry.parent_id):
438
show_status(state, entry.kind, quotefn(path))
440
for file_id in missing_ids:
441
# have to do this later so we don't mess up the iterator.
442
# since parents may be removed before their children we
445
# FIXME: There's probably a better way to do this; perhaps
446
# the workingtree should know how to filter itself.
447
if work_inv.has_id(file_id):
448
del work_inv[file_id]
451
inv_id = rev_id = _gen_revision_id(time.time())
453
inv_tmp = tempfile.TemporaryFile()
454
inv.write_xml(inv_tmp)
456
self.inventory_store.add(inv_tmp, inv_id)
457
mutter('new inventory_id is {%s}' % inv_id)
459
self._write_inventory(work_inv)
461
if timestamp == None:
462
timestamp = time.time()
464
if committer == None:
465
committer = username()
468
timezone = local_time_offset()
470
mutter("building commit log message")
471
rev = Revision(timestamp=timestamp,
474
precursor = self.last_patch(),
479
rev_tmp = tempfile.TemporaryFile()
480
rev.write_xml(rev_tmp)
482
self.revision_store.add(rev_tmp, rev_id)
483
mutter("new revision_id is {%s}" % rev_id)
485
## XXX: Everything up to here can simply be orphaned if we abort
486
## the commit; it will leave junk files behind but that doesn't
489
## TODO: Read back the just-generated changeset, and make sure it
490
## applies and recreates the right state.
492
## TODO: Also calculate and store the inventory SHA1
493
mutter("committing patch r%d" % (self.revno() + 1))
495
mutter("append to revision-history")
496
self.controlfile('revision-history', 'at').write(rev_id + '\n')
501
def get_revision(self, revision_id):
502
"""Return the Revision object for a named revision"""
503
r = Revision.read_xml(self.revision_store[revision_id])
504
assert r.revision_id == revision_id
508
def get_inventory(self, inventory_id):
509
"""Get Inventory object by hash.
511
:todo: Perhaps for this and similar methods, take a revision
512
parameter which can be either an integer revno or a
514
i = Inventory.read_xml(self.inventory_store[inventory_id])
518
def get_revision_inventory(self, revision_id):
519
"""Return inventory of a past revision."""
520
if revision_id == None:
523
return self.get_inventory(self.get_revision(revision_id).inventory_id)
207
526
def revision_history(self):
208
"""Return sequence of revision hashes on to this branch."""
209
raise NotImplementedError('revision_history is abstract')
527
"""Return sequence of revision hashes on to this branch.
529
>>> ScratchBranch().revision_history()
532
return [chomp(l) for l in self.controlfile('revision-history').readlines()]
212
536
"""Return current revision number for this branch.
214
538
That is equivalent to the number of revisions committed to
541
>>> b = ScratchBranch()
544
>>> b.commit('no foo')
217
548
return len(self.revision_history())
219
def last_revision(self):
220
"""Return last patch hash, or None if no history."""
551
def last_patch(self):
552
"""Return last patch hash, or None if no history.
554
>>> ScratchBranch().last_patch() == None
221
557
ph = self.revision_history()
227
def missing_revisions(self, other, stop_revision=None, diverged_ok=False):
228
"""Return a list of new revisions that would perfectly fit.
230
If self and other have not diverged, return a list of the revisions
231
present in other, but missing from self.
233
>>> from bzrlib.workingtree import WorkingTree
234
>>> bzrlib.trace.silent = True
235
>>> d1 = bzrdir.ScratchDir()
236
>>> br1 = d1.open_branch()
237
>>> wt1 = d1.open_workingtree()
238
>>> d2 = bzrdir.ScratchDir()
239
>>> br2 = d2.open_branch()
240
>>> wt2 = d2.open_workingtree()
241
>>> br1.missing_revisions(br2)
243
>>> wt2.commit("lala!", rev_id="REVISION-ID-1")
244
>>> br1.missing_revisions(br2)
246
>>> br2.missing_revisions(br1)
248
>>> wt1.commit("lala!", rev_id="REVISION-ID-1")
249
>>> br1.missing_revisions(br2)
251
>>> wt2.commit("lala!", rev_id="REVISION-ID-2A")
252
>>> br1.missing_revisions(br2)
254
>>> wt1.commit("lala!", rev_id="REVISION-ID-2B")
255
>>> br1.missing_revisions(br2)
256
Traceback (most recent call last):
257
DivergedBranches: These branches have diverged. Try merge.
259
self_history = self.revision_history()
260
self_len = len(self_history)
261
other_history = other.revision_history()
262
other_len = len(other_history)
263
common_index = min(self_len, other_len) -1
264
if common_index >= 0 and \
265
self_history[common_index] != other_history[common_index]:
266
raise DivergedBranches(self, other)
268
if stop_revision is None:
269
stop_revision = other_len
271
assert isinstance(stop_revision, int)
272
if stop_revision > other_len:
273
raise bzrlib.errors.NoSuchRevision(self, stop_revision)
274
return other_history[self_len:stop_revision]
276
def update_revisions(self, other, stop_revision=None):
277
"""Pull in new perfect-fit revisions."""
278
raise NotImplementedError('update_revisions is abstract')
280
def pullable_revisions(self, other, stop_revision):
281
raise NotImplementedError('pullable_revisions is abstract')
283
def revision_id_to_revno(self, revision_id):
284
"""Given a revision id, return its revno"""
285
if revision_id is None:
287
history = self.revision_history()
289
return history.index(revision_id) + 1
291
raise bzrlib.errors.NoSuchRevision(self, revision_id)
293
def get_rev_id(self, revno, history=None):
294
"""Find the revision id of the specified revno."""
562
def lookup_revision(self, revno):
563
"""Return revision hash for revision number."""
298
history = self.revision_history()
299
elif revno <= 0 or revno > len(history):
300
raise bzrlib.errors.NoSuchRevision(self, revno)
301
return history[revno - 1]
303
def pull(self, source, overwrite=False, stop_revision=None):
304
raise NotImplementedError('pull is abstract')
568
# list is 0-based; revisions are 1-based
569
return self.revision_history()[revno-1]
571
bailout("no such revision %s" % revno)
574
def revision_tree(self, revision_id):
575
"""Return Tree for a revision on this branch.
577
`revision_id` may be None for the null revision, in which case
578
an `EmptyTree` is returned."""
580
if revision_id == None:
583
inv = self.get_revision_inventory(revision_id)
584
return RevisionTree(self.text_store, inv)
587
def working_tree(self):
588
"""Return a `Tree` for the working copy."""
589
return WorkingTree(self.base, self.read_working_inventory())
306
592
def basis_tree(self):
307
593
"""Return `Tree` object for last revision.
309
595
If there are no revisions yet, return an `EmptyTree`.
311
return self.repository.revision_tree(self.last_revision())
313
def rename_one(self, from_rel, to_rel):
316
This can change the directory or the filename or both.
318
raise NotImplementedError('rename_one is abstract')
320
def move(self, from_paths, to_name):
323
to_name must exist as a versioned directory.
325
If to_name exists and is a directory, the files are moved into
326
it, keeping their old names. If it is a directory,
328
Note that to_name is only the last component of the new name;
329
this doesn't change the directory.
331
This returns a list of (from_path, to_path) pairs for each
334
raise NotImplementedError('move is abstract')
336
def get_parent(self):
337
"""Return the parent location of the branch.
339
This is the default location for push/pull/missing. The usual
340
pattern is that the user can override it by specifying a
343
raise NotImplementedError('get_parent is abstract')
345
def get_push_location(self):
346
"""Return the None or the location to push this branch to."""
347
raise NotImplementedError('get_push_location is abstract')
349
def set_push_location(self, location):
350
"""Set a new push location for this branch."""
351
raise NotImplementedError('set_push_location is abstract')
353
def set_parent(self, url):
354
raise NotImplementedError('set_parent is abstract')
356
def check_revno(self, revno):
358
Check whether a revno corresponds to any revision.
359
Zero (the NULL revision) is considered valid.
362
self.check_real_revno(revno)
364
def check_real_revno(self, revno):
366
Check whether a revno corresponds to a real revision.
367
Zero (the NULL revision) is considered invalid
369
if revno < 1 or revno > self.revno():
370
raise InvalidRevisionNumber(revno)
373
def clone(self, *args, **kwargs):
374
"""Clone this branch into to_bzrdir preserving all semantic values.
376
revision_id: if not None, the revision history in the new branch will
377
be truncated to end with revision_id.
379
# for API compatability, until 0.8 releases we provide the old api:
380
# def clone(self, to_location, revision=None, basis_branch=None, to_branch_format=None):
381
# after 0.8 releases, the *args and **kwargs should be changed:
382
# def clone(self, to_bzrdir, revision_id=None):
383
if (kwargs.get('to_location', None) or
384
kwargs.get('revision', None) or
385
kwargs.get('basis_branch', None) or
386
(len(args) and isinstance(args[0], basestring))):
387
# backwards compatability api:
388
warn("Branch.clone() has been deprecated for BzrDir.clone() from"
389
" bzrlib 0.8.", DeprecationWarning, stacklevel=3)
392
basis_branch = args[2]
394
basis_branch = kwargs.get('basis_branch', None)
396
basis = basis_branch.bzrdir
401
revision_id = args[1]
403
revision_id = kwargs.get('revision', None)
408
# no default to raise if not provided.
409
url = kwargs.get('to_location')
410
return self.bzrdir.clone(url,
411
revision_id=revision_id,
412
basis=basis).open_branch()
414
# generate args by hand
416
revision_id = args[1]
418
revision_id = kwargs.get('revision_id', None)
422
# no default to raise if not provided.
423
to_bzrdir = kwargs.get('to_bzrdir')
424
result = self._format.initialize(to_bzrdir)
425
self.copy_content_into(result, revision_id=revision_id)
429
def sprout(self, to_bzrdir, revision_id=None):
430
"""Create a new line of development from the branch, into to_bzrdir.
432
revision_id: if not None, the revision history in the new branch will
433
be truncated to end with revision_id.
435
result = self._format.initialize(to_bzrdir)
436
self.copy_content_into(result, revision_id=revision_id)
437
result.set_parent(self.bzrdir.root_transport.base)
441
def copy_content_into(self, destination, revision_id=None):
442
"""Copy the content of self into destination.
444
revision_id: if not None, the revision history in the new branch will
445
be truncated to end with revision_id.
447
new_history = self.revision_history()
448
if revision_id is not None:
450
new_history = new_history[:new_history.index(revision_id) + 1]
452
rev = self.repository.get_revision(revision_id)
453
new_history = rev.get_history(self.repository)[1:]
454
destination.set_revision_history(new_history)
455
parent = self.get_parent()
457
destination.set_parent(parent)
460
class BranchFormat(object):
461
"""An encapsulation of the initialization and open routines for a format.
463
Formats provide three things:
464
* An initialization routine,
468
Formats are placed in an dict by their format string for reference
469
during branch opening. Its not required that these be instances, they
470
can be classes themselves with class methods - it simply depends on
471
whether state is needed for a given format or not.
473
Once a format is deprecated, just deprecate the initialize and open
474
methods on the format class. Do not deprecate the object, as the
475
object will be created every time regardless.
478
_default_format = None
479
"""The default format used for new branches."""
482
"""The known formats."""
485
def find_format(klass, a_bzrdir):
486
"""Return the format for the branch object in a_bzrdir."""
488
transport = a_bzrdir.get_branch_transport(None)
489
format_string = transport.get("format").read()
490
return klass._formats[format_string]
492
raise NotBranchError(path=transport.base)
494
raise errors.UnknownFormatError(format_string)
497
def get_default_format(klass):
498
"""Return the current default format."""
499
return klass._default_format
501
def get_format_string(self):
502
"""Return the ASCII format string that identifies this format."""
503
raise NotImplementedError(self.get_format_string)
505
def _find_modes(self, t):
506
"""Determine the appropriate modes for files and directories.
508
FIXME: When this merges into, or from storage,
509
this code becomes delgatable to a LockableFiles instance.
511
For now its cribbed and returns (dir_mode, file_mode)
515
except errors.TransportNotPossible:
519
dir_mode = st.st_mode & 07777
520
# Remove the sticky and execute bits for files
521
file_mode = dir_mode & ~07111
522
if not BzrBranch._set_dir_mode:
524
if not BzrBranch._set_file_mode:
526
return dir_mode, file_mode
528
def initialize(self, a_bzrdir):
529
"""Create a branch of this format in a_bzrdir."""
530
raise NotImplementedError(self.initialized)
532
def is_supported(self):
533
"""Is this format supported?
535
Supported formats can be initialized and opened.
536
Unsupported formats may not support initialization or committing or
537
some other features depending on the reason for not being supported.
541
def open(self, a_bzrdir, _found=False):
542
"""Return the branch object for a_bzrdir
544
_found is a private parameter, do not use it. It is used to indicate
545
if format probing has already be done.
547
raise NotImplementedError(self.open)
550
def register_format(klass, format):
551
klass._formats[format.get_format_string()] = format
554
def set_default_format(klass, format):
555
klass._default_format = format
558
def unregister_format(klass, format):
559
assert klass._formats[format.get_format_string()] is format
560
del klass._formats[format.get_format_string()]
563
class BzrBranchFormat4(BranchFormat):
564
"""Bzr branch format 4.
567
- a revision-history file.
568
- a branch-lock lock file [ to be shared with the bzrdir ]
571
def initialize(self, a_bzrdir):
572
"""Create a branch of this format in a_bzrdir."""
573
mutter('creating branch in %s', a_bzrdir.transport.base)
574
branch_transport = a_bzrdir.get_branch_transport(self)
575
utf8_files = [('revision-history', ''),
578
control_files = LockableFiles(branch_transport, 'branch-lock')
579
control_files.lock_write()
581
for file, content in utf8_files:
582
control_files.put_utf8(file, content)
584
control_files.unlock()
585
return self.open(a_bzrdir, _found=True)
588
super(BzrBranchFormat4, self).__init__()
589
self._matchingbzrdir = bzrdir.BzrDirFormat6()
591
def open(self, a_bzrdir, _found=False):
592
"""Return the branch object for a_bzrdir
594
_found is a private parameter, do not use it. It is used to indicate
595
if format probing has already be done.
598
# we are being called directly and must probe.
599
raise NotImplementedError
600
transport = a_bzrdir.get_branch_transport(self)
601
control_files = LockableFiles(transport, 'branch-lock')
602
return BzrBranch(_format=self,
603
_control_files=control_files,
607
class BzrBranchFormat5(BranchFormat):
608
"""Bzr branch format 5.
611
- a revision-history file.
616
def get_format_string(self):
617
"""See BranchFormat.get_format_string()."""
618
return "Bazaar-NG branch format 5\n"
620
def initialize(self, a_bzrdir):
621
"""Create a branch of this format in a_bzrdir."""
622
mutter('creating branch in %s', a_bzrdir.transport.base)
623
branch_transport = a_bzrdir.get_branch_transport(self)
625
utf8_files = [('revision-history', ''),
629
branch_transport.put(lock_file, StringIO()) # TODO get the file mode from the bzrdir lock files., mode=file_mode)
630
control_files = LockableFiles(branch_transport, 'lock')
631
control_files.lock_write()
632
control_files.put_utf8('format', self.get_format_string())
634
for file, content in utf8_files:
635
control_files.put_utf8(file, content)
637
control_files.unlock()
638
return self.open(a_bzrdir, _found=True, )
641
super(BzrBranchFormat5, self).__init__()
642
self._matchingbzrdir = bzrdir.BzrDirMetaFormat1()
644
def open(self, a_bzrdir, _found=False):
645
"""Return the branch object for a_bzrdir
647
_found is a private parameter, do not use it. It is used to indicate
648
if format probing has already be done.
651
format = BranchFormat.find_format(a_bzrdir)
652
assert format.__class__ == self.__class__
653
transport = a_bzrdir.get_branch_transport(None)
654
control_files = LockableFiles(transport, 'lock')
655
return BzrBranch(_format=self,
656
_control_files=control_files,
660
class BranchReferenceFormat(BranchFormat):
661
"""Bzr branch reference format.
663
Branch references are used in implementing checkouts, they
664
act as an alias to the real branch which is at some other url.
671
def get_format_string(self):
672
"""See BranchFormat.get_format_string()."""
673
return "Bazaar-NG Branch Reference Format 1\n"
675
def initialize(self, a_bzrdir, target_branch=None):
676
"""Create a branch of this format in a_bzrdir."""
677
if target_branch is None:
678
# this format does not implement branch itself, thus the implicit
679
# creation contract must see it as uninitializable
680
raise errors.UninitializableFormat(self)
681
mutter('creating branch reference in %s', a_bzrdir.transport.base)
682
branch_transport = a_bzrdir.get_branch_transport(self)
683
# FIXME rbc 20060209 one j-a-ms encoding branch lands this str() cast is not needed.
684
branch_transport.put('location', StringIO(str(target_branch.bzrdir.root_transport.base)))
685
branch_transport.put('format', StringIO(self.get_format_string()))
686
return self.open(a_bzrdir, _found=True)
689
super(BranchReferenceFormat, self).__init__()
690
self._matchingbzrdir = bzrdir.BzrDirMetaFormat1()
692
def _make_reference_clone_function(format, a_branch):
693
"""Create a clone() routine for a branch dynamically."""
694
def clone(to_bzrdir, revision_id=None):
695
"""See Branch.clone()."""
696
return format.initialize(to_bzrdir, a_branch)
697
# cannot obey revision_id limits when cloning a reference ...
698
# FIXME RBC 20060210 either nuke revision_id for clone, or
699
# emit some sort of warning/error to the caller ?!
702
def open(self, a_bzrdir, _found=False):
703
"""Return the branch that the branch reference in a_bzrdir points at.
705
_found is a private parameter, do not use it. It is used to indicate
706
if format probing has already be done.
709
format = BranchFormat.find_format(a_bzrdir)
710
assert format.__class__ == self.__class__
711
transport = a_bzrdir.get_branch_transport(None)
712
real_bzrdir = bzrdir.BzrDir.open(transport.get('location').read())
713
result = real_bzrdir.open_branch()
714
# this changes the behaviour of result.clone to create a new reference
715
# rather than a copy of the content of the branch.
716
# I did not use a proxy object because that needs much more extensive
717
# testing, and we are only changing one behaviour at the moment.
718
# If we decide to alter more behaviours - i.e. the implicit nickname
719
# then this should be refactored to introduce a tested proxy branch
720
# and a subclass of that for use in overriding clone() and ....
722
result.clone = self._make_reference_clone_function(result)
726
# formats which have no format string are not discoverable
727
# and not independently creatable, so are not registered.
728
__default_format = BzrBranchFormat5()
729
BranchFormat.register_format(__default_format)
730
BranchFormat.register_format(BranchReferenceFormat())
731
BranchFormat.set_default_format(__default_format)
732
_legacy_formats = [BzrBranchFormat4(),
735
class BzrBranch(Branch):
736
"""A branch stored in the actual filesystem.
738
Note that it's "local" in the context of the filesystem; it doesn't
739
really matter if it's on an nfs/smb/afs/coda/... share, as long as
740
it's writable, and can be accessed via the normal filesystem API.
742
# We actually expect this class to be somewhat short-lived; part of its
743
# purpose is to try to isolate what bits of the branch logic are tied to
744
# filesystem access, so that in a later step, we can extricate them to
745
# a separarte ("storage") class.
746
_inventory_weave = None
748
# Map some sort of prefix into a namespace
749
# stuff like "revno:10", "revid:", etc.
750
# This should match a prefix with a function which accepts
751
REVISION_NAMESPACES = {}
753
def __init__(self, transport=DEPRECATED_PARAMETER, init=DEPRECATED_PARAMETER,
754
relax_version_check=DEPRECATED_PARAMETER, _format=None,
755
_control_files=None, a_bzrdir=None):
756
"""Create new branch object at a particular location.
758
transport -- A Transport object, defining how to access files.
760
init -- If True, create new control files in a previously
761
unversioned directory. If False, the branch must already
764
relax_version_check -- If true, the usual check for the branch
765
version is not applied. This is intended only for
766
upgrade/recovery type use; it's not guaranteed that
767
all operations will work on old format branches.
770
self.bzrdir = bzrdir.BzrDir.open(transport.base)
772
self.bzrdir = a_bzrdir
773
self._transport = self.bzrdir.transport.clone('..')
774
self._base = self._transport.base
775
self._format = _format
776
if _control_files is None:
777
raise BzrBadParameterMissing('_control_files')
778
self.control_files = _control_files
779
if deprecated_passed(init):
780
warn("BzrBranch.__init__(..., init=XXX): The init parameter is "
781
"deprecated as of bzr 0.8. Please use Branch.create().",
785
# this is slower than before deprecation, oh well never mind.
787
self._initialize(transport.base)
788
self._check_format(_format)
789
if deprecated_passed(relax_version_check):
790
warn("BzrBranch.__init__(..., relax_version_check=XXX_: The "
791
"relax_version_check parameter is deprecated as of bzr 0.8. "
792
"Please use BzrDir.open_downlevel, or a BzrBranchFormat's "
796
if (not relax_version_check
797
and not self._format.is_supported()):
798
raise errors.UnsupportedFormatError(
799
'sorry, branch format %r not supported' % fmt,
800
['use a different bzr version',
801
'or remove the .bzr directory'
802
' and "bzr init" again'])
803
if deprecated_passed(transport):
804
warn("BzrBranch.__init__(transport=XXX...): The transport "
805
"parameter is deprecated as of bzr 0.8. "
806
"Please use Branch.open, or bzrdir.open_branch().",
809
# TODO change this to search upwards if needed.
810
self.repository = self.bzrdir.open_repository()
813
return '%s(%r)' % (self.__class__.__name__, self.base)
597
>>> b = ScratchBranch(files=['foo'])
598
>>> b.basis_tree().has_filename('foo')
600
>>> b.working_tree().has_filename('foo')
603
>>> b.commit('add foo')
604
>>> b.basis_tree().has_filename('foo')
607
r = self.last_patch()
611
return RevisionTree(self.text_store, self.get_revision_inventory(r))
615
def write_log(self, utc=False):
616
"""Write out human-readable log of commits to this branch
618
:param utc: If true, show dates in universal time, not local time."""
619
## TODO: Option to choose either original, utc or local timezone
622
for p in self.revision_history():
624
print 'revno:', revno
625
## TODO: Show hash if --id is given.
626
##print 'revision-hash:', p
627
rev = self.get_revision(p)
628
print 'committer:', rev.committer
629
print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0))
631
## opportunistic consistency check, same as check_patch_chaining
632
if rev.precursor != precursor:
633
bailout("mismatched precursor!")
637
print ' (no message)'
639
for l in rev.message.split('\n'):
647
def show_status(branch, show_all=False):
648
"""Display single-line status for non-ignored working files.
650
The list is show sorted in order by file name.
652
>>> b = ScratchBranch(files=['foo', 'foo~'])
658
>>> b.commit("add foo")
661
:todo: Get state for single files.
663
:todo: Perhaps show a slash at the end of directory names.
667
# We have to build everything into a list first so that it can
668
# sorted by name, incorporating all the different sources.
670
# FIXME: Rather than getting things in random order and then sorting,
671
# just step through in order.
673
# Interesting case: the old ID for a file has been removed,
674
# but a new file has been created under that name.
676
old = branch.basis_tree()
677
old_inv = old.inventory
678
new = branch.working_tree()
679
new_inv = new.inventory
681
for fs, fid, oldname, newname, kind in diff_trees(old, new):
683
show_status(fs, kind,
684
oldname + ' => ' + newname)
685
elif fs == 'A' or fs == 'M':
686
show_status(fs, kind, newname)
688
show_status(fs, kind, oldname)
691
show_status(fs, kind, newname)
694
show_status(fs, kind, newname)
696
show_status(fs, kind, newname)
698
bailout("wierd file state %r" % ((fs, fid),))
702
class ScratchBranch(Branch):
703
"""Special test class: a branch that cleans up after itself.
705
>>> b = ScratchBranch()
713
def __init__(self, files = []):
714
"""Make a test branch.
716
This creates a temporary directory and runs init-tree in it.
718
If any files are listed, they are created in the working copy.
720
Branch.__init__(self, tempfile.mkdtemp(), init=True)
722
file(os.path.join(self.base, f), 'w').write('content of %s' % f)
817
725
def __del__(self):
818
# TODO: It might be best to do this somewhere else,
819
# but it is nice for a Branch object to automatically
820
# cache it's information.
821
# Alternatively, we could have the Transport objects cache requests
822
# See the earlier discussion about how major objects (like Branch)
823
# should never expect their __del__ function to run.
824
# XXX: cache_root seems to be unused, 2006-01-13 mbp
825
if hasattr(self, 'cache_root') and self.cache_root is not None:
827
shutil.rmtree(self.cache_root)
830
self.cache_root = None
835
base = property(_get_base, doc="The URL for the root of this branch.")
837
def _finish_transaction(self):
838
"""Exit the current transaction."""
839
return self.control_files._finish_transaction()
841
def get_transaction(self):
842
"""Return the current active transaction.
844
If no transaction is active, this returns a passthrough object
845
for which all data is immediately flushed and no caching happens.
847
# this is an explicit function so that we can do tricky stuff
848
# when the storage in rev_storage is elsewhere.
849
# we probably need to hook the two 'lock a location' and
850
# 'have a transaction' together more delicately, so that
851
# we can have two locks (branch and storage) and one transaction
852
# ... and finishing the transaction unlocks both, but unlocking
853
# does not. - RBC 20051121
854
return self.control_files.get_transaction()
856
def _set_transaction(self, transaction):
857
"""Set a new active transaction."""
858
return self.control_files._set_transaction(transaction)
860
def abspath(self, name):
861
"""See Branch.abspath."""
862
return self.control_files._transport.abspath(name)
864
def _check_format(self, format):
865
"""Identify the branch format if needed.
867
The format is stored as a reference to the format object in
868
self._format for code that needs to check it later.
870
The format parameter is either None or the branch format class
871
used to open this branch.
873
FIXME: DELETE THIS METHOD when pre 0.8 support is removed.
876
format = BzrBranchFormat.find_format(self.bzrdir)
877
self._format = format
878
mutter("got branch format %s", self._format)
881
def get_root_id(self):
882
"""See Branch.get_root_id."""
883
tree = self.repository.revision_tree(self.last_revision())
884
return tree.inventory.root.file_id
886
def lock_write(self):
887
# TODO: test for failed two phase locks. This is known broken.
888
self.control_files.lock_write()
889
self.repository.lock_write()
892
# TODO: test for failed two phase locks. This is known broken.
893
self.control_files.lock_read()
894
self.repository.lock_read()
897
# TODO: test for failed two phase locks. This is known broken.
898
self.repository.unlock()
899
self.control_files.unlock()
901
def peek_lock_mode(self):
902
if self.control_files._lock_count == 0:
905
return self.control_files._lock_mode
908
def print_file(self, file, revision_id):
909
"""See Branch.print_file."""
910
return self.repository.print_file(file, revision_id)
913
def append_revision(self, *revision_ids):
914
"""See Branch.append_revision."""
915
for revision_id in revision_ids:
916
mutter("add {%s} to revision-history" % revision_id)
917
rev_history = self.revision_history()
918
rev_history.extend(revision_ids)
919
self.set_revision_history(rev_history)
922
def set_revision_history(self, rev_history):
923
"""See Branch.set_revision_history."""
924
self.control_files.put_utf8(
925
'revision-history', '\n'.join(rev_history))
927
def get_revision_delta(self, revno):
928
"""Return the delta for one revision.
930
The delta is relative to its mainline predecessor, or the
931
empty tree for revision 1.
933
assert isinstance(revno, int)
934
rh = self.revision_history()
935
if not (1 <= revno <= len(rh)):
936
raise InvalidRevisionNumber(revno)
938
# revno is 1-based; list is 0-based
940
new_tree = self.repository.revision_tree(rh[revno-1])
942
old_tree = EmptyTree()
944
old_tree = self.repository.revision_tree(rh[revno-2])
945
return compare_trees(old_tree, new_tree)
948
def revision_history(self):
949
"""See Branch.revision_history."""
950
# FIXME are transactions bound to control files ? RBC 20051121
951
transaction = self.get_transaction()
952
history = transaction.map.find_revision_history()
953
if history is not None:
954
mutter("cache hit for revision-history in %s", self)
956
history = [l.rstrip('\r\n') for l in
957
self.control_files.get_utf8('revision-history').readlines()]
958
transaction.map.add_revision_history(history)
959
# this call is disabled because revision_history is
960
# not really an object yet, and the transaction is for objects.
961
# transaction.register_clean(history, precious=True)
964
def update_revisions(self, other, stop_revision=None):
965
"""See Branch.update_revisions."""
966
from bzrlib.fetch import greedy_fetch
968
if stop_revision is None:
969
stop_revision = other.last_revision()
970
### Should this be checking is_ancestor instead of revision_history?
971
if (stop_revision is not None and
972
stop_revision in self.revision_history()):
974
greedy_fetch(to_branch=self, from_branch=other,
975
revision=stop_revision)
976
pullable_revs = self.pullable_revisions(other, stop_revision)
977
if len(pullable_revs) > 0:
978
self.append_revision(*pullable_revs)
980
def pullable_revisions(self, other, stop_revision):
981
"""See Branch.pullable_revisions."""
982
other_revno = other.revision_id_to_revno(stop_revision)
984
return self.missing_revisions(other, other_revno)
985
except DivergedBranches, e:
987
pullable_revs = get_intervening_revisions(self.last_revision(),
990
assert self.last_revision() not in pullable_revs
992
except bzrlib.errors.NotAncestor:
993
if is_ancestor(self.last_revision(), stop_revision, self):
998
def basis_tree(self):
999
"""See Branch.basis_tree."""
1000
return self.repository.revision_tree(self.last_revision())
1002
@deprecated_method(zero_eight)
1003
def working_tree(self):
1004
"""Create a Working tree object for this branch."""
1005
from bzrlib.workingtree import WorkingTree
1006
from bzrlib.transport.local import LocalTransport
1007
if (self.base.find('://') != -1 or
1008
not isinstance(self._transport, LocalTransport)):
1009
raise NoWorkingTree(self.base)
1010
return self.bzrdir.open_workingtree()
1013
def pull(self, source, overwrite=False, stop_revision=None):
1014
"""See Branch.pull."""
1017
old_count = len(self.revision_history())
1019
self.update_revisions(source,stop_revision)
1020
except DivergedBranches:
1024
self.set_revision_history(source.revision_history())
1025
new_count = len(self.revision_history())
1026
return new_count - old_count
1030
def get_parent(self):
1031
"""See Branch.get_parent."""
1033
_locs = ['parent', 'pull', 'x-pull']
1036
return self.control_files.get_utf8(l).read().strip('\n')
1041
def get_push_location(self):
1042
"""See Branch.get_push_location."""
1043
config = bzrlib.config.BranchConfig(self)
1044
push_loc = config.get_user_option('push_location')
1047
def set_push_location(self, location):
1048
"""See Branch.set_push_location."""
1049
config = bzrlib.config.LocationConfig(self.base)
1050
config.set_user_option('push_location', location)
1053
def set_parent(self, url):
1054
"""See Branch.set_parent."""
1055
# TODO: Maybe delete old location files?
1056
# URLs should never be unicode, even on the local fs,
1057
# FIXUP this and get_parent in a future branch format bump:
1058
# read and rewrite the file, and have the new format code read
1059
# using .get not .get_utf8. RBC 20060125
1060
self.control_files.put_utf8('parent', url + '\n')
1062
def tree_config(self):
1063
return TreeConfig(self)
1065
def _get_truncated_history(self, revision_id):
1066
history = self.revision_history()
1067
if revision_id is None:
1070
idx = history.index(revision_id)
1072
raise InvalidRevisionId(revision_id=revision, branch=self)
1073
return history[:idx+1]
1076
def _clone_weave(self, to_location, revision=None, basis_branch=None):
1078
from bzrlib.workingtree import WorkingTree
1079
assert isinstance(to_location, basestring)
1080
if basis_branch is not None:
1081
note("basis_branch is not supported for fast weave copy yet.")
1083
history = self._get_truncated_history(revision)
1084
if not bzrlib.osutils.lexists(to_location):
1085
os.mkdir(to_location)
1086
bzrdir_to = self.bzrdir._format.initialize(to_location)
1087
self.repository.clone(bzrdir_to)
1088
branch_to = bzrdir_to.create_branch()
1089
mutter("copy branch from %s to %s", self, branch_to)
1091
# FIXME duplicate code with base .clone().
1092
# .. would template method be useful here? RBC 20051207
1093
branch_to.set_parent(self.base)
1094
branch_to.append_revision(*history)
1095
WorkingTree.create(branch_to, branch_to.base)
1100
class BranchTestProviderAdapter(object):
1101
"""A tool to generate a suite testing multiple branch formats at once.
1103
This is done by copying the test once for each transport and injecting
1104
the transport_server, transport_readonly_server, and branch_format
1105
classes into each copy. Each copy is also given a new id() to make it
1109
def __init__(self, transport_server, transport_readonly_server, formats):
1110
self._transport_server = transport_server
1111
self._transport_readonly_server = transport_readonly_server
1112
self._formats = formats
726
"""Destroy the test branch, removing the scratch directory."""
727
shutil.rmtree(self.base)
1114
def adapt(self, test):
1115
result = TestSuite()
1116
for branch_format, bzrdir_format in self._formats:
1117
new_test = deepcopy(test)
1118
new_test.transport_server = self._transport_server
1119
new_test.transport_readonly_server = self._transport_readonly_server
1120
new_test.bzrdir_format = bzrdir_format
1121
new_test.branch_format = branch_format
1122
def make_new_test_id():
1123
new_id = "%s(%s)" % (new_test.id(), branch_format.__class__.__name__)
1124
return lambda: new_id
1125
new_test.id = make_new_test_id()
1126
result.addTest(new_test)
1130
731
######################################################################
1134
@deprecated_function(zero_eight)
1135
def ScratchBranch(*args, **kwargs):
1136
"""See bzrlib.bzrdir.ScratchDir."""
1137
d = ScratchDir(*args, **kwargs)
1138
return d.open_branch()
1141
@deprecated_function(zero_eight)
1142
def is_control_file(*args, **kwargs):
1143
"""See bzrlib.workingtree.is_control_file."""
1144
return bzrlib.workingtree.is_control_file(*args, **kwargs)
735
def is_control_file(filename):
736
## FIXME: better check
737
filename = os.path.normpath(filename)
738
while filename != '':
739
head, tail = os.path.split(filename)
740
## mutter('check %r for control file' % ((head, tail), ))
741
if tail == bzrlib.BZRDIR:
748
def _gen_revision_id(when):
749
"""Return new revision-id."""
750
s = '%s-%s-' % (user_email(), compact_date(when))
751
s += hexlify(rand_bytes(8))
755
def _gen_file_id(name):
756
"""Return new file id.
758
This should probably generate proper UUIDs, but for the moment we
759
cope with just randomness because running uuidgen every time is
761
assert '/' not in name
762
while name[0] == '.':
764
s = hexlify(rand_bytes(8))
765
return '-'.join((name, compact_date(time.time()), s))