Compare commits

..

No commits in common. "main" and "v0.0.2" have entirely different histories.
main ... v0.0.2

6 changed files with 33 additions and 177 deletions

View File

@ -10,26 +10,6 @@ The package is named `tinyvm` for 'tiny version manager'.
Since this is an internship application project, I have assumed that a minimal usage of external libraries is preferred, Since this is an internship application project, I have assumed that a minimal usage of external libraries is preferred,
so that a greater technical understanding can be demonstrated. so that a greater technical understanding can be demonstrated.
## Usage
### Gradle
```kotlin
repositories {
// other repositories
maven { url "https://git.koval.net/api/packages/cyclane/maven" }
}
dependencies {
// other dependencies
implementation("net.koval.teamcity-gitea-test-task:tinyvm:0.1.1")
}
```
### Documentation
Use autocompletion and hover menus in your IDE, or download the
generated HTML documentation
from the [latest release](https://git.koval.net/cyclane/teamcity-gitea-test-task/releases).
## Instructions ## Instructions
Create a library that implements simple Git functionality. You need to implement at least three entities: Create a library that implements simple Git functionality. You need to implement at least three entities:

View File

@ -1,6 +1,22 @@
package tinyvm package tinyvm
import java.security.MessageDigest
import java.time.Instant import java.time.Instant
import java.util.HexFormat
/**
* Represents an arbitrary version manager object.
*/
abstract class Object(val type: String) {
abstract val data: String
fun hash(): String =
HexFormat.of().formatHex(
MessageDigest
.getInstance("SHA-1")
.digest("$type ${data.length}\u0000$data".toByteArray()),
)
}
/** /**
* Commits are a pointer to a 'head' tree with some metadata. * Commits are a pointer to a 'head' tree with some metadata.
@ -12,6 +28,7 @@ class Commit(
val timestamp: Instant, val timestamp: Instant,
) : Object("commit") { ) : Object("commit") {
override val data: String override val data: String
// Use \n\n for end of header in-case additional metadata is implemented in the future.
get() = "tree ${tree.hash()}\nauthor $author\ntimestamp ${timestamp.epochSecond}\n\n$message" get() = "tree ${tree.hash()}\nauthor $author\ntimestamp ${timestamp.epochSecond}\n\n$message"
override fun toString(): String = "commit ${hash()}\n$data" override fun toString(): String = "commit ${hash()}\n$data"

View File

@ -1,17 +0,0 @@
package tinyvm
import java.security.MessageDigest
import java.util.HexFormat
/**
* Represents an arbitrary version manager object.
*/
abstract class Object(val type: String) {
abstract val data: String
fun hash(): String =
HexFormat.of().formatHex(
MessageDigest.getInstance("SHA-1")
.digest("$type ${data.length}\u0000$data".toByteArray()),
)
}

View File

@ -1,59 +1,22 @@
package tinyvm package tinyvm
import java.util.TreeSet
class HashCollisionException(hash: String) : Exception("Different object types with identical hash '$hash'") class HashCollisionException(hash: String) : Exception("Different object types with identical hash '$hash'")
private class CommitTimeComparator : Comparator<Commit> { class CommitTimeComparator : Comparator<Commit> {
override fun compare( override fun compare(
o1: Commit, o1: Commit,
o2: Commit, o2: Commit,
): Int = (o1.timestamp.epochSecond - o2.timestamp.epochSecond).toInt() ): Int = (o1.timestamp.epochSecond - o2.timestamp.epochSecond).toInt()
} }
private val commitTimeComparator = CommitTimeComparator() class Repository {
private val commits = sortedSetOf(CommitTimeComparator())
private class Branch(val name: String, val commits: TreeSet<Commit>)
class Repository(initialBranch: String = "master") {
private var head = Branch(initialBranch, sortedSetOf(commitTimeComparator))
private val commits = mutableMapOf(initialBranch to head)
val branch: String
get() = head.name
val branches: Set<String>
get() = commits.keys
// Store all objects in one map like git does. This would simplify the data persistence implementation (if there was // 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 // one) and allows for other objects to be added in the future without modifying the data persistence implementation
// at all. // at all.
private val objects = mutableMapOf<String, Object>() private val objects = mutableMapOf<String, Object>()
/**
* Create a new branch as a copy of the current branch.
* @param name New branch name.
* @param use Whether to switch to this branch.
*/
fun createBranch(
name: String,
use: Boolean = true,
) {
commits[name] =
Branch(
name,
head.commits.clone() as? TreeSet<Commit> ?: throw Exception("TreeSet.clone() unexpected result"),
)
if (use) useBranch(name)
}
/**
* Use (switch to) a branch.
*/
fun useBranch(name: String) {
head = commits[name] ?: throw BranchNotFoundException(name)
}
/** /**
* (Deep) get or put a commit object into the repository. * (Deep) get or put a commit object into the repository.
* This will also get or put all child trees and blobs. * This will also get or put all child trees and blobs.
@ -70,7 +33,7 @@ class Repository(initialBranch: String = "master") {
timestamp = commit.timestamp, timestamp = commit.timestamp,
) )
objects[hash] = newCommit objects[hash] = newCommit
head.commits.add(newCommit) commits.add(newCommit)
return newCommit return newCommit
} }
@ -82,12 +45,12 @@ class Repository(initialBranch: String = "master") {
/** /**
* List all commits. * List all commits.
*/ */
fun listCommits(): List<Commit> = head.commits.toList() fun listCommits(): List<Commit> = commits.toList()
/** /**
* Find commit. * Find commit.
*/ */
fun findCommit(predicate: (Commit) -> Boolean): Commit? = head.commits.find(predicate) fun findCommit(predicate: (Commit) -> Boolean): Commit? = commits.find(predicate)
/** /**
* Dump repository objects * Dump repository objects
@ -127,5 +90,3 @@ class Repository(initialBranch: String = "master") {
private inline fun <reified T : Object> findObject(hash: String): T? = private inline fun <reified T : Object> findObject(hash: String): T? =
objects[hash]?.let { it as? T ?: throw HashCollisionException(hash) } objects[hash]?.let { it as? T ?: throw HashCollisionException(hash) }
} }
class BranchNotFoundException(name: String) : Exception("Branch '$name' does not exist")

View File

@ -15,22 +15,9 @@ class Tree(val nodes: Map<String, Node>) : Node("tree") {
nodes.map { (name, node) -> nodes.map { (name, node) ->
"${node.type} $name\u0000${node.hash()}" "${node.type} $name\u0000${node.hash()}"
}.sorted().joinToString() }.sorted().joinToString()
override fun toString(): String =
"tree ${hash()}\n" +
nodes.map { (name, node) ->
when (node) {
is Tree -> "+$name/\n"
is Blob -> "+$name\n"
} + node.toString().insertLeftMargin()
}.sorted().joinToString("\n")
} }
/** /**
* A blob is a data container. * A blob is a data container.
*/ */
class Blob(override val data: String) : Node("blob") { class Blob(override val data: String) : Node("blob")
override fun toString(): String = "blob ${hash()}\n${data.insertLeftMargin()}"
}
private fun String.insertLeftMargin(): String = split('\n').joinToString("\n") { "| $it" }

View File

@ -1,6 +1,5 @@
package tinyvm package tinyvm
import org.junit.jupiter.api.assertThrows
import java.time.Instant import java.time.Instant
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
@ -19,22 +18,22 @@ internal class RepositoryTest {
tree = tree =
Tree( Tree(
mapOf( mapOf(
"dir2" to
Tree(
mapOf(
"test1.txt" to Blob("This is a second file"),
),
),
"dir1" to "dir1" to
Tree( Tree(
mapOf( mapOf(
"test1.txt" to Blob("Hello World!"), "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"), author = Author("Gleb Koval", "gleb@koval.net"),
message = "Move test1.txt to dir1 and add dir2/test1.txt", message = "Move test1.txt and add dir2/test2.txt",
timestamp = Instant.ofEpochSecond(50), timestamp = Instant.ofEpochSecond(50),
), ),
Commit( Commit(
@ -50,7 +49,7 @@ internal class RepositoryTest {
"dir2" to "dir2" to
Tree( Tree(
mapOf( mapOf(
"test1.txt" to Blob("This is a second file"), "test2.txt" to Blob("This is a second file"),
), ),
), ),
"README.md" to Blob("# This is a test repo!"), "README.md" to Blob("# This is a test repo!"),
@ -118,75 +117,4 @@ internal class RepositoryTest {
assertEquals(committed, repository.findCommit { it.message.matches("Move.*".toRegex()) }) assertEquals(committed, repository.findCommit { it.message.matches("Move.*".toRegex()) })
assertEquals(null, repository.getCommit("00000000000000000000")) assertEquals(null, repository.getCommit("00000000000000000000"))
} }
@Test
fun `can create and use branch`() {
repository.commit(commits[0])
repository.commit(commits[1])
repository.commit(commits[2])
repository.createBranch("test-branch", use = false)
assertEquals("master", repository.branch)
repository.createBranch("test-branch-2")
assertEquals("test-branch-2", repository.branch)
assertEquals(setOf("master", "test-branch", "test-branch-2"), repository.branches)
assertEquals(commits.map { it.hash() }, repository.listCommits().map { it.hash() })
assertEquals(10, repository.dumpObjects().size)
}
@Test
fun `cannot use non-existent branch`() {
assertThrows<BranchNotFoundException> {
repository.useBranch("test-branch")
}
}
@Test
fun `can commit to branch`() {
repository.commit(commits[0])
repository.createBranch("test-branch")
repository.commit(commits[1])
repository.createBranch("test-branch-2")
repository.commit(commits[2])
assertEquals(commits.map { it.hash() }, repository.listCommits().map { it.hash() })
repository.useBranch("test-branch")
assertEquals(commits.take(2).map { it.hash() }, repository.listCommits().map { it.hash() })
repository.useBranch("master")
assertEquals(commits.take(1).map { it.hash() }, repository.listCommits().map { it.hash() })
}
@Test
fun `can display commit`() {
assertEquals(
"""
commit 804c6f0c66da0ea0eab8ac29f23a627e03a962b2
tree aaebbb258bce30749fc302cbd161b78462252a32
author Gleb Koval <gleb@koval.net>
timestamp 0
Add test1.txt
""".trimIndent(),
commits[0].toString(),
)
}
@Test
fun `can display tree`() {
assertEquals(
"""
tree 048b8e267dd7a6c5d4849d59f6ff82e6ae948f13
+dir1/
| tree aaebbb258bce30749fc302cbd161b78462252a32
| +test1.txt
| | blob c57eff55ebc0c54973903af5f72bac72762cf4f4
| | | Hello World!
+dir2/
| tree 0e14cbdba02924cd63a58c2492fc3dd05dc682bd
| +test1.txt
| | blob da3fcc31cbc16fcbb7526d68771f77e7e7f02fb1
| | | This is a second file
""".trimIndent(),
commits[1].tree.toString(),
)
}
} }