|
1 | 1 | package org.modelix.model |
2 | 2 |
|
3 | 3 | import org.modelix.model.lazy.CLVersion |
4 | | -import org.modelix.model.lazy.IDeserializingKeyValueStore |
5 | | -import org.modelix.model.lazy.KVEntryReference |
6 | | -import org.modelix.model.persistent.CPVersion |
7 | 4 |
|
8 | | -/** |
9 | | - * Was introduced in https://github.com/modelix/modelix/commit/19c74bed5921028af3ac3ee9d997fc1c4203ad44 |
10 | | - * together with the UndoOp. The idea is that an undo should only revert changes if there is no other change that relies |
11 | | - * on it. In that case the undo should do nothing, to not indirectly undo newer changes. |
12 | | - * For example, if you added a node and someone else started changing properties on the that node, your undo should not |
13 | | - * remove the node to not lose the property changes. |
14 | | - * This requires the versions to be ordered in a way that the undo appears later. |
15 | | - */ |
16 | | -class LinearHistory(val baseVersionHash: String?) { |
| 5 | +class LinearHistory(private val baseVersionHash: String?) { |
| 6 | + /** |
| 7 | + * Children indexed by their parent versions. |
| 8 | + * A version is a parent of a child, |
| 9 | + * if the [CLVersion.baseVersion], the [CLVersion.getMergedVersion1] or [CLVersion.getMergedVersion2] |
| 10 | + */ |
| 11 | + private val byVersionChildren = mutableMapOf<CLVersion, MutableSet<CLVersion>>() |
17 | 12 |
|
18 | | - val version2directDescendants: MutableMap<Long, Set<Long>> = HashMap() |
19 | | - val versions: MutableMap<Long, CLVersion> = LinkedHashMap() |
| 13 | + /** |
| 14 | + * Global roots are versions without parents. |
| 15 | + * It may be only the version denoted [baseVersionHash] |
| 16 | + * or many versions, if no base version was specified and versions without a common global root are ordered. |
| 17 | + */ |
| 18 | + private val globalRoot = mutableSetOf<CLVersion>() |
20 | 19 |
|
21 | 20 | /** |
22 | | - * @param fromVersions it is assumed that the versions are sorted by the oldest version first. When merging a new |
23 | | - * version into an existing one the new version should appear after the existing one. The resulting order |
24 | | - * will prefer existing versions to new ones, meaning during the conflict resolution the existing changes |
25 | | - * have a higher probability of surviving. |
26 | | - * @returns oldest version first |
| 21 | + * The distance of a version from its root. |
| 22 | + * Aka how many children a between the root and a version. |
| 23 | + */ |
| 24 | + private val byVersionDistanceFromGlobalRoot = mutableMapOf<CLVersion, Int>() |
| 25 | + |
| 26 | + /** |
| 27 | + * Returns all versions between the [fromVersions] and a common version. |
| 28 | + * The common version may be identified by [baseVersionHash]. |
| 29 | + * If no [baseVersionHash] is given, the common version wile be the first version |
| 30 | + * aka the version without a [CLVersion.baseVersion]. |
| 31 | + * |
| 32 | + * The order also ensures three properties: |
| 33 | + * 1. The versions are ordered topologically starting with the versions without parents. |
| 34 | + * 2. The order is also "monotonic". |
| 35 | + * This means adding a version to the set of all versions will never change |
| 36 | + * the order of versions that were previously in the history. |
| 37 | + * For example, given versions 1, 2 and 3: |
| 38 | + * If 1 and 2 are ordered as (1, 2), ordering 1, 2 and 3 will never produce (2, 3, 1). |
| 39 | + * 3 can come anywhere (respecting the topological ordering), but 2 has to come after 1. |
| 40 | + * 3. "Close versions are kept together" |
| 41 | + * Formally: A version that has only one child (ignoring) should always come before the child. |
| 42 | + * Example: 1 <- 2 <- 3 and 1 <- x, then [1, 2, 4, 3] is not allowed, |
| 43 | + * because 3 is the only child of 2. |
| 44 | + * Valid orders would be (1, x, 3, 4) and (1, x, 2, 3) |
| 45 | + * This is relevant for UnduOp and RedoOp. |
| 46 | + * See UndoTest. |
27 | 47 | */ |
28 | 48 | fun load(vararg fromVersions: CLVersion): List<CLVersion> { |
29 | | - for (fromVersion in fromVersions) { |
30 | | - collect(fromVersion) |
31 | | - } |
| 49 | + // Traverse the versions once to index need data: |
| 50 | + // * Collect all relevant versions. |
| 51 | + // * Collect the distance to the base for each version. |
| 52 | + // * Collect the roots of relevant versions. |
| 53 | + // * Collect the children of each version. |
| 54 | + indexData(*fromVersions) |
32 | 55 |
|
33 | | - var result: List<Long> = emptyList() |
| 56 | + // The following algorithm orders the version by |
| 57 | + // 1. Finding out the roots of so-called subtrees. |
| 58 | + // A subtree is a tree of all versions that have the same version as root ancestor. |
| 59 | + // A root ancestor of a version is the first ancestor in the chain of ancestors |
| 60 | + // that is either a merge or a global root. |
| 61 | + // Each version belongs to exactly one root ancestor, and it will be the same (especially future merges). |
| 62 | + // 2. Sort the roots of subtrees according to their distance (primary) and id (secondary) |
| 63 | + // 3. Order topologically inside each subtree. |
34 | 64 |
|
35 | | - for (version in versions.values.filter { !it.isMerge() }.sortedBy { it.id }) { |
36 | | - val descendantIds = collectAllDescendants(version.id).filter { !versions[it]!!.isMerge() }.sorted().toSet() |
37 | | - val idsInResult = result.toHashSet() |
38 | | - if (idsInResult.contains(version.id)) { |
39 | | - result = |
40 | | - result + |
41 | | - descendantIds.filter { !idsInResult.contains(it) } |
42 | | - } else { |
43 | | - result = |
44 | | - result.filter { !descendantIds.contains(it) } + |
45 | | - version.id + |
46 | | - result.filter { descendantIds.contains(it) } + |
47 | | - descendantIds.filter { !idsInResult.contains(it) } |
48 | | - } |
49 | | - } |
50 | | - return result.map { versions[it]!! } |
51 | | - } |
| 65 | + // Ordering the subtree root first, ensures the order is also "monotonic". |
| 66 | + // Then ordering the inside subtree ensures "close versions are kept together" without breaking "monotonicity". |
| 67 | + // Ordering inside a subtree ensures "monotonicity", because a subtree has no merges. |
| 68 | + // Only a subtrees root can be a merge. |
52 | 69 |
|
53 | | - private fun collectAllDescendants(root: Long): Set<Long> { |
54 | | - val result = LinkedHashSet<Long>() |
55 | | - var previousSize = 0 |
56 | | - result += root |
| 70 | + // Sorting the subtree roots by distance from base ensures topological order. |
| 71 | + val comparator = compareBy(byVersionDistanceFromGlobalRoot::getValue) |
| 72 | + // Sorting the subtree roots by distance from base and then by id ensures "monotonic" order. |
| 73 | + .thenBy(CLVersion::id) |
| 74 | + val rootsOfSubtreesToVisit = globalRoot + byVersionDistanceFromGlobalRoot.keys.filter(CLVersion::isMerge) |
| 75 | + val orderedRootsOfSubtree = rootsOfSubtreesToVisit.distinct().sortedWith(comparator) |
57 | 76 |
|
58 | | - while (previousSize != result.size) { |
59 | | - val nextElements = result.asSequence().drop(previousSize).toList() |
60 | | - previousSize = result.size |
61 | | - for (ancestor in nextElements) { |
62 | | - version2directDescendants[ancestor]?.let { result += it } |
| 77 | + val history = orderedRootsOfSubtree.flatMap { rootOfSubtree -> |
| 78 | + val historyOfSubtree = mutableListOf<CLVersion>() |
| 79 | + val stack = ArrayDeque<CLVersion>() |
| 80 | + stack.add(rootOfSubtree) |
| 81 | + while (stack.isNotEmpty()) { |
| 82 | + val version = stack.removeLast() |
| 83 | + historyOfSubtree.add(version) |
| 84 | + val children = byVersionChildren.getOrElse(version, ::emptyList) |
| 85 | + val childrenWithoutMerges = children.filterNot(CLVersion::isMerge) |
| 86 | + // Order so that child with the lowest id is processed first |
| 87 | + // and comes first in the history. |
| 88 | + stack.addAll(childrenWithoutMerges.sortedByDescending(CLVersion::id)) |
63 | 89 | } |
| 90 | + historyOfSubtree |
64 | 91 | } |
65 | | - |
66 | | - return result.drop(1).toSet() |
| 92 | + return history.filterNot(CLVersion::isMerge) |
67 | 93 | } |
68 | 94 |
|
69 | | - private fun collect(root: CLVersion) { |
70 | | - if (root.getContentHash() == baseVersionHash) return |
71 | | - |
72 | | - var previousSize = versions.size |
73 | | - versions[root.id] = root |
74 | | - |
75 | | - while (previousSize != versions.size) { |
76 | | - val nextElements = versions.asSequence().drop(previousSize).map { it.value }.toList() |
77 | | - previousSize = versions.size |
78 | | - |
79 | | - for (descendant in nextElements) { |
80 | | - val ancestors = if (descendant.isMerge()) { |
81 | | - sequenceOf( |
82 | | - getVersion(descendant.data!!.mergedVersion1!!, descendant.store), |
83 | | - getVersion(descendant.data!!.mergedVersion2!!, descendant.store), |
84 | | - ) |
| 95 | + private fun indexData(vararg fromVersions: CLVersion): MutableMap<CLVersion, Int> { |
| 96 | + val stack = ArrayDeque<CLVersion>() |
| 97 | + fromVersions.forEach { fromVersion -> |
| 98 | + if (byVersionDistanceFromGlobalRoot.contains(fromVersion)) { |
| 99 | + return@forEach |
| 100 | + } |
| 101 | + stack.addLast(fromVersion) |
| 102 | + while (stack.isNotEmpty()) { |
| 103 | + val version = stack.last() |
| 104 | + val parents = version.getParents() |
| 105 | + // Version is the base version or the first version and therfore a root. |
| 106 | + if (parents.isEmpty()) { |
| 107 | + stack.removeLast() |
| 108 | + globalRoot.add(version) |
| 109 | + byVersionDistanceFromGlobalRoot[version] = 0 |
85 | 110 | } else { |
86 | | - sequenceOf(descendant.baseVersion) |
87 | | - }.filterNotNull().filter { it.getContentHash() != baseVersionHash }.toList() |
88 | | - for (ancestor in ancestors) { |
89 | | - versions[ancestor.id] = ancestor |
90 | | - version2directDescendants[ancestor.id] = (version2directDescendants[ancestor.id] ?: emptySet()) + setOf(descendant.id) |
| 111 | + parents.forEach { parent -> |
| 112 | + byVersionChildren.getOrPut(parent, ::mutableSetOf).add(version) |
| 113 | + } |
| 114 | + val (visitedParents, notVisitedParents) = parents.partition(byVersionDistanceFromGlobalRoot::contains) |
| 115 | + // All children where already visited and have their distance known. |
| 116 | + if (notVisitedParents.isEmpty()) { |
| 117 | + stack.removeLast() |
| 118 | + val depth = visitedParents.maxOf { byVersionDistanceFromGlobalRoot[it]!! } + 1 |
| 119 | + byVersionDistanceFromGlobalRoot[version] = depth |
| 120 | + // Children need to be visited |
| 121 | + } else { |
| 122 | + stack.addAll(notVisitedParents) |
| 123 | + } |
91 | 124 | } |
92 | 125 | } |
93 | 126 | } |
| 127 | + return byVersionDistanceFromGlobalRoot |
94 | 128 | } |
95 | 129 |
|
96 | | - private fun getVersion(hash: KVEntryReference<CPVersion>, store: IDeserializingKeyValueStore): CLVersion { |
97 | | - return CLVersion(hash.getValue(store), store) |
| 130 | + private fun CLVersion.getParents(): List<CLVersion> { |
| 131 | + val ancestors = if (isMerge()) { |
| 132 | + listOf(getMergedVersion1()!!, getMergedVersion2()!!) |
| 133 | + } else { |
| 134 | + listOfNotNull(baseVersion) |
| 135 | + } |
| 136 | + return ancestors.filter { it.getContentHash() != baseVersionHash } |
98 | 137 | } |
99 | 138 | } |
0 commit comments