Skip to content

Commit 5a816de

Browse files
committed
fix(model-datastructure): use the ObjectHash instead of the version ID
In addition to the hash a version also had a generated ID, but
1 parent f5746c8 commit 5a816de

File tree

12 files changed

+69
-47
lines changed

12 files changed

+69
-47
lines changed

datastructures/src/commonMain/kotlin/org/modelix/datastructures/objects/ObjectHash.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import org.modelix.kotlin.utils.base64UrlEncoded
55
import kotlin.jvm.JvmInline
66

77
@JvmInline
8-
value class ObjectHash(private val hash: String) {
8+
value class ObjectHash(private val hash: String) : Comparable<ObjectHash> {
99
init {
1010
require(isValidHashString(hash)) { "Not an object hash: $hash" }
1111
}
@@ -14,6 +14,10 @@ value class ObjectHash(private val hash: String) {
1414
return hash
1515
}
1616

17+
override fun compareTo(other: ObjectHash): Int {
18+
return hash.compareTo(other.hash)
19+
}
20+
1721
companion object {
1822
val HASH_PATTERN = Regex("""[a-zA-Z0-9\-_]{5}\*[a-zA-Z0-9\-_]{38}""")
1923

model-client/src/jvmMain/kotlin/org/modelix/model/client/ReplicatedRepository.kt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -124,9 +124,9 @@ actual open class ReplicatedRepository actual constructor(
124124
LOG.debug {
125125
String.format(
126126
"Merged local %s with remote %s -> %s",
127-
newLocalVersion.hash,
128-
remoteBase!!.hash,
129-
mergedVersion.hash,
127+
newLocalVersion.getObjectHash().toString(),
128+
remoteBase!!.getObjectHash().toString(),
129+
mergedVersion.getObjectHash().toString(),
130130
)
131131
}
132132
} catch (ex: Exception) {
@@ -163,7 +163,7 @@ actual open class ReplicatedRepository actual constructor(
163163

164164
protected fun writeRemoteVersion(newVersion: CLVersion) {
165165
synchronized(mergeLock) {
166-
if (remoteVersion!!.hash != newVersion.hash) {
166+
if (remoteVersion!!.getObjectHash() != newVersion.getObjectHash()) {
167167
remoteVersion = newVersion
168168
client.asyncStore.put(branchReference.getKey(), newVersion.getContentHash())
169169
}
@@ -271,9 +271,9 @@ actual open class ReplicatedRepository actual constructor(
271271
LOG.debug(
272272
String.format(
273273
"Merged remote %s with local %s -> %s",
274-
newRemoteVersion.hash,
275-
localBase.value!!.hash,
276-
mergedVersion.hash,
274+
newRemoteVersion.getObjectHash().toString(),
275+
localBase.value!!.getObjectHash().toString(),
276+
mergedVersion.getObjectHash().toString(),
277277
),
278278
)
279279
}

model-datastructure/src/commonMain/kotlin/org/modelix/model/LinearHistory.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class LinearHistory(private val baseVersionHash: String?) {
2424
private val byVersionDistanceFromGlobalRoot = mutableMapOf<CLVersion, Int>()
2525

2626
/**
27-
* Returns all versions between the [fromVersions] (inclusive) and a common version (inclusive).
27+
* Returns all versions between the [fromVersions] (inclusive) and a common version (exclusive).
2828
* The common version may be identified by [baseVersionHash].
2929
* If no [baseVersionHash] is given, the common version will be the first version
3030
* aka the version without a [CLVersion.baseVersion].
@@ -70,7 +70,7 @@ class LinearHistory(private val baseVersionHash: String?) {
7070
// Sorting the subtree roots by distance from base ensures topological order.
7171
val comparator = compareBy(byVersionDistanceFromGlobalRoot::getValue)
7272
// Sorting the subtree roots by distance from base and then by id ensures "monotonic" order.
73-
.thenBy(CLVersion::id)
73+
.thenBy(CLVersion::getObjectHash)
7474
val rootsOfSubtreesToVisit = globalRoot + byVersionDistanceFromGlobalRoot.keys.filter(CLVersion::isMerge)
7575
val orderedRootsOfSubtree = rootsOfSubtreesToVisit.distinct().sortedWith(comparator)
7676

@@ -85,7 +85,7 @@ class LinearHistory(private val baseVersionHash: String?) {
8585
val childrenWithoutMerges = children.filterNot(CLVersion::isMerge)
8686
// Order so that child with the lowest id is processed first
8787
// and comes first in the history.
88-
stack.addAll(childrenWithoutMerges.sortedByDescending(CLVersion::id))
88+
stack.addAll(childrenWithoutMerges.sortedByDescending(CLVersion::getObjectHash))
8989
}
9090
historyOfSubtree
9191
}
@@ -102,11 +102,11 @@ class LinearHistory(private val baseVersionHash: String?) {
102102
while (stack.isNotEmpty()) {
103103
val version = stack.last()
104104
val parents = version.getParents()
105-
// Version is the base version or the first version and therfore a root.
105+
// Version is the base version or the first version and therefore a root.
106106
if (parents.isEmpty()) {
107107
stack.removeLast()
108108
globalRoot.add(version)
109-
byVersionDistanceFromGlobalRoot[version] = 0
109+
byVersionDistanceFromGlobalRoot[version] = if (version.getObjectHash().toString() == baseVersionHash) 0 else 1
110110
} else {
111111
parents.forEach { parent ->
112112
byVersionChildren.getOrPut(parent, ::mutableSetOf).add(version)

model-datastructure/src/commonMain/kotlin/org/modelix/model/VersionMerger.kt

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.modelix.model
22

3+
import org.modelix.datastructures.objects.ObjectHash
34
import org.modelix.model.api.IIdGenerator
45
import org.modelix.model.async.IAsyncObjectStore
56
import org.modelix.model.lazy.CLVersion
@@ -24,7 +25,7 @@ class VersionMerger(private val idGenerator: IIdGenerator) {
2425
require(lastMergedVersion.graph == newVersion.graph) {
2526
"Versions are part of different object graphs: $lastMergedVersion, $newVersion"
2627
}
27-
if (newVersion.hash == lastMergedVersion.hash) {
28+
if (newVersion.getObjectHash() == lastMergedVersion.getObjectHash()) {
2829
return lastMergedVersion
2930
}
3031
checkRepositoryIds(lastMergedVersion, newVersion)
@@ -40,31 +41,31 @@ class VersionMerger(private val idGenerator: IIdGenerator) {
4041
}
4142
}
4243

43-
private fun collectLatestNonMerges(version: CLVersion?, visited: MutableSet<String>, result: MutableSet<Long>) {
44+
private fun collectLatestNonMerges(version: CLVersion?, visited: MutableSet<ObjectHash>, result: MutableSet<ObjectHash>) {
4445
if (version == null) return
45-
if (!visited.add(version.getContentHash())) return
46+
if (!visited.add(version.getObjectHash())) return
4647
if (version.isMerge()) {
4748
collectLatestNonMerges(version.getMergedVersion1(), visited, result)
4849
collectLatestNonMerges(version.getMergedVersion2(), visited, result)
4950
} else {
50-
result.add(version.id)
51+
result.add(version.getObjectHash())
5152
}
5253
}
5354

5455
protected fun mergeHistory(leftVersion: CLVersion, rightVersion: CLVersion): CLVersion {
55-
if (leftVersion.hash == rightVersion.hash) return leftVersion
56+
if (leftVersion.getObjectHash() == rightVersion.getObjectHash()) return leftVersion
5657
val commonBase = requireNotNull(commonBaseVersion(leftVersion, rightVersion)) {
5758
"Cannot merge versions without a common base: $leftVersion, $rightVersion"
5859
}
5960
if (commonBase.getContentHash() == leftVersion.getContentHash()) return rightVersion
6061
if (commonBase.getContentHash() == rightVersion.getContentHash()) return leftVersion
6162

62-
val leftNonMerges = HashSet<Long>().also { collectLatestNonMerges(leftVersion, HashSet(), it) }
63-
val rightNonMerges = HashSet<Long>().also { collectLatestNonMerges(rightVersion, HashSet(), it) }
63+
val leftNonMerges = HashSet<ObjectHash>().also { collectLatestNonMerges(leftVersion, HashSet(), it) }
64+
val rightNonMerges = HashSet<ObjectHash>().also { collectLatestNonMerges(rightVersion, HashSet(), it) }
6465
if (leftNonMerges == rightNonMerges) {
6566
// If there is no actual change on both sides, but they just did the same merge, we have to pick one
6667
// of them, otherwise both sides will continue creating merges forever.
67-
return if (leftVersion.id < rightVersion.id) leftVersion else rightVersion
68+
return if (leftVersion.getObjectHash() < rightVersion.getObjectHash()) leftVersion else rightVersion
6869
}
6970

7071
val versionsToApply = filterUndo(LinearHistory(commonBase.getContentHash()).load(leftVersion, rightVersion))
@@ -104,7 +105,7 @@ class VersionMerger(private val idGenerator: IIdGenerator) {
104105
.build()
105106
}
106107
if (mergedVersion == null) {
107-
throw RuntimeException("Failed to merge ${leftVersion.hash} and ${rightVersion.hash}")
108+
throw RuntimeException("Failed to merge ${leftVersion.getObjectHash()} and ${rightVersion.getObjectHash()}")
108109
}
109110
return mergedVersion
110111
}
@@ -131,7 +132,7 @@ class VersionMerger(private val idGenerator: IIdGenerator) {
131132
val operations = version.operations.toList()
132133
if (operations.isEmpty()) return listOf()
133134
val baseVersion = version.baseVersion
134-
?: throw RuntimeException("Version ${version.hash} has operations but no baseVersion")
135+
?: throw RuntimeException("Version ${version.getObjectHash()} has operations but no baseVersion")
135136
val tree = baseVersion.getModelTree()
136137
val mutableTree = tree.asMutableSingleThreaded()
137138
return mutableTree.runWrite {

model-datastructure/src/commonMain/kotlin/org/modelix/model/lazy/CLVersion.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ class CLVersion(val obj: Object<CPVersion>) : IVersion {
6161
val author: String?
6262
get() = data.author
6363

64+
@Deprecated("Use the ObjectHash instead. New versions of Modelix may set this to 0 and not generate actual IDs.")
6465
val id: Long
6566
get() = data.id
6667

@@ -79,7 +80,7 @@ class CLVersion(val obj: Object<CPVersion>) : IVersion {
7980
return null
8081
}
8182

82-
@Deprecated("Use getContentHash()", ReplaceWith("getContentHash()"))
83+
@Deprecated("Use getObjectHash()", ReplaceWith("getObjectHash()"))
8384
val hash: String
8485
get() = getContentHash()
8586

model-datastructure/src/commonMain/kotlin/org/modelix/model/lazy/VersionBuilder.kt

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.modelix.model.lazy
22

3+
import kotlinx.datetime.Clock
34
import kotlinx.datetime.Instant
45
import org.modelix.datastructures.model.IModelTree
56
import org.modelix.datastructures.objects.IObjectGraph
@@ -30,10 +31,13 @@ class VersionBuilder {
3031
private var numberOfOperations: Int = 0
3132
private var graph: IObjectGraph? = null
3233

34+
@Deprecated("Not mandatory anymore. Usages of the ID should be replaced by the ObjectHash.")
3335
fun id(value: Long) = also { this.id = value }
36+
3437
fun author(value: String?) = also { this.author = value }
3538
fun time(value: String?) = also { this.time = value }
3639
fun time(value: Instant) = time(value.epochSeconds.toString())
40+
fun currentTime() = time(Clock.System.now())
3741
fun tree(type: TreeType, value: Object<CPTree>) = also {
3842
this.treeRefs[type] = value
3943
if (it.graph == null) it.graph = value.graph
@@ -44,12 +48,13 @@ class VersionBuilder {
4448
fun graph(value: IObjectGraph?) = also { it.graph = value }
4549

4650
fun regularUpdate(baseVersion: IVersion) = regularUpdate((baseVersion as CLVersion).obj.ref)
51+
fun baseVersion(baseVersion: IVersion?) = regularUpdate((baseVersion as CLVersion?)?.obj?.ref)
4752

48-
fun regularUpdate(baseVersion: ObjectReference<CPVersion>) = also {
53+
fun regularUpdate(baseVersion: ObjectReference<CPVersion>?) = also {
4954
it.baseVersion = baseVersion
5055
it.mergedVersion1 = null
5156
it.mergedVersion2 = null
52-
if (it.graph == null) it.graph = baseVersion.graph
57+
if (baseVersion != null && it.graph == null) it.graph = baseVersion.graph
5358
}
5459

5560
fun autoMerge(commonBase: ObjectReference<CPVersion>, version1: ObjectReference<CPVersion>, version2: ObjectReference<CPVersion>) = also {
@@ -80,7 +85,7 @@ class VersionBuilder {
8085
}
8186

8287
fun buildData() = CPVersion(
83-
id = checkNotNull(id) { "id not specified" },
88+
id = id ?: 0,
8489
time = time,
8590
author = author,
8691
treeRefs = treeRefs.also {

model-datastructure/src/commonMain/kotlin/org/modelix/model/operations/RevertToOp.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ class RevertToOp(val latestKnownVersionRef: ObjectReference<CPVersion>, val vers
4848
val result = mutableListOf<IOperation>()
4949
val commonBase = VersionMerger.commonBaseVersion(latestKnownVersion, versionToRevertTo)
5050
result += getPath(latestKnownVersion, commonBase).map { UndoOp(it.resolvedData.ref) }
51-
if (commonBase == null || commonBase.hash != versionToRevertTo.hash) {
51+
if (commonBase == null || commonBase.getObjectHash() != versionToRevertTo.getObjectHash()) {
5252
// redo operations on a branch
5353
result += getPath(versionToRevertTo, commonBase).reversed().flatMap { it.operations }
5454
}
@@ -58,7 +58,7 @@ class RevertToOp(val latestKnownVersionRef: ObjectReference<CPVersion>, val vers
5858
private fun getPath(newerVersion: CLVersion, olderVersionExclusive: CLVersion?): List<CLVersion> {
5959
val result = mutableListOf<CLVersion>()
6060
var v = newerVersion
61-
while (olderVersionExclusive == null || v.hash != olderVersionExclusive.hash) {
61+
while (olderVersionExclusive == null || v.getObjectHash() != olderVersionExclusive.getObjectHash()) {
6262
result += v
6363
v = v.baseVersion ?: break
6464
}

model-datastructure/src/commonMain/kotlin/org/modelix/model/persistent/CPVersion.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,14 @@ import org.modelix.model.persistent.SerializationUtil.unescape
1818
import org.modelix.streams.IStream
1919

2020
data class CPVersion(
21+
/**
22+
* This ID was used in earlier versions where merges were done by rebasing.
23+
* After supporting a non-linear history where the two merged versions are recorded and stay part of the history
24+
* with their original ObjectHash, this ID isn't necessary anymore.
25+
*/
26+
@Deprecated("Use the ObjectHash instead")
2127
val id: Long,
28+
2229
val time: String?,
2330
val author: String?,
2431

model-datastructure/src/commonTest/kotlin/LinearHistoryTest.kt

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class LinearHistoryTest {
4343
val v20 = version(20, null)
4444
val v21 = version(21, null)
4545

46-
assertHistory(v20, v21, listOf(v20, v21))
46+
assertHistory(v20, v21, listOf(v21, v20))
4747
}
4848

4949
@Test
@@ -52,7 +52,7 @@ class LinearHistoryTest {
5252
val v20 = version(20, v10)
5353
val v21 = version(21, v10)
5454

55-
assertHistory(v20, v21, listOf(v20, v21))
55+
assertHistory(v20, v21, listOf(v21, v20))
5656
}
5757

5858
@Test
@@ -69,14 +69,26 @@ class LinearHistoryTest {
6969

7070
@Test
7171
fun correctHistoryIfIdsAreNotAscending() {
72+
/**
73+
* v1
74+
* / \
75+
* v2 v3
76+
* / \ /
77+
* v9 v4
78+
* | |
79+
* v8 |
80+
* \ /
81+
* x
82+
*/
83+
7284
val v1 = version(1, null)
7385
val v2 = version(2, v1)
7486
val v3 = version(3, v1)
7587
val v9 = version(9, v2)
7688
val v4 = merge(4, v2, v3)
7789
val v8 = version(8, v9)
7890

79-
val expected = listOf(v2, v9, v8, v3)
91+
val expected = listOf(v3, v2, v9, v8)
8092
assertHistory(v4, v8, expected)
8193
}
8294

@@ -132,14 +144,11 @@ class LinearHistoryTest {
132144
}
133145

134146
private fun version(id: Long, base: CLVersion?): CLVersion {
135-
return CLVersion.createRegularVersion(
136-
id,
137-
null,
138-
null,
139-
initialTree,
140-
base,
141-
emptyArray(),
142-
)
147+
return CLVersion.builder()
148+
.id(id)
149+
.tree(initialTree)
150+
.baseVersion(base)
151+
.build()
143152
}
144153

145154
private fun merge(id: Long, v1: CLVersion, v2: CLVersion): CLVersion {

model-datastructure/src/commonTest/kotlin/TreeSerializationTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ class TreeSerializationTest {
9292
assertTree(tree)
9393
assertEquals(expectedVersionHash, versionHash) // ensures that JVM and JS targets produce the same serialized data
9494

95-
val deserializedVersion = CLVersion.loadFromHash(version.hash, store)
95+
val deserializedVersion = CLVersion.loadFromHash(version.getObjectHash().toString(), store)
9696
assertTree(deserializedVersion.tree)
9797
}
9898

0 commit comments

Comments
 (0)