/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/transform.py

  • Committer: Breezy landing bot
  • Author(s): Jelmer Vernooij
  • Date: 2020-08-23 01:15:41 UTC
  • mfrom: (7520.1.4 merge-3.1)
  • Revision ID: breezy.the.bot@gmail.com-20200823011541-nv0oh7nzaganx2qy
Merge lp:brz/3.1.

Merged from https://code.launchpad.net/~jelmer/brz/merge-3.1/+merge/389690

Show diffs side-by-side

added added

removed removed

Lines of Context:
32
32
    )
33
33
lazy_import.lazy_import(globals(), """
34
34
from breezy import (
35
 
    conflicts,
36
35
    multiparent,
37
36
    revision as _mod_revision,
38
37
    ui,
56
55
from .progress import ProgressPhase
57
56
from .tree import (
58
57
    InterTree,
59
 
    TreeChange,
60
58
    find_previous_path,
61
59
    )
62
60
 
67
65
class NoFinalPath(BzrError):
68
66
 
69
67
    _fmt = ("No final name for trans_id %(trans_id)r\n"
70
 
            "file-id: %(file_id)r\n"
71
68
            "root trans-id: %(root_trans_id)r\n")
72
69
 
73
70
    def __init__(self, trans_id, transform):
74
71
        self.trans_id = trans_id
75
 
        self.file_id = transform.final_file_id(trans_id)
76
72
        self.root_trans_id = transform.root
77
73
 
78
74
 
237
233
 
238
234
    def create_path(self, name, parent):
239
235
        """Assign a transaction id to a new path"""
240
 
        trans_id = self._assign_id()
 
236
        trans_id = self.assign_id()
241
237
        unique_add(self._new_name, trans_id, name)
242
238
        unique_add(self._new_parent, trans_id, parent)
243
239
        return trans_id
246
242
        """Change the path that is assigned to a transaction id."""
247
243
        if parent is None:
248
244
            raise ValueError("Parent trans-id may not be None")
249
 
        if trans_id == self._new_root:
 
245
        if trans_id == self.root:
250
246
            raise CantMoveRoot
251
247
        self._new_name[trans_id] = name
252
248
        self._new_parent[trans_id] = parent
275
271
        """
276
272
        raise NotImplementedError(self.fixup_new_roots)
277
273
 
278
 
    def _assign_id(self):
 
274
    def assign_id(self):
279
275
        """Produce a new tranform id"""
280
276
        new_id = "new-%s" % self._id_number
281
277
        self._id_number += 1
285
281
        """Determine (and maybe set) the transaction ID for a tree path."""
286
282
        path = self.canonical_path(path)
287
283
        if path not in self._tree_path_ids:
288
 
            self._tree_path_ids[path] = self._assign_id()
 
284
            self._tree_path_ids[path] = self.assign_id()
289
285
            self._tree_id_paths[self._tree_path_ids[path]] = path
290
286
        return self._tree_path_ids[path]
291
287
 
352
348
            contents insertion command)
353
349
        """
354
350
        if trans_id in self._new_contents:
 
351
            if trans_id in self._new_reference_revision:
 
352
                return 'tree-reference'
355
353
            return self._new_contents[trans_id]
356
354
        elif trans_id in self._removed_contents:
357
355
            return None
392
390
    def new_contents(self, trans_id):
393
391
        return (trans_id in self._new_contents)
394
392
 
395
 
    def find_conflicts(self):
 
393
    def find_raw_conflicts(self):
396
394
        """Find any violations of inventory or filesystem invariants"""
397
 
        raise NotImplementedError(self.find_conflicts)
 
395
        raise NotImplementedError(self.find_raw_conflicts)
398
396
 
399
397
    def new_file(self, name, parent_id, contents, file_id=None,
400
398
                 executable=None, sha1=None):
444
442
        """Produce output in the same format as Tree.iter_changes.
445
443
 
446
444
        Will produce nonsensical results if invoked while inventory/filesystem
447
 
        conflicts (as reported by TreeTransform.find_conflicts()) are present.
 
445
        conflicts (as reported by TreeTransform.find_raw_conflicts()) are present.
448
446
 
449
447
        This reads the Transform, but only reproduces changes involving a
450
448
        file_id.  Files that are not versioned in either of the FROM or TO
524
522
        """Cancel the creation of new file contents."""
525
523
        raise NotImplementedError(self.cancel_creation)
526
524
 
 
525
    def cook_conflicts(self, raw_conflicts):
 
526
        """Cook conflicts.
 
527
        """
 
528
        raise NotImplementedError(self.cook_conflicts)
 
529
 
527
530
 
528
531
class OrphaningError(errors.BzrError):
529
532
 
617
620
        self.transform = transform
618
621
 
619
622
    def _determine_path(self, trans_id):
620
 
        if (trans_id == self.transform.root or trans_id == ROOT_PARENT):
 
623
        if trans_id == self.transform.root or trans_id == ROOT_PARENT:
621
624
            return u""
622
625
        name = self.transform.final_name(trans_id)
623
626
        parent_id = self.transform.final_parent(trans_id)
768
771
        raw_conflicts = resolve_conflicts(tt, pass_func=resolver)
769
772
        if len(raw_conflicts) > 0:
770
773
            precomputed_delta = None
771
 
        conflicts = cook_conflicts(raw_conflicts, tt)
 
774
        conflicts = tt.cook_conflicts(raw_conflicts)
772
775
        for conflict in conflicts:
773
776
            trace.warning(str(conflict))
774
777
        try:
987
990
    with ui.ui_factory.nested_progress_bar() as child_pb:
988
991
        raw_conflicts = resolve_conflicts(
989
992
            tt, child_pb, lambda t, c: conflict_pass(t, c, target_tree))
990
 
    conflicts = cook_conflicts(raw_conflicts, tt)
 
993
    conflicts = tt.cook_conflicts(raw_conflicts)
991
994
    return conflicts, merge_modified
992
995
 
993
996
 
1008
1011
    try:
1009
1012
        deferred_files = []
1010
1013
        for id_num, change in enumerate(change_list):
1011
 
            file_id = change.file_id
1012
1014
            target_path, wt_path = change.path
1013
1015
            target_versioned, wt_versioned = change.versioned
1014
 
            target_parent, wt_parent = change.parent_id
 
1016
            target_parent = change.parent_id[0]
1015
1017
            target_name, wt_name = change.name
1016
1018
            target_kind, wt_kind = change.kind
1017
1019
            target_executable, wt_executable = change.executable
1018
 
            if skip_root and wt_parent is None:
 
1020
            if skip_root and wt_path == '':
1019
1021
                continue
1020
 
            trans_id = tt.trans_id_file_id(file_id)
 
1022
            trans_id = tt.trans_id_file_id(change.file_id)
1021
1023
            mode_id = None
1022
1024
            if change.changed_content:
1023
1025
                keep_content = False
1042
1044
                    if not keep_content:
1043
1045
                        tt.delete_contents(trans_id)
1044
1046
                    elif target_kind is not None:
1045
 
                        parent_trans_id = tt.trans_id_file_id(wt_parent)
 
1047
                        parent_trans_id = tt.trans_id_tree_path(osutils.dirname(wt_path))
1046
1048
                        backup_name = tt._available_backup_name(
1047
1049
                            wt_name, parent_trans_id)
1048
1050
                        tt.adjust_path(backup_name, parent_trans_id, trans_id)
1049
1051
                        new_trans_id = tt.create_path(wt_name, parent_trans_id)
1050
1052
                        if wt_versioned and target_versioned:
1051
1053
                            tt.unversion_file(trans_id)
1052
 
                            tt.version_file(new_trans_id, file_id=file_id)
 
1054
                            tt.version_file(
 
1055
                                new_trans_id, file_id=getattr(change, 'file_id', None))
1053
1056
                        # New contents should have the same unix perms as old
1054
1057
                        # contents
1055
1058
                        mode_id = trans_id
1065
1068
                        target_path), trans_id)
1066
1069
                elif target_kind == 'file':
1067
1070
                    deferred_files.append(
1068
 
                        (target_path, (trans_id, mode_id, file_id)))
 
1071
                        (target_path, (trans_id, mode_id, target_path)))
1069
1072
                    if basis_tree is None:
1070
1073
                        basis_tree = working_tree.basis_tree()
1071
1074
                        basis_tree.lock_read()
1087
1090
                elif target_kind is not None:
1088
1091
                    raise AssertionError(target_kind)
1089
1092
            if not wt_versioned and target_versioned:
1090
 
                tt.version_file(trans_id, file_id=file_id)
 
1093
                tt.version_file(
 
1094
                    trans_id, file_id=getattr(change, 'file_id', None))
1091
1095
            if wt_versioned and not target_versioned:
1092
1096
                tt.unversion_file(trans_id)
1093
1097
            if (target_name is not None
1094
 
                    and (wt_name != target_name or wt_parent != target_parent)):
1095
 
                if target_name == '' and target_parent is None:
 
1098
                    and (wt_name != target_name or change.is_reparented())):
 
1099
                if target_path == '':
1096
1100
                    parent_trans = ROOT_PARENT
1097
1101
                else:
1098
1102
                    parent_trans = tt.trans_id_file_id(target_parent)
1099
 
                if wt_parent is None and wt_versioned:
 
1103
                if wt_path == '' and wt_versioned:
1100
1104
                    tt.adjust_root_path(target_name, parent_trans)
1101
1105
                else:
1102
1106
                    tt.adjust_path(target_name, parent_trans, trans_id)
1103
1107
            if wt_executable != target_executable and target_kind == "file":
1104
1108
                tt.set_executability(target_executable, trans_id)
1105
1109
        if working_tree.supports_content_filtering():
1106
 
            for (trans_id, mode_id, file_id), bytes in (
 
1110
            for (trans_id, mode_id, target_path), bytes in (
1107
1111
                    target_tree.iter_files_bytes(deferred_files)):
1108
1112
                # We're reverting a tree to the target tree so using the
1109
1113
                # target tree to find the file path seems the best choice
1110
1114
                # here IMO - Ian C 27/Oct/2009
1111
 
                filter_tree_path = target_tree.id2path(file_id)
1112
 
                filters = working_tree._content_filter_stack(filter_tree_path)
 
1115
                filters = working_tree._content_filter_stack(target_path)
1113
1116
                bytes = filtered_output_bytes(
1114
1117
                    bytes, filters,
1115
 
                    ContentFilterContext(filter_tree_path, working_tree))
 
1118
                    ContentFilterContext(target_path, working_tree))
1116
1119
                tt.create_file(bytes, trans_id, mode_id)
1117
1120
        else:
1118
 
            for (trans_id, mode_id, file_id), bytes in target_tree.iter_files_bytes(
 
1121
            for (trans_id, mode_id, target_path), bytes in target_tree.iter_files_bytes(
1119
1122
                    deferred_files):
1120
1123
                tt.create_file(bytes, trans_id, mode_id)
1121
1124
        tt.fixup_new_roots()
1133
1136
    with ui.ui_factory.nested_progress_bar() as pb:
1134
1137
        for n in range(10):
1135
1138
            pb.update(gettext('Resolution pass'), n + 1, 10)
1136
 
            conflicts = tt.find_conflicts()
 
1139
            conflicts = tt.find_raw_conflicts()
1137
1140
            if len(conflicts) == 0:
1138
1141
                return new_conflicts
1139
1142
            new_conflicts.update(pass_func(tt, conflicts))
1140
1143
        raise MalformedTransform(conflicts=conflicts)
1141
1144
 
1142
1145
 
 
1146
def resolve_duplicate_id(tt, path_tree, c_type, old_trans_id, trans_id):
 
1147
    tt.unversion_file(old_trans_id)
 
1148
    yield (c_type, 'Unversioned existing file', old_trans_id, trans_id)
 
1149
 
 
1150
 
 
1151
def resolve_duplicate(tt, path_tree, c_type, last_trans_id, trans_id, name):
 
1152
    # files that were renamed take precedence
 
1153
    final_parent = tt.final_parent(last_trans_id)
 
1154
    if tt.path_changed(last_trans_id):
 
1155
        existing_file, new_file = trans_id, last_trans_id
 
1156
    else:
 
1157
        existing_file, new_file = last_trans_id, trans_id
 
1158
    new_name = tt.final_name(existing_file) + '.moved'
 
1159
    tt.adjust_path(new_name, final_parent, existing_file)
 
1160
    yield (c_type, 'Moved existing file to', existing_file, new_file)
 
1161
 
 
1162
 
 
1163
def resolve_parent_loop(tt, path_tree, c_type, cur):
 
1164
    # break the loop by undoing one of the ops that caused the loop
 
1165
    while not tt.path_changed(cur):
 
1166
        cur = tt.final_parent(cur)
 
1167
    yield (c_type, 'Cancelled move', cur, tt.final_parent(cur),)
 
1168
    tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
 
1169
 
 
1170
 
 
1171
def resolve_missing_parent(tt, path_tree, c_type, trans_id):
 
1172
    if trans_id in tt._removed_contents:
 
1173
        cancel_deletion = True
 
1174
        orphans = tt._get_potential_orphans(trans_id)
 
1175
        if orphans:
 
1176
            cancel_deletion = False
 
1177
            # All children are orphans
 
1178
            for o in orphans:
 
1179
                try:
 
1180
                    tt.new_orphan(o, trans_id)
 
1181
                except OrphaningError:
 
1182
                    # Something bad happened so we cancel the directory
 
1183
                    # deletion which will leave it in place with a
 
1184
                    # conflict. The user can deal with it from there.
 
1185
                    # Note that this also catch the case where we don't
 
1186
                    # want to create orphans and leave the directory in
 
1187
                    # place.
 
1188
                    cancel_deletion = True
 
1189
                    break
 
1190
        if cancel_deletion:
 
1191
            # Cancel the directory deletion
 
1192
            tt.cancel_deletion(trans_id)
 
1193
            yield ('deleting parent', 'Not deleting', trans_id)
 
1194
    else:
 
1195
        create = True
 
1196
        try:
 
1197
            tt.final_name(trans_id)
 
1198
        except NoFinalPath:
 
1199
            if path_tree is not None:
 
1200
                file_id = tt.final_file_id(trans_id)
 
1201
                if file_id is None:
 
1202
                    file_id = tt.inactive_file_id(trans_id)
 
1203
                _, entry = next(path_tree.iter_entries_by_dir(
 
1204
                    specific_files=[path_tree.id2path(file_id)]))
 
1205
                # special-case the other tree root (move its
 
1206
                # children to current root)
 
1207
                if entry.parent_id is None:
 
1208
                    create = False
 
1209
                    moved = _reparent_transform_children(
 
1210
                        tt, trans_id, tt.root)
 
1211
                    for child in moved:
 
1212
                        yield (c_type, 'Moved to root', child)
 
1213
                else:
 
1214
                    parent_trans_id = tt.trans_id_file_id(
 
1215
                        entry.parent_id)
 
1216
                    tt.adjust_path(entry.name, parent_trans_id,
 
1217
                                   trans_id)
 
1218
        if create:
 
1219
            tt.create_directory(trans_id)
 
1220
            yield (c_type, 'Created directory', trans_id)
 
1221
 
 
1222
 
 
1223
def resolve_unversioned_parent(tt, path_tree, c_type, trans_id):
 
1224
    file_id = tt.inactive_file_id(trans_id)
 
1225
    # special-case the other tree root (move its children instead)
 
1226
    if path_tree and path_tree.path2id('') == file_id:
 
1227
        # This is the root entry, skip it
 
1228
        return
 
1229
    tt.version_file(trans_id, file_id=file_id)
 
1230
    yield (c_type, 'Versioned directory', trans_id)
 
1231
 
 
1232
 
 
1233
def resolve_non_directory_parent(tt, path_tree, c_type, parent_id):
 
1234
    parent_parent = tt.final_parent(parent_id)
 
1235
    parent_name = tt.final_name(parent_id)
 
1236
    # TODO(jelmer): Make this code transform-specific
 
1237
    if tt._tree.supports_setting_file_ids():
 
1238
        parent_file_id = tt.final_file_id(parent_id)
 
1239
    else:
 
1240
        parent_file_id = b'DUMMY'
 
1241
    new_parent_id = tt.new_directory(parent_name + '.new',
 
1242
                                     parent_parent, parent_file_id)
 
1243
    _reparent_transform_children(tt, parent_id, new_parent_id)
 
1244
    if parent_file_id is not None:
 
1245
        tt.unversion_file(parent_id)
 
1246
    yield (c_type, 'Created directory', new_parent_id)
 
1247
 
 
1248
 
 
1249
def resolve_versioning_no_contents(tt, path_tree, c_type, trans_id):
 
1250
    tt.cancel_versioning(trans_id)
 
1251
    return []
 
1252
 
 
1253
 
 
1254
CONFLICT_RESOLVERS = {
 
1255
    'duplicate id': resolve_duplicate_id,
 
1256
    'duplicate': resolve_duplicate,
 
1257
    'parent loop': resolve_parent_loop,
 
1258
    'missing parent': resolve_missing_parent,
 
1259
    'unversioned parent': resolve_unversioned_parent,
 
1260
    'non-directory parent': resolve_non_directory_parent,
 
1261
    'versioning no contents': resolve_versioning_no_contents,
 
1262
}
 
1263
 
 
1264
 
1143
1265
def conflict_pass(tt, conflicts, path_tree=None):
1144
1266
    """Resolve some classes of conflicts.
1145
1267
 
1148
1270
    :param path_tree: A Tree to get supplemental paths from
1149
1271
    """
1150
1272
    new_conflicts = set()
1151
 
    for c_type, conflict in ((c[0], c) for c in conflicts):
1152
 
        if c_type == 'duplicate id':
1153
 
            tt.unversion_file(conflict[1])
1154
 
            new_conflicts.add((c_type, 'Unversioned existing file',
1155
 
                               conflict[1], conflict[2], ))
1156
 
        elif c_type == 'duplicate':
1157
 
            # files that were renamed take precedence
1158
 
            final_parent = tt.final_parent(conflict[1])
1159
 
            if tt.path_changed(conflict[1]):
1160
 
                existing_file, new_file = conflict[2], conflict[1]
1161
 
            else:
1162
 
                existing_file, new_file = conflict[1], conflict[2]
1163
 
            new_name = tt.final_name(existing_file) + '.moved'
1164
 
            tt.adjust_path(new_name, final_parent, existing_file)
1165
 
            new_conflicts.add((c_type, 'Moved existing file to',
1166
 
                               existing_file, new_file))
1167
 
        elif c_type == 'parent loop':
1168
 
            # break the loop by undoing one of the ops that caused the loop
1169
 
            cur = conflict[1]
1170
 
            while not tt.path_changed(cur):
1171
 
                cur = tt.final_parent(cur)
1172
 
            new_conflicts.add((c_type, 'Cancelled move', cur,
1173
 
                               tt.final_parent(cur),))
1174
 
            tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
1175
 
 
1176
 
        elif c_type == 'missing parent':
1177
 
            trans_id = conflict[1]
1178
 
            if trans_id in tt._removed_contents:
1179
 
                cancel_deletion = True
1180
 
                orphans = tt._get_potential_orphans(trans_id)
1181
 
                if orphans:
1182
 
                    cancel_deletion = False
1183
 
                    # All children are orphans
1184
 
                    for o in orphans:
1185
 
                        try:
1186
 
                            tt.new_orphan(o, trans_id)
1187
 
                        except OrphaningError:
1188
 
                            # Something bad happened so we cancel the directory
1189
 
                            # deletion which will leave it in place with a
1190
 
                            # conflict. The user can deal with it from there.
1191
 
                            # Note that this also catch the case where we don't
1192
 
                            # want to create orphans and leave the directory in
1193
 
                            # place.
1194
 
                            cancel_deletion = True
1195
 
                            break
1196
 
                if cancel_deletion:
1197
 
                    # Cancel the directory deletion
1198
 
                    tt.cancel_deletion(trans_id)
1199
 
                    new_conflicts.add(('deleting parent', 'Not deleting',
1200
 
                                       trans_id))
1201
 
            else:
1202
 
                create = True
1203
 
                try:
1204
 
                    tt.final_name(trans_id)
1205
 
                except NoFinalPath:
1206
 
                    if path_tree is not None:
1207
 
                        file_id = tt.final_file_id(trans_id)
1208
 
                        if file_id is None:
1209
 
                            file_id = tt.inactive_file_id(trans_id)
1210
 
                        _, entry = next(path_tree.iter_entries_by_dir(
1211
 
                            specific_files=[path_tree.id2path(file_id)]))
1212
 
                        # special-case the other tree root (move its
1213
 
                        # children to current root)
1214
 
                        if entry.parent_id is None:
1215
 
                            create = False
1216
 
                            moved = _reparent_transform_children(
1217
 
                                tt, trans_id, tt.root)
1218
 
                            for child in moved:
1219
 
                                new_conflicts.add((c_type, 'Moved to root',
1220
 
                                                   child))
1221
 
                        else:
1222
 
                            parent_trans_id = tt.trans_id_file_id(
1223
 
                                entry.parent_id)
1224
 
                            tt.adjust_path(entry.name, parent_trans_id,
1225
 
                                           trans_id)
1226
 
                if create:
1227
 
                    tt.create_directory(trans_id)
1228
 
                    new_conflicts.add((c_type, 'Created directory', trans_id))
1229
 
        elif c_type == 'unversioned parent':
1230
 
            file_id = tt.inactive_file_id(conflict[1])
1231
 
            # special-case the other tree root (move its children instead)
1232
 
            if path_tree and path_tree.path2id('') == file_id:
1233
 
                # This is the root entry, skip it
1234
 
                continue
1235
 
            tt.version_file(conflict[1], file_id=file_id)
1236
 
            new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
1237
 
        elif c_type == 'non-directory parent':
1238
 
            parent_id = conflict[1]
1239
 
            parent_parent = tt.final_parent(parent_id)
1240
 
            parent_name = tt.final_name(parent_id)
1241
 
            parent_file_id = tt.final_file_id(parent_id)
1242
 
            new_parent_id = tt.new_directory(parent_name + '.new',
1243
 
                                             parent_parent, parent_file_id)
1244
 
            _reparent_transform_children(tt, parent_id, new_parent_id)
1245
 
            if parent_file_id is not None:
1246
 
                tt.unversion_file(parent_id)
1247
 
            new_conflicts.add((c_type, 'Created directory', new_parent_id))
1248
 
        elif c_type == 'versioning no contents':
1249
 
            tt.cancel_versioning(conflict[1])
 
1273
    for conflict in conflicts:
 
1274
        resolver = CONFLICT_RESOLVERS.get(conflict[0])
 
1275
        if resolver is None:
 
1276
            continue
 
1277
        new_conflicts.update(resolver(tt, path_tree, *conflict))
1250
1278
    return new_conflicts
1251
1279
 
1252
1280
 
1253
 
def cook_conflicts(raw_conflicts, tt):
1254
 
    """Generate a list of cooked conflicts, sorted by file path"""
1255
 
    conflict_iter = iter_cook_conflicts(raw_conflicts, tt)
1256
 
    return sorted(conflict_iter, key=conflicts.Conflict.sort_key)
1257
 
 
1258
 
 
1259
 
def iter_cook_conflicts(raw_conflicts, tt):
1260
 
    fp = FinalPaths(tt)
1261
 
    for conflict in raw_conflicts:
1262
 
        c_type = conflict[0]
1263
 
        action = conflict[1]
1264
 
        modified_path = fp.get_path(conflict[2])
1265
 
        modified_id = tt.final_file_id(conflict[2])
1266
 
        if len(conflict) == 3:
1267
 
            yield conflicts.Conflict.factory(
1268
 
                c_type, action=action, path=modified_path, file_id=modified_id)
1269
 
 
1270
 
        else:
1271
 
            conflicting_path = fp.get_path(conflict[3])
1272
 
            conflicting_id = tt.final_file_id(conflict[3])
1273
 
            yield conflicts.Conflict.factory(
1274
 
                c_type, action=action, path=modified_path,
1275
 
                file_id=modified_id,
1276
 
                conflict_path=conflicting_path,
1277
 
                conflict_file_id=conflicting_id)
1278
 
 
1279
 
 
1280
1281
class _FileMover(object):
1281
1282
    """Moves and deletes files for TreeTransform, tracking operations"""
1282
1283
 
1357
1358
        self._all_children_cache = {}
1358
1359
        self._final_name_cache = {}
1359
1360
 
 
1361
    def supports_setting_file_ids(self):
 
1362
        raise NotImplementedError(self.supports_setting_file_ids)
 
1363
 
1360
1364
    @property
1361
1365
    def _by_parent(self):
1362
1366
        if self.__by_parent is None:
1383
1387
        pass
1384
1388
 
1385
1389
    def _path2trans_id(self, path):
 
1390
        """Look up the trans id associated with a path.
 
1391
 
 
1392
        :param path: path to look up, None when the path does not exist
 
1393
        :return: trans_id
 
1394
        """
1386
1395
        # We must not use None here, because that is a valid value to store.
1387
1396
        trans_id = self._path2trans_id_cache.get(path, object)
1388
1397
        if trans_id is not object: