@@ -2,57 +2,137 @@ package org.modelix.model
22
33import org.modelix.model.lazy.CLVersion
44
5- 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 >>()
12+
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 >()
619
720 /* *
8- * Order all versions descending from any versions in [[fromVersions]] topologically.
9- * This means that a version must come after all its descendants.
10- * Returns the ordered versions starting with the earliest version.
21+ * The distance of a version from its root.
22+ * Aka how many children a between the root and a version.
1123 */
12- fun computeHistoryLazy (vararg fromVersions : CLVersion ) = sequence {
13- // The algorithm sorts the versions topologically.
14- // It performs a depth-first search.
15- // It is implemented as an iterative algorithm with a stack.
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.
47+ */
48+ fun load (vararg fromVersions : CLVersion ): List <CLVersion > {
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)
55+
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.
64+
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.
1669
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)
76+
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))
89+ }
90+ historyOfSubtree
91+ }
92+ return history.filterNot(CLVersion ::isMerge)
93+ }
94+
95+ private fun indexData (vararg fromVersions : CLVersion ): MutableMap <CLVersion , Int > {
1796 val stack = ArrayDeque <CLVersion >()
18- val visited = mutableSetOf<CLVersion >()
19-
20- // Ensure deterministic merging,
21- // by putting versions with lower id before versions with higher id.
22- fromVersions.sortedBy { it.id }.forEach { fromVersion ->
23- // Not putting fromVersions directly on the stack and checking visited.contains(fromVersion) ,
24- // ensures the algorithm terminates if one version in `fromVersion`
25- // is a descendant of another version in `fromVersion`
26- if (! visited.contains(fromVersion)) {
27- stack.addLast(fromVersion)
97+ fromVersions.forEach { fromVersion ->
98+ if (byVersionDistanceFromGlobalRoot.contains(fromVersion)) {
99+ return @forEach
28100 }
101+ stack.addLast(fromVersion)
29102 while (stack.isNotEmpty()) {
30103 val version = stack.last()
31- val versionWasVisited = ! visited.add(version)
32- if (versionWasVisited) {
104+ val parents = version.getParents()
105+ // Version is the base version or the first version and therfore a root.
106+ if (parents.isEmpty()) {
33107 stack.removeLast()
34- yield (version)
35- }
36- val descendants = if (version.isMerge()) {
37- // Put version 1 last, so that is processed first.
38- // We are using a stack and the last version is viewed first.
39- listOf (version.getMergedVersion2()!! , version.getMergedVersion1()!! )
108+ globalRoot.add(version)
109+ byVersionDistanceFromGlobalRoot[version] = 0
40110 } else {
41- listOfNotNull(version.baseVersion)
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+ }
42124 }
43- // Ignore descendant, if it is a base version.
44- val relevantDescendants = descendants.filter { it.getContentHash() != baseVersionHash }
45- // Ignore already visited descendants.
46- val nonCheckedDescendants = relevantDescendants.filterNot { visited.contains(it) }
47- nonCheckedDescendants.forEach { stack.addLast(it) }
48125 }
49126 }
127+ return byVersionDistanceFromGlobalRoot
50128 }
51129
52- /* *
53- * Same as [[computeHistoryLazy]], but returning as a list instead of a lazy sequence and omitting merge versions.
54- */
55- fun computeHistoryWithoutMerges (vararg fromVersions : CLVersion ): List <CLVersion > {
56- return computeHistoryLazy(* fromVersions).filterNot { it.isMerge() }.toList()
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 }
57137 }
58138}
0 commit comments