1
1
# Copyright (C) 2005, 2006 Canonical Ltd
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
5
5
# the Free Software Foundation; either version 2 of the License, or
6
6
# (at your option) any later version.
8
8
# This program is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
11
# GNU General Public License for more details.
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
39
39
# At the moment they may alias the inventory and have old copies of it in
40
40
# memory. (Now done? -- mbp 20060309)
42
from binascii import hexlify
44
42
from copy import deepcopy
45
43
from cStringIO import StringIO
55
from bzrlib import bzrdir, errors, ignores, osutils, urlutils
56
50
from bzrlib.atomicfile import AtomicFile
51
from bzrlib.branch import (Branch,
58
53
from bzrlib.conflicts import Conflict, ConflictList, CONFLICT_SUFFIXES
54
import bzrlib.bzrdir as bzrdir
59
55
from bzrlib.decorators import needs_read_lock, needs_write_lock
56
import bzrlib.errors as errors
60
57
from bzrlib.errors import (BzrCheckError,
62
59
ConflictFormatError,
63
61
WeaveRevisionNotPresent,
91
90
from bzrlib.progress import DummyProgress, ProgressPhase
92
91
from bzrlib.revision import NULL_REVISION
93
92
from bzrlib.rio import RioReader, rio_file, Stanza
94
from bzrlib.symbol_versioning import (deprecated_passed,
93
from bzrlib.symbol_versioning import *
94
from bzrlib.textui import show_status
96
from bzrlib.transform import build_tree
100
97
from bzrlib.trace import mutter, note
101
from bzrlib.transform import build_tree
102
98
from bzrlib.transport import get_transport
103
99
from bzrlib.transport.local import LocalTransport
104
from bzrlib.textui import show_status
107
101
import bzrlib.xml5
110
# the regex removes any weird characters; we don't escape them
111
# but rather just pull them out
112
_gen_file_id_re = re.compile(r'[^\w.]')
113
_gen_id_suffix = None
117
def _next_id_suffix():
118
"""Create a new file id suffix that is reasonably unique.
120
On the first call we combine the current time with 64 bits of randomness
121
to give a highly probably globally unique number. Then each call in the same
122
process adds 1 to a serial number we append to that unique value.
124
# XXX TODO: change bzrlib.add.smart_add to call workingtree.add() rather
125
# than having to move the id randomness out of the inner loop like this.
126
# XXX TODO: for the global randomness this uses we should add the thread-id
127
# before the serial #.
128
global _gen_id_suffix, _gen_id_serial
129
if _gen_id_suffix is None:
130
_gen_id_suffix = "-%s-%s-" % (compact_date(time()), rand_chars(16))
132
return _gen_id_suffix + str(_gen_id_serial)
135
104
def gen_file_id(name):
136
"""Return new file id for the basename 'name'.
138
The uniqueness is supplied from _next_id_suffix.
140
# The real randomness is in the _next_id_suffix, the
141
# rest of the identifier is just to be nice.
143
# 1) Remove non-ascii word characters to keep the ids portable
144
# 2) squash to lowercase, so the file id doesn't have to
145
# be escaped (case insensitive filesystems would bork for ids
146
# that only differred in case without escaping).
147
# 3) truncate the filename to 20 chars. Long filenames also bork on some
149
# 4) Removing starting '.' characters to prevent the file ids from
150
# being considered hidden.
151
ascii_word_only = _gen_file_id_re.sub('', name.lower())
152
short_no_dots = ascii_word_only.lstrip('.')[:20]
153
return short_no_dots + _next_id_suffix()
105
"""Return new file id.
107
This should probably generate proper UUIDs, but for the moment we
108
cope with just randomness because running uuidgen every time is
111
from binascii import hexlify
112
from time import time
115
idx = name.rfind('/')
117
name = name[idx+1 : ]
118
idx = name.rfind('\\')
120
name = name[idx+1 : ]
122
# make it not a hidden file
123
name = name.lstrip('.')
125
# remove any wierd characters; we don't escape them but rather
127
name = re.sub(r'[^\w.]', '', name)
129
s = hexlify(rand_bytes(8))
130
return '-'.join((name, compact_date(time()), s))
156
133
def gen_root_id():
270
247
self._branch = branch
272
249
self._branch = self.bzrdir.open_branch()
250
assert isinstance(self.branch, Branch), \
251
"branch %r is not a Branch" % self.branch
273
252
self.basedir = realpath(basedir)
274
253
# if branch is at our basedir and is a format 6 or less
275
254
if isinstance(self._format, WorkingTreeFormat2):
276
255
# share control object
277
256
self._control_files = self.branch.control_files
279
# assume all other formats have their own control files.
258
# only ready for format 3
259
assert isinstance(self._format, WorkingTreeFormat3)
280
260
assert isinstance(_control_files, LockableFiles), \
281
261
"_control_files must be a LockableFiles, not %r" \
287
267
# if needed, or, when the cache sees a change, append it to the hash
288
268
# cache file, and have the parser take the most recent entry for a
289
269
# given path only.
290
cache_filename = self.bzrdir.get_workingtree_transport(None).local_abspath('stat-cache')
270
cache_filename = self.bzrdir.get_workingtree_transport(None).abspath('stat-cache')
291
271
hc = self._hashcache = HashCache(basedir, cache_filename, self._control_files._file_mode)
293
273
# is this scan needed ? it makes things kinda slow.
296
276
if hc.needs_write:
297
277
mutter("write hc")
328
308
def is_control_filename(self, filename):
329
309
"""True if filename is the name of a control file in this tree.
331
:param filename: A filename within the tree. This is a relative path
332
from the root of this tree.
334
311
This is true IF and ONLY IF the filename is part of the meta data
335
312
that bzr controls in this tree. I.E. a random .bzr directory placed
336
313
on disk will not be a control file for this tree.
338
return self.bzrdir.is_control_filename(filename)
316
self.bzrdir.transport.relpath(self.abspath(filename))
318
except errors.PathNotChild:
341
322
def open(path=None, _unsupported=False):
357
338
run into /. If there isn't one, raises NotBranchError.
358
339
TODO: give this a new exception.
359
340
If there is one, it is returned, along with the unused portion of path.
361
:return: The WorkingTree that contains 'path', and the rest of path
364
path = osutils.getcwd()
365
344
control, relpath = bzrdir.BzrDir.open_containing(path)
367
345
return control.open_workingtree(), relpath
446
424
return bzrdir.BzrDir.create_standalone_workingtree(directory)
448
def relpath(self, path):
449
"""Return the local path portion from a given path.
451
The path may be absolute or relative. If its a relative path it is
452
interpreted relative to the python current working directory.
454
return relpath(self.basedir, path)
426
def relpath(self, abs):
427
"""Return the local path portion from a given absolute path."""
428
return relpath(self.basedir, abs)
456
430
def has_filename(self, filename):
457
return osutils.lexists(self.abspath(filename))
431
return bzrlib.osutils.lexists(self.abspath(filename))
459
433
def get_file(self, file_id):
460
434
return self.get_file_byname(self.id2path(file_id))
462
def get_file_text(self, file_id):
463
return self.get_file(file_id).read()
465
436
def get_file_byname(self, filename):
466
437
return file(self.abspath(filename), 'rb')
468
def get_parent_ids(self):
469
"""See Tree.get_parent_ids.
471
This implementation reads the pending merges list and last_revision
472
value and uses that to decide what the parents list should be.
474
last_rev = self.last_revision()
479
other_parents = self.pending_merges()
480
return parents + other_parents
482
439
def get_root_id(self):
483
440
"""Return the id of this trees root"""
484
441
inv = self.read_working_inventory()
533
490
# but with branch a kwarg now, passing in args as is results in the
534
491
#message being used for the branch
535
492
args = (DEPRECATED_PARAMETER, message, ) + args
536
committed_id = Commit().commit( working_tree=self, revprops=revprops,
493
Commit().commit(working_tree=self, revprops=revprops, *args, **kwargs)
538
494
self._set_inventory(self.read_working_inventory())
541
496
def id2abspath(self, file_id):
542
497
return self.abspath(self.id2path(file_id))
560
515
return os.path.getsize(self.id2abspath(file_id))
563
def get_file_sha1(self, file_id, path=None):
565
path = self._inventory.id2path(file_id)
518
def get_file_sha1(self, file_id):
519
path = self._inventory.id2path(file_id)
566
520
return self._hashcache.get_sha1(path)
568
def get_file_mtime(self, file_id, path=None):
570
path = self._inventory.id2path(file_id)
571
return os.lstat(self.abspath(path)).st_mtime
573
if not supports_executable():
574
def is_executable(self, file_id, path=None):
522
def is_executable(self, file_id):
523
if not supports_executable():
575
524
return self._inventory[file_id].executable
577
def is_executable(self, file_id, path=None):
579
path = self._inventory.id2path(file_id)
526
path = self._inventory.id2path(file_id)
580
527
mode = os.lstat(self.abspath(path)).st_mode
581
return bool(stat.S_ISREG(mode) and stat.S_IEXEC & mode)
528
return bool(stat.S_ISREG(mode) and stat.S_IEXEC&mode)
583
530
@needs_write_lock
584
531
def add(self, files, ids=None):
627
574
raise BzrError("cannot add top-level %r" % f)
629
576
fullpath = normpath(self.abspath(f))
631
579
kind = file_kind(fullpath)
632
580
except OSError, e:
633
581
if e.errno == errno.ENOENT:
634
582
raise NoSuchFile(fullpath)
583
# maybe something better?
584
raise BzrError('cannot add: not a regular file, symlink or directory: %s' % quotefn(f))
635
586
if not InventoryEntry.versionable_kind(kind):
636
raise errors.BadFileKindError(filename=f, kind=kind)
587
raise BzrError('cannot add: not a versionable file ('
588
'i.e. regular file, symlink or directory): %s' % quotefn(f))
637
590
if file_id is None:
638
inv.add_path(f, kind=kind)
640
inv.add_path(f, kind=kind, file_id=file_id)
591
file_id = gen_file_id(f)
592
inv.add_path(f, kind=kind, file_id=file_id)
594
mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
642
595
self._write_inventory(inv)
644
597
@needs_write_lock
730
685
Skips the control directory.
732
687
inv = self._inventory
733
# Convert these into local objects to save lookup times
734
pathjoin = osutils.pathjoin
735
file_kind = osutils.file_kind
737
# transport.base ends in a slash, we want the piece
738
# between the last two slashes
739
transport_base_dir = self.bzrdir.transport.base.rsplit('/', 2)[1]
741
fk_entries = {'directory':TreeDirectory, 'file':TreeFile, 'symlink':TreeLink}
743
# directory file_id, relative path, absolute path, reverse sorted children
744
children = os.listdir(self.basedir)
746
# jam 20060527 The kernel sized tree seems equivalent whether we
747
# use a deque and popleft to keep them sorted, or if we use a plain
748
# list and just reverse() them.
749
children = collections.deque(children)
750
stack = [(inv.root.file_id, u'', self.basedir, children)]
752
from_dir_id, from_dir_relpath, from_dir_abspath, children = stack[-1]
755
f = children.popleft()
689
def descend(from_dir_relpath, from_dir_id, dp):
756
693
## TODO: If we find a subdirectory with its own .bzr
757
694
## directory, then that is a separate tree and we
758
695
## should exclude it.
760
697
# the bzrdir for this tree
761
if transport_base_dir == f:
698
if self.bzrdir.transport.base.endswith(f + '/'):
764
# we know that from_dir_relpath and from_dir_abspath never end in a slash
765
# and 'f' doesn't begin with one, we can do a string op, rather
766
# than the checks of pathjoin(), all relative paths will have an extra slash
768
fp = from_dir_relpath + '/' + f
702
fp = appendpath(from_dir_relpath, f)
771
fap = from_dir_abspath + '/' + f
705
fap = appendpath(dp, f)
773
707
f_ie = inv.get_child(from_dir_id, f)
776
elif self.is_ignored(fp[1:]):
710
elif self.is_ignored(fp):
779
# we may not have found this file, because of a unicode issue
780
f_norm, can_access = osutils.normalized_filename(f)
781
if f == f_norm or not can_access:
782
# No change, so treat this file normally
785
# this file can be accessed by a normalized path
786
# check again if it is versioned
787
# these lines are repeated here for performance
789
fp = from_dir_relpath + '/' + f
790
fap = from_dir_abspath + '/' + f
791
f_ie = inv.get_child(from_dir_id, f)
794
elif self.is_ignored(fp[1:]):
799
715
fk = file_kind(fap)
807
723
# make a last minute entry
809
yield fp[1:], c, fk, f_ie.file_id, f_ie
812
yield fp[1:], c, fk, None, fk_entries[fk]()
814
yield fp[1:], c, fk, None, TreeEntry()
727
if fk == 'directory':
728
entry = TreeDirectory()
731
elif fk == 'symlink':
736
yield fp, c, fk, (f_ie and f_ie.file_id), entry
817
738
if fk != 'directory':
820
# But do this child first
821
new_children = os.listdir(fap)
823
new_children = collections.deque(new_children)
824
stack.append((f_ie.file_id, fp, fap, new_children))
825
# Break out of inner loop, so that we start outer loop with child
828
# if we finished all children, pop it off the stack
742
# don't descend unversioned directories
745
for ff in descend(fp, f_ie.file_id, fap):
748
for f in descend(u'', inv.root.file_id, self.basedir):
832
751
@needs_write_lock
833
752
def move(self, from_paths, to_name):
953
872
These are files in the working directory that are not versioned or
954
873
control files or ignored.
875
>>> from bzrlib.bzrdir import ScratchDir
876
>>> d = ScratchDir(files=['foo', 'foo~'])
877
>>> b = d.open_branch()
878
>>> tree = d.open_workingtree()
879
>>> map(str, tree.unknowns())
882
>>> list(b.unknowns())
884
>>> tree.remove('foo')
885
>>> list(b.unknowns())
956
888
for subp in self.extras():
957
889
if not self.is_ignored(subp):
1028
959
for subf in os.listdir(dirabs):
1031
if subf not in dir_entry.children:
1032
subf_norm, can_access = osutils.normalized_filename(subf)
1033
if subf_norm != subf and can_access:
1034
if subf_norm not in dir_entry.children:
1035
fl.append(subf_norm)
961
and (subf not in dir_entry.children)):
1041
subp = pathjoin(path, subf)
966
subp = appendpath(path, subf)
1044
def _translate_ignore_rule(self, rule):
1045
"""Translate a single ignore rule to a regex.
1047
There are two types of ignore rules. Those that do not contain a / are
1048
matched against the tail of the filename (that is, they do not care
1049
what directory the file is in.) Rules which do contain a slash must
1050
match the entire path. As a special case, './' at the start of the
1051
string counts as a slash in the string but is removed before matching
1052
(e.g. ./foo.c, ./src/foo.c)
1054
:return: The translated regex.
1056
if rule[:2] in ('./', '.\\'):
1058
result = fnmatch.translate(rule[2:])
1059
elif '/' in rule or '\\' in rule:
1061
result = fnmatch.translate(rule)
1063
# default rule style.
1064
result = "(?:.*/)?(?!.*/)" + fnmatch.translate(rule)
1065
assert result[-1] == '$', "fnmatch.translate did not add the expected $"
1066
return "(" + result + ")"
1068
def _combine_ignore_rules(self, rules):
1069
"""Combine a list of ignore rules into a single regex object.
1071
Each individual rule is combined with | to form a big regex, which then
1072
has $ added to it to form something like ()|()|()$. The group index for
1073
each subregex's outermost group is placed in a dictionary mapping back
1074
to the rule. This allows quick identification of the matching rule that
1076
:return: a list of the compiled regex and the matching-group index
1077
dictionaries. We return a list because python complains if you try to
1078
combine more than 100 regexes.
1083
translated_rules = []
1085
translated_rule = self._translate_ignore_rule(rule)
1086
compiled_rule = re.compile(translated_rule)
1087
groups[next_group] = rule
1088
next_group += compiled_rule.groups
1089
translated_rules.append(translated_rule)
1090
if next_group == 99:
1091
result.append((re.compile("|".join(translated_rules)), groups))
1094
translated_rules = []
1095
if len(translated_rules):
1096
result.append((re.compile("|".join(translated_rules)), groups))
1099
970
def ignored_files(self):
1100
971
"""Yield list of PATH, IGNORE_PATTERN"""
1106
978
def get_ignore_list(self):
1107
979
"""Return list of ignore patterns.
1109
981
Cached in the Tree object after the first call.
1111
ignoreset = getattr(self, '_ignoreset', None)
1112
if ignoreset is not None:
1115
ignore_globs = set(bzrlib.DEFAULT_IGNORE)
1116
ignore_globs.update(ignores.get_runtime_ignores())
1118
ignore_globs.update(ignores.get_user_ignores())
983
if hasattr(self, '_ignorelist'):
984
return self._ignorelist
986
l = bzrlib.DEFAULT_IGNORE[:]
1120
987
if self.has_filename(bzrlib.IGNORE_FILENAME):
1121
988
f = self.get_file_byname(bzrlib.IGNORE_FILENAME)
1123
ignore_globs.update(ignores.parse_ignore_file(f))
1127
self._ignoreset = ignore_globs
1128
self._ignore_regex = self._combine_ignore_rules(ignore_globs)
1131
def _get_ignore_rules_as_regex(self):
1132
"""Return a regex of the ignore rules and a mapping dict.
1134
:return: (ignore rules compiled regex, dictionary mapping rule group
1135
indices to original rule.)
1137
if getattr(self, '_ignoreset', None) is None:
1138
self.get_ignore_list()
1139
return self._ignore_regex
989
l.extend([line.rstrip("\n\r") for line in f.readlines()])
1141
994
def is_ignored(self, filename):
1142
995
r"""Check whether the filename matches an ignore pattern.
1156
1009
# treat dotfiles correctly and allows * to match /.
1157
1010
# Eventually it should be replaced with something more
1160
rules = self._get_ignore_rules_as_regex()
1161
for regex, mapping in rules:
1162
match = regex.match(filename)
1163
if match is not None:
1164
# one or more of the groups in mapping will have a non-None group
1166
groups = match.groups()
1167
rules = [mapping[group] for group in
1168
mapping if groups[group] is not None]
1013
for pat in self.get_ignore_list():
1014
if '/' in pat or '\\' in pat:
1016
# as a special case, you can put ./ at the start of a
1017
# pattern; this is good to match in the top-level
1020
if (pat[:2] == './') or (pat[:2] == '.\\'):
1024
if fnmatch.fnmatchcase(filename, newpat):
1027
if fnmatch.fnmatchcase(splitpath(filename)[-1], pat):
1172
1032
def kind(self, file_id):
1173
1033
return file_kind(self.id2abspath(file_id))
1224
1084
if new_revision is None:
1225
1085
self.branch.set_revision_history([])
1087
# current format is locked in with the branch
1088
revision_history = self.branch.revision_history()
1228
self.branch.generate_revision_history(new_revision)
1229
except errors.NoSuchRevision:
1230
# not present in the repo - dont try to set it deeper than the tip
1231
self.branch.set_revision_history([new_revision])
1090
position = revision_history.index(new_revision)
1092
raise errors.NoSuchRevision(self.branch, new_revision)
1093
self.branch.set_revision_history(revision_history[:position + 1])
1234
1096
def _cache_basis_inventory(self, new_revision):
1235
1097
"""Cache new_revision as the basis inventory."""
1236
# TODO: this should allow the ready-to-use inventory to be passed in,
1237
# as commit already has that ready-to-use [while the format is the
1240
1099
# this double handles the inventory - unpack and repack -
1241
1100
# but is easier to understand. We can/should put a conditional
1242
1101
# in here based on whether the inventory is in the latest format
1243
1102
# - perhaps we should repack all inventories on a repository
1245
# the fast path is to copy the raw xml from the repository. If the
1246
# xml contains 'revision_id="', then we assume the right
1247
# revision_id is set. We must check for this full string, because a
1248
# root node id can legitimately look like 'revision_id' but cannot
1250
xml = self.branch.repository.get_inventory_xml(new_revision)
1251
if not 'revision_id="' in xml.split('\n', 1)[0]:
1252
inv = self.branch.repository.deserialise_inventory(
1254
inv.revision_id = new_revision
1255
xml = bzrlib.xml5.serializer_v5.write_inventory_to_string(inv)
1256
assert isinstance(xml, str), 'serialised xml must be bytestring.'
1104
inv = self.branch.repository.get_inventory(new_revision)
1105
inv.revision_id = new_revision
1106
xml = bzrlib.xml5.serializer_v5.write_inventory_to_string(inv)
1257
1108
path = self._basis_inventory_name()
1259
self._control_files.put(path, sio)
1260
except (errors.NoSuchRevision, errors.RevisionNotPresent):
1109
self._control_files.put_utf8(path, xml)
1110
except WeaveRevisionNotPresent:
1263
1113
def read_basis_inventory(self):
1264
1114
"""Read the cached basis inventory."""
1265
1115
path = self._basis_inventory_name()
1266
return self._control_files.get(path).read()
1116
return self._control_files.get_utf8(path).read()
1268
1118
@needs_read_lock
1269
1119
def read_working_inventory(self):
1304
1154
# TODO: Perhaps make this just a warning, and continue?
1305
1155
# This tends to happen when
1306
1156
raise NotVersionedError(path=f)
1157
mutter("remove inventory entry %s {%s}", quotefn(f), fid)
1308
1159
# having remove it, it must be either ignored or unknown
1309
1160
if self.is_ignored(f):
1310
1161
new_status = 'I'
1312
1163
new_status = '?'
1313
show_status(new_status, inv[fid].kind, f, to_file=to_file)
1164
show_status(new_status, inv[fid].kind, quotefn(f))
1316
1167
self._write_inventory(inv)
1378
1229
between multiple working trees, i.e. via shared storage, then we
1379
1230
would probably want to lock both the local tree, and the branch.
1381
raise NotImplementedError(self.unlock)
1232
# FIXME: We want to write out the hashcache only when the last lock on
1233
# this working copy is released. Peeking at the lock count is a bit
1234
# of a nasty hack; probably it's better to have a transaction object,
1235
# which can do some finalization when it's either successfully or
1236
# unsuccessfully completed. (Denys's original patch did that.)
1237
# RBC 20060206 hookinhg into transaction will couple lock and transaction
1238
# wrongly. Hookinh into unllock on the control files object is fine though.
1240
# TODO: split this per format so there is no ugly if block
1241
if self._hashcache.needs_write and (
1242
# dedicated lock files
1243
self._control_files._lock_count==1 or
1245
(self._control_files is self.branch.control_files and
1246
self._control_files._lock_count==3)):
1247
self._hashcache.write()
1248
# reverse order of locking.
1250
return self._control_files.unlock()
1252
self.branch.unlock()
1383
1254
@needs_write_lock
1384
1255
def update(self):
1462
1330
if file_kind(self.abspath(conflicted)) != "file":
1464
except errors.NoSuchFile:
1333
if e.errno == errno.ENOENT:
1466
1337
if text is True:
1467
1338
for suffix in ('.THIS', '.OTHER'):
1469
1340
kind = file_kind(self.abspath(conflicted+suffix))
1342
if e.errno == errno.ENOENT:
1472
except errors.NoSuchFile:
1476
1350
ctype = {True: 'text conflict', False: 'contents conflict'}[text]
1477
1351
conflicts.append(Conflict.factory(ctype, path=conflicted,
1479
1353
return conflicts
1482
class WorkingTree2(WorkingTree):
1483
"""This is the Format 2 working tree.
1485
This was the first weave based working tree.
1486
- uses os locks for locking.
1487
- uses the branch last-revision.
1491
# we share control files:
1492
if self._hashcache.needs_write and self._control_files._lock_count==3:
1493
self._hashcache.write()
1494
# reverse order of locking.
1496
return self._control_files.unlock()
1498
self.branch.unlock()
1501
1356
class WorkingTree3(WorkingTree):
1502
1357
"""This is the Format 3 working tree.
1814
1657
raise NotImplementedError
1815
1658
if not isinstance(a_bzrdir.transport, LocalTransport):
1816
1659
raise errors.NotLocalUrl(a_bzrdir.transport.base)
1817
return self._open(a_bzrdir, self._open_control_files(a_bzrdir))
1819
def _open(self, a_bzrdir, control_files):
1820
"""Open the tree itself.
1822
:param a_bzrdir: the dir for the tree.
1823
:param control_files: the control files for the tree.
1825
return WorkingTree3(a_bzrdir.root_transport.local_abspath('.'),
1660
control_files = self._open_control_files(a_bzrdir)
1661
return WorkingTree3(a_bzrdir.root_transport.base,
1826
1662
_internal=True,
1828
1664
_bzrdir=a_bzrdir,
1855
1691
self._transport_readonly_server = transport_readonly_server
1856
1692
self._formats = formats
1858
def _clone_test(self, test, bzrdir_format, workingtree_format, variation):
1859
"""Clone test for adaption."""
1860
new_test = deepcopy(test)
1861
new_test.transport_server = self._transport_server
1862
new_test.transport_readonly_server = self._transport_readonly_server
1863
new_test.bzrdir_format = bzrdir_format
1864
new_test.workingtree_format = workingtree_format
1865
def make_new_test_id():
1866
new_id = "%s(%s)" % (test.id(), variation)
1867
return lambda: new_id
1868
new_test.id = make_new_test_id()
1871
1694
def adapt(self, test):
1872
1695
from bzrlib.tests import TestSuite
1873
1696
result = TestSuite()
1874
1697
for workingtree_format, bzrdir_format in self._formats:
1875
new_test = self._clone_test(
1878
workingtree_format, workingtree_format.__class__.__name__)
1698
new_test = deepcopy(test)
1699
new_test.transport_server = self._transport_server
1700
new_test.transport_readonly_server = self._transport_readonly_server
1701
new_test.bzrdir_format = bzrdir_format
1702
new_test.workingtree_format = workingtree_format
1703
def make_new_test_id():
1704
new_id = "%s(%s)" % (new_test.id(), workingtree_format.__class__.__name__)
1705
return lambda: new_id
1706
new_test.id = make_new_test_id()
1879
1707
result.addTest(new_test)