@@ -2,57 +2,137 @@ package org.modelix.model
2
2
3
3
import org.modelix.model.lazy.CLVersion
4
4
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 >()
6
19
7
20
/* *
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.
11
23
*/
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.
16
69
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 > {
17
96
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
28
100
}
101
+ stack.addLast(fromVersion)
29
102
while (stack.isNotEmpty()) {
30
103
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()) {
33
107
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
40
110
} 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
+ }
42
124
}
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) }
48
125
}
49
126
}
127
+ return byVersionDistanceFromGlobalRoot
50
128
}
51
129
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 }
57
137
}
58
138
}
0 commit comments