313
329
return ROOT_PARENT
314
330
return self.trans_id_tree_path(os.path.dirname(path))
332
def create_file(self, contents, trans_id, mode_id=None):
333
"""Schedule creation of a new file.
337
Contents is an iterator of strings, all of which will be written
338
to the target destination.
340
New file takes the permissions of any existing file with that id,
341
unless mode_id is specified.
343
name = self._limbo_name(trans_id)
347
unique_add(self._new_contents, trans_id, 'file')
349
# Clean up the file, it never got registered so
350
# TreeTransform.finalize() won't clean it up.
355
f.writelines(contents)
358
self._set_mode(trans_id, mode_id, S_ISREG)
360
def _set_mode(self, trans_id, mode_id, typefunc):
361
"""Set the mode of new file contents.
362
The mode_id is the existing file to get the mode from (often the same
363
as trans_id). The operation is only performed if there's a mode match
364
according to typefunc.
369
old_path = self._tree_id_paths[mode_id]
373
mode = os.stat(self._tree.abspath(old_path)).st_mode
375
if e.errno in (errno.ENOENT, errno.ENOTDIR):
376
# Either old_path doesn't exist, or the parent of the
377
# target is not a directory (but will be one eventually)
378
# Either way, we know it doesn't exist *right now*
379
# See also bug #248448
384
os.chmod(self._limbo_name(trans_id), mode)
386
def create_hardlink(self, path, trans_id):
387
"""Schedule creation of a hard link"""
388
name = self._limbo_name(trans_id)
392
if e.errno != errno.EPERM:
394
raise errors.HardLinkNotSupported(path)
396
unique_add(self._new_contents, trans_id, 'file')
398
# Clean up the file, it never got registered so
399
# TreeTransform.finalize() won't clean it up.
403
def create_directory(self, trans_id):
404
"""Schedule creation of a new directory.
406
See also new_directory.
408
os.mkdir(self._limbo_name(trans_id))
409
unique_add(self._new_contents, trans_id, 'directory')
411
def create_symlink(self, target, trans_id):
412
"""Schedule creation of a new symbolic link.
414
target is a bytestring.
415
See also new_symlink.
418
os.symlink(target, self._limbo_name(trans_id))
419
unique_add(self._new_contents, trans_id, 'symlink')
422
path = FinalPaths(self).get_path(trans_id)
425
raise UnableCreateSymlink(path=path)
427
def cancel_creation(self, trans_id):
428
"""Cancel the creation of new file contents."""
429
del self._new_contents[trans_id]
430
children = self._limbo_children.get(trans_id)
431
# if this is a limbo directory with children, move them before removing
433
if children is not None:
434
self._rename_in_limbo(children)
435
del self._limbo_children[trans_id]
436
del self._limbo_children_names[trans_id]
437
delete_any(self._limbo_name(trans_id))
316
439
def delete_contents(self, trans_id):
317
440
"""Schedule the contents of a path entry for deletion"""
318
# Ensure that the object exists in the WorkingTree, this will raise an
319
# exception if there is a problem
320
441
self.tree_kind(trans_id)
321
442
self._removed_contents.add(trans_id)
923
1119
def get_preview_tree(self):
924
1120
"""Return a tree representing the result of the transform.
926
The tree is a snapshot, and altering the TreeTransform will invalidate
1122
This tree only supports the subset of Tree functionality required
1123
by show_diff_trees. It must only be compared to tt._tree.
929
1125
return _PreviewTree(self)
931
def commit(self, branch, message, merge_parents=None, strict=False,
932
timestamp=None, timezone=None, committer=None, authors=None,
933
revprops=None, revision_id=None):
934
"""Commit the result of this TreeTransform to a branch.
936
:param branch: The branch to commit to.
937
:param message: The message to attach to the commit.
938
:param merge_parents: Additional parent revision-ids specified by
940
:param strict: If True, abort the commit if there are unversioned
942
:param timestamp: if not None, seconds-since-epoch for the time and
943
date. (May be a float.)
944
:param timezone: Optional timezone for timestamp, as an offset in
946
:param committer: Optional committer in email-id format.
947
(e.g. "J Random Hacker <jrandom@example.com>")
948
:param authors: Optional list of authors in email-id format.
949
:param revprops: Optional dictionary of revision properties.
950
:param revision_id: Optional revision id. (Specifying a revision-id
951
may reduce performance for some non-native formats.)
952
:return: The revision_id of the revision committed.
954
self._check_malformed()
956
unversioned = set(self._new_contents).difference(set(self._new_id))
957
for trans_id in unversioned:
958
if self.final_file_id(trans_id) is None:
959
raise errors.StrictCommitFailed()
961
revno, last_rev_id = branch.last_revision_info()
962
if last_rev_id == _mod_revision.NULL_REVISION:
963
if merge_parents is not None:
964
raise ValueError('Cannot supply merge parents for first'
968
parent_ids = [last_rev_id]
969
if merge_parents is not None:
970
parent_ids.extend(merge_parents)
971
if self._tree.get_revision_id() != last_rev_id:
972
raise ValueError('TreeTransform not based on branch basis: %s' %
973
self._tree.get_revision_id())
974
revprops = commit.Commit.update_revprops(revprops, branch, authors)
975
builder = branch.get_commit_builder(parent_ids,
980
revision_id=revision_id)
981
preview = self.get_preview_tree()
982
list(builder.record_iter_changes(preview, last_rev_id,
983
self.iter_changes()))
984
builder.finish_inventory()
985
revision_id = builder.commit(message)
986
branch.set_last_revision_info(revno + 1, revision_id)
989
def _text_parent(self, trans_id):
990
file_id = self.tree_file_id(trans_id)
992
if file_id is None or self._tree.kind(file_id) != 'file':
994
except errors.NoSuchFile:
998
def _get_parents_texts(self, trans_id):
999
"""Get texts for compression parents of this file."""
1000
file_id = self._text_parent(trans_id)
1003
return (self._tree.get_file_text(file_id),)
1005
def _get_parents_lines(self, trans_id):
1006
"""Get lines for compression parents of this file."""
1007
file_id = self._text_parent(trans_id)
1010
return (self._tree.get_file_lines(file_id),)
1012
def serialize(self, serializer):
1013
"""Serialize this TreeTransform.
1015
:param serializer: A Serialiser like pack.ContainerSerializer.
1017
new_name = dict((k, v.encode('utf-8')) for k, v in
1018
self._new_name.items())
1019
new_executability = dict((k, int(v)) for k, v in
1020
self._new_executability.items())
1021
tree_path_ids = dict((k.encode('utf-8'), v)
1022
for k, v in self._tree_path_ids.items())
1024
'_id_number': self._id_number,
1025
'_new_name': new_name,
1026
'_new_parent': self._new_parent,
1027
'_new_executability': new_executability,
1028
'_new_id': self._new_id,
1029
'_tree_path_ids': tree_path_ids,
1030
'_removed_id': list(self._removed_id),
1031
'_removed_contents': list(self._removed_contents),
1032
'_non_present_ids': self._non_present_ids,
1034
yield serializer.bytes_record(bencode.bencode(attribs),
1036
for trans_id, kind in self._new_contents.items():
1038
lines = osutils.chunks_to_lines(
1039
self._read_file_chunks(trans_id))
1040
parents = self._get_parents_lines(trans_id)
1041
mpdiff = multiparent.MultiParent.from_lines(lines, parents)
1042
content = ''.join(mpdiff.to_patch())
1043
if kind == 'directory':
1045
if kind == 'symlink':
1046
content = self._read_symlink_target(trans_id)
1047
yield serializer.bytes_record(content, ((trans_id, kind),))
1049
def deserialize(self, records):
1050
"""Deserialize a stored TreeTransform.
1052
:param records: An iterable of (names, content) tuples, as per
1053
pack.ContainerPushParser.
1055
names, content = records.next()
1056
attribs = bencode.bdecode(content)
1057
self._id_number = attribs['_id_number']
1058
self._new_name = dict((k, v.decode('utf-8'))
1059
for k, v in attribs['_new_name'].items())
1060
self._new_parent = attribs['_new_parent']
1061
self._new_executability = dict((k, bool(v)) for k, v in
1062
attribs['_new_executability'].items())
1063
self._new_id = attribs['_new_id']
1064
self._r_new_id = dict((v, k) for k, v in self._new_id.items())
1065
self._tree_path_ids = {}
1066
self._tree_id_paths = {}
1067
for bytepath, trans_id in attribs['_tree_path_ids'].items():
1068
path = bytepath.decode('utf-8')
1069
self._tree_path_ids[path] = trans_id
1070
self._tree_id_paths[trans_id] = path
1071
self._removed_id = set(attribs['_removed_id'])
1072
self._removed_contents = set(attribs['_removed_contents'])
1073
self._non_present_ids = attribs['_non_present_ids']
1074
for ((trans_id, kind),), content in records:
1076
mpdiff = multiparent.MultiParent.from_patch(content)
1077
lines = mpdiff.to_lines(self._get_parents_texts(trans_id))
1078
self.create_file(lines, trans_id)
1079
if kind == 'directory':
1080
self.create_directory(trans_id)
1081
if kind == 'symlink':
1082
self.create_symlink(content.decode('utf-8'), trans_id)
1085
class DiskTreeTransform(TreeTransformBase):
1086
"""Tree transform storing its contents on disk."""
1088
def __init__(self, tree, limbodir, pb=None,
1089
case_sensitive=True):
1091
:param tree: The tree that will be transformed, but not necessarily
1093
:param limbodir: A directory where new files can be stored until
1094
they are installed in their proper places
1096
:param case_sensitive: If True, the target of the transform is
1097
case sensitive, not just case preserving.
1099
TreeTransformBase.__init__(self, tree, pb, case_sensitive)
1100
self._limbodir = limbodir
1101
self._deletiondir = None
1102
# A mapping of transform ids to their limbo filename
1103
self._limbo_files = {}
1104
# A mapping of transform ids to a set of the transform ids of children
1105
# that their limbo directory has
1106
self._limbo_children = {}
1107
# Map transform ids to maps of child filename to child transform id
1108
self._limbo_children_names = {}
1109
# List of transform ids that need to be renamed from limbo into place
1110
self._needs_rename = set()
1111
self._creation_mtime = None
1114
"""Release the working tree lock, if held, clean up limbo dir.
1116
This is required if apply has not been invoked, but can be invoked
1119
if self._tree is None:
1122
entries = [(self._limbo_name(t), t, k) for t, k in
1123
self._new_contents.iteritems()]
1124
entries.sort(reverse=True)
1125
for path, trans_id, kind in entries:
1128
delete_any(self._limbodir)
1130
# We don't especially care *why* the dir is immortal.
1131
raise ImmortalLimbo(self._limbodir)
1133
if self._deletiondir is not None:
1134
delete_any(self._deletiondir)
1136
raise errors.ImmortalPendingDeletion(self._deletiondir)
1138
TreeTransformBase.finalize(self)
1140
def _limbo_name(self, trans_id):
1141
"""Generate the limbo name of a file"""
1142
limbo_name = self._limbo_files.get(trans_id)
1143
if limbo_name is None:
1144
limbo_name = self._generate_limbo_path(trans_id)
1145
self._limbo_files[trans_id] = limbo_name
1148
def _generate_limbo_path(self, trans_id):
1149
"""Generate a limbo path using the trans_id as the relative path.
1151
This is suitable as a fallback, and when the transform should not be
1152
sensitive to the path encoding of the limbo directory.
1154
self._needs_rename.add(trans_id)
1155
return pathjoin(self._limbodir, trans_id)
1157
def adjust_path(self, name, parent, trans_id):
1158
previous_parent = self._new_parent.get(trans_id)
1159
previous_name = self._new_name.get(trans_id)
1160
TreeTransformBase.adjust_path(self, name, parent, trans_id)
1161
if (trans_id in self._limbo_files and
1162
trans_id not in self._needs_rename):
1163
self._rename_in_limbo([trans_id])
1164
if previous_parent != parent:
1165
self._limbo_children[previous_parent].remove(trans_id)
1166
if previous_parent != parent or previous_name != name:
1167
del self._limbo_children_names[previous_parent][previous_name]
1169
def _rename_in_limbo(self, trans_ids):
1170
"""Fix limbo names so that the right final path is produced.
1172
This means we outsmarted ourselves-- we tried to avoid renaming
1173
these files later by creating them with their final names in their
1174
final parents. But now the previous name or parent is no longer
1175
suitable, so we have to rename them.
1177
Even for trans_ids that have no new contents, we must remove their
1178
entries from _limbo_files, because they are now stale.
1180
for trans_id in trans_ids:
1181
old_path = self._limbo_files.pop(trans_id)
1182
if trans_id not in self._new_contents:
1184
new_path = self._limbo_name(trans_id)
1185
osutils.rename(old_path, new_path)
1186
for descendant in self._limbo_descendants(trans_id):
1187
desc_path = self._limbo_files[descendant]
1188
desc_path = new_path + desc_path[len(old_path):]
1189
self._limbo_files[descendant] = desc_path
1191
def _limbo_descendants(self, trans_id):
1192
"""Return the set of trans_ids whose limbo paths descend from this."""
1193
descendants = set(self._limbo_children.get(trans_id, []))
1194
for descendant in list(descendants):
1195
descendants.update(self._limbo_descendants(descendant))
1198
def create_file(self, contents, trans_id, mode_id=None):
1199
"""Schedule creation of a new file.
1203
Contents is an iterator of strings, all of which will be written
1204
to the target destination.
1206
New file takes the permissions of any existing file with that id,
1207
unless mode_id is specified.
1209
name = self._limbo_name(trans_id)
1210
f = open(name, 'wb')
1213
unique_add(self._new_contents, trans_id, 'file')
1215
# Clean up the file, it never got registered so
1216
# TreeTransform.finalize() won't clean it up.
1221
f.writelines(contents)
1224
self._set_mtime(name)
1225
self._set_mode(trans_id, mode_id, S_ISREG)
1227
def _read_file_chunks(self, trans_id):
1228
cur_file = open(self._limbo_name(trans_id), 'rb')
1230
return cur_file.readlines()
1234
def _read_symlink_target(self, trans_id):
1235
return os.readlink(self._limbo_name(trans_id))
1237
def _set_mtime(self, path):
1238
"""All files that are created get the same mtime.
1240
This time is set by the first object to be created.
1242
if self._creation_mtime is None:
1243
self._creation_mtime = time.time()
1244
os.utime(path, (self._creation_mtime, self._creation_mtime))
1246
def create_hardlink(self, path, trans_id):
1247
"""Schedule creation of a hard link"""
1248
name = self._limbo_name(trans_id)
1252
if e.errno != errno.EPERM:
1254
raise errors.HardLinkNotSupported(path)
1256
unique_add(self._new_contents, trans_id, 'file')
1258
# Clean up the file, it never got registered so
1259
# TreeTransform.finalize() won't clean it up.
1263
def create_directory(self, trans_id):
1264
"""Schedule creation of a new directory.
1266
See also new_directory.
1268
os.mkdir(self._limbo_name(trans_id))
1269
unique_add(self._new_contents, trans_id, 'directory')
1271
def create_symlink(self, target, trans_id):
1272
"""Schedule creation of a new symbolic link.
1274
target is a bytestring.
1275
See also new_symlink.
1278
os.symlink(target, self._limbo_name(trans_id))
1279
unique_add(self._new_contents, trans_id, 'symlink')
1282
path = FinalPaths(self).get_path(trans_id)
1285
raise UnableCreateSymlink(path=path)
1287
def cancel_creation(self, trans_id):
1288
"""Cancel the creation of new file contents."""
1289
del self._new_contents[trans_id]
1290
children = self._limbo_children.get(trans_id)
1291
# if this is a limbo directory with children, move them before removing
1293
if children is not None:
1294
self._rename_in_limbo(children)
1295
del self._limbo_children[trans_id]
1296
del self._limbo_children_names[trans_id]
1297
delete_any(self._limbo_name(trans_id))
1300
class TreeTransform(DiskTreeTransform):
1128
class TreeTransform(TreeTransformBase):
1301
1129
"""Represent a tree transformation.
1303
1131
This object is designed to support incremental generation of the transform,
1392
# Cache of realpath results, to speed up canonical_path
1393
self._realpaths = {}
1394
# Cache of relpath results, to speed up canonical_path
1396
DiskTreeTransform.__init__(self, tree, limbodir, pb,
1220
TreeTransformBase.__init__(self, tree, limbodir, pb,
1397
1221
tree.case_sensitive)
1398
1222
self._deletiondir = deletiondir
1400
def canonical_path(self, path):
1401
"""Get the canonical tree-relative path"""
1402
# don't follow final symlinks
1403
abs = self._tree.abspath(path)
1404
if abs in self._relpaths:
1405
return self._relpaths[abs]
1406
dirname, basename = os.path.split(abs)
1407
if dirname not in self._realpaths:
1408
self._realpaths[dirname] = os.path.realpath(dirname)
1409
dirname = self._realpaths[dirname]
1410
abs = pathjoin(dirname, basename)
1411
if dirname in self._relpaths:
1412
relpath = pathjoin(self._relpaths[dirname], basename)
1413
relpath = relpath.rstrip('/\\')
1415
relpath = self._tree.relpath(abs)
1416
self._relpaths[abs] = relpath
1419
def tree_kind(self, trans_id):
1420
"""Determine the file kind in the working tree.
1422
Raises NoSuchFile if the file does not exist
1424
path = self._tree_id_paths.get(trans_id)
1426
raise NoSuchFile(None)
1428
return file_kind(self._tree.abspath(path))
1430
if e.errno != errno.ENOENT:
1433
raise NoSuchFile(path)
1435
def _set_mode(self, trans_id, mode_id, typefunc):
1436
"""Set the mode of new file contents.
1437
The mode_id is the existing file to get the mode from (often the same
1438
as trans_id). The operation is only performed if there's a mode match
1439
according to typefunc.
1444
old_path = self._tree_id_paths[mode_id]
1448
mode = os.stat(self._tree.abspath(old_path)).st_mode
1450
if e.errno in (errno.ENOENT, errno.ENOTDIR):
1451
# Either old_path doesn't exist, or the parent of the
1452
# target is not a directory (but will be one eventually)
1453
# Either way, we know it doesn't exist *right now*
1454
# See also bug #248448
1459
os.chmod(self._limbo_name(trans_id), mode)
1461
def iter_tree_children(self, parent_id):
1462
"""Iterate through the entry's tree children, if any"""
1464
path = self._tree_id_paths[parent_id]
1468
children = os.listdir(self._tree.abspath(path))
1470
if not (osutils._is_error_enotdir(e)
1471
or e.errno in (errno.ENOENT, errno.ESRCH)):
1475
for child in children:
1476
childpath = joinpath(path, child)
1477
if self._tree.is_control_filename(childpath):
1479
yield self.trans_id_tree_path(childpath)
1481
def _generate_limbo_path(self, trans_id):
1482
"""Generate a limbo path using the final path if possible.
1484
This optimizes the performance of applying the tree transform by
1485
avoiding renames. These renames can be avoided only when the parent
1486
directory is already scheduled for creation.
1488
If the final path cannot be used, falls back to using the trans_id as
1491
parent = self._new_parent.get(trans_id)
1492
# if the parent directory is already in limbo (e.g. when building a
1493
# tree), choose a limbo name inside the parent, to reduce further
1495
use_direct_path = False
1496
if self._new_contents.get(parent) == 'directory':
1497
filename = self._new_name.get(trans_id)
1498
if filename is not None:
1499
if parent not in self._limbo_children:
1500
self._limbo_children[parent] = set()
1501
self._limbo_children_names[parent] = {}
1502
use_direct_path = True
1503
# the direct path can only be used if no other file has
1504
# already taken this pathname, i.e. if the name is unused, or
1505
# if it is already associated with this trans_id.
1506
elif self._case_sensitive_target:
1507
if (self._limbo_children_names[parent].get(filename)
1508
in (trans_id, None)):
1509
use_direct_path = True
1511
for l_filename, l_trans_id in\
1512
self._limbo_children_names[parent].iteritems():
1513
if l_trans_id == trans_id:
1515
if l_filename.lower() == filename.lower():
1518
use_direct_path = True
1520
if not use_direct_path:
1521
return DiskTreeTransform._generate_limbo_path(self, trans_id)
1523
limbo_name = pathjoin(self._limbo_files[parent], filename)
1524
self._limbo_children[parent].add(trans_id)
1525
self._limbo_children_names[parent][filename] = trans_id
1529
1224
def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
1530
1225
"""Apply all changes to the inventory and filesystem.