Initial library implementations and tests (#1)
All checks were successful
Main Workflow / Lint and test library (push) Successful in 16m40s

- [x] Implements library according to instructions, with tests.
- [x] GitHub Actions workflow to lint and test library.

Reviewed-on: #1
This commit is contained in:
2023-12-01 20:42:07 +00:00
parent d5ad54bea5
commit e2764a5473
10 changed files with 305 additions and 2 deletions

View File

@@ -0,0 +1,34 @@
package tinyvm
import java.security.MessageDigest
import java.time.Instant
abstract class Object(
val type: String,
) {
abstract val data: String
fun hash(): String =
MessageDigest
.getInstance("SHA-1")
.digest("$type ${data.length}\u0000$data".toByteArray())
.toHex()
}
class Commit(
val tree: Tree,
val author: Author,
val message: String,
val timestamp: Instant,
) : Object("commit") {
// Use \n\n for end of header in-case additional metadata is implemented in the future.
override val data: String
get() = "tree ${tree.hash()}\nauthor $author\ntimestamp ${timestamp.epochSecond}\n\n$message"
}
data class Author(
val name: String,
val email: String,
) {
override fun toString(): String = "$name <$email>"
}

View File

@@ -0,0 +1,5 @@
package tinyvm
import java.util.HexFormat
fun ByteArray.toHex(): String = HexFormat.of().formatHex(this)

View File

@@ -0,0 +1,92 @@
package tinyvm
class HashCollisionException(hash: String) : Exception("Different object types with identical hash '$hash'")
class CommitTimeComparator : Comparator<Commit> {
override fun compare(
o1: Commit,
o2: Commit,
): Int = (o1.timestamp.epochSecond - o2.timestamp.epochSecond).toInt()
}
class Repository {
private val commits = sortedSetOf(CommitTimeComparator())
// Store all objects in one map like git does. This would simplify the data persistence implementation (if there was
// one) and allows for other objects to be added in the future without modifying the data persistence implementation
// at all.
private val objects = mutableMapOf<String, Object>()
/**
* (Deep) get or put a commit object into the repository.
* This will also get or put all child trees and blobs.
*/
fun commit(commit: Commit): Commit {
val hash = commit.hash()
val obj = findObject<Commit>(hash)
if (obj != null) return obj
val newCommit =
Commit(
tree = addTree(commit.tree),
author = commit.author,
message = commit.message,
timestamp = commit.timestamp,
)
objects[hash] = newCommit
commits.add(newCommit)
return newCommit
}
/**
* Get a commit by its hash.
*/
fun getCommit(hash: String): Commit? = findObject<Commit>(hash)
/**
* List all commits.
*/
fun listCommits(): List<Commit> = commits.toList()
/**
* Find commit.
*/
fun findCommit(predicate: (Commit) -> Boolean): Commit? = commits.find(predicate)
/**
* Dump repository objects
*/
fun dumpObjects(): Map<String, Object> = objects
/**
* (Deep) get or put a tree object into the repository.
* This will also get or put all child trees and blobs.
*/
private fun addTree(tree: Tree): Tree {
val hash = tree.hash()
val obj = findObject<Tree>(hash)
if (obj != null) return obj
val newTree =
Tree(
tree.nodes.map { (name, node) ->
when (node) {
is Tree -> name to addTree(node)
is Blob -> name to addObject(node)
}
}.toMap(),
)
objects[hash] = newTree
return tree
}
/**
* (Shallow) get or put an object into the repository.
*/
private inline fun <reified T : Object> addObject(obj: T): T =
objects.getOrPut(obj.hash()) { obj } as? T ?: throw HashCollisionException(obj.hash())
/**
* Find an object in the repository by its hash.
*/
private inline fun <reified T : Object> findObject(hash: String): T? =
objects[hash]?.let { it as? T ?: throw HashCollisionException(hash) }
}

View File

@@ -0,0 +1,14 @@
package tinyvm
sealed class Node(type: String) : Object(type)
class Tree(val nodes: Map<String, Node>) : Node("tree") {
// For simplicity just use the hex-formatted hash, not the actual value like git does.
override val data: String
get() =
nodes.map { (name, node) ->
"${node.type} $name\u0000${node.hash()}"
}.sorted().joinToString()
}
class Blob(override val data: String) : Node("blob")

View File

@@ -0,0 +1,120 @@
package tinyvm
import java.time.Instant
import kotlin.test.Test
import kotlin.test.assertEquals
internal class RepositoryTest {
private val repository = Repository()
private val commits =
listOf(
Commit(
tree = Tree(mapOf("test1.txt" to Blob("Hello World!"))),
author = Author("Gleb Koval", "gleb@koval.net"),
message = "Add test1.txt",
timestamp = Instant.ofEpochSecond(0),
),
Commit(
tree =
Tree(
mapOf(
"dir1" to
Tree(
mapOf(
"test1.txt" to Blob("Hello World!"),
),
),
"dir2" to
Tree(
mapOf(
"test2.txt" to Blob("This is a second file"),
),
),
),
),
author = Author("Gleb Koval", "gleb@koval.net"),
message = "Move test1.txt and add dir2/test2.txt",
timestamp = Instant.ofEpochSecond(50),
),
Commit(
tree =
Tree(
mapOf(
"dir1" to
Tree(
mapOf(
"test1.txt" to Blob("Hello World!"),
),
),
"dir2" to
Tree(
mapOf(
"test2.txt" to Blob("This is a second file"),
),
),
"README.md" to Blob("# This is a test repo!"),
),
),
author = Author("Gleb Koval", "gleb@koval.net"),
message = "Add README.md",
timestamp = Instant.ofEpochSecond(100),
),
)
@Test
fun `can commit`() {
val committed = repository.commit(commits[0])
assertEquals(commits[0].hash(), committed.hash())
assertEquals(1, repository.listCommits().size)
assertEquals(3, repository.dumpObjects().size)
}
@Test
fun `can deduplicate commit`() {
repository.commit(commits[0])
assertEquals(1, repository.listCommits().size)
val committed = repository.commit(commits[0])
assertEquals(1, repository.listCommits().size)
assertEquals(3, repository.dumpObjects().size)
assertEquals(commits[0].hash(), committed.hash())
}
@Test
fun `can commit twice and deduplicate objects`() {
repository.commit(commits[0])
val committed = repository.commit(commits[1])
assertEquals(2, repository.listCommits().size)
assertEquals(7, repository.dumpObjects().size)
assertEquals(commits[1].hash(), committed.hash())
assertEquals(commits.take(2).map { it.hash() }, repository.listCommits().map { it.hash() })
}
@Test
fun `can commit in-between existing commits`() {
repository.commit(commits[0])
repository.commit(commits[2])
val committed = repository.commit(commits[1])
assertEquals(3, repository.listCommits().size)
assertEquals(10, repository.dumpObjects().size)
assertEquals(commits[1].hash(), committed.hash())
assertEquals(commits.map { it.hash() }, repository.listCommits().map { it.hash() })
}
@Test
fun `can get commit by hash`() {
repository.commit(commits[0])
val committed = repository.commit(commits[1])
repository.commit(commits[2])
assertEquals(committed, repository.getCommit(commits[1].hash()))
assertEquals(null, repository.getCommit("00000000000000000000"))
}
@Test
fun `can find commit by predicate`() {
repository.commit(commits[0])
val committed = repository.commit(commits[1])
repository.commit(commits[2])
assertEquals(committed, repository.findCommit { it.message.matches("Move.*".toRegex()) })
assertEquals(null, repository.getCommit("00000000000000000000"))
}
}