Compare commits

...

7 Commits
v0.0.1 ... main

Author SHA1 Message Date
a6d7802a83
Fix README and test for throws on branch not found.
All checks were successful
Test Workflow / Lint and test library (push) Successful in 1m52s
2024-01-10 12:40:10 +00:00
dbc1bdd105 Branch support (#7)
All checks were successful
Test Workflow / Lint and test library (push) Successful in 1m51s
Publish Workflow / Publish library (push) Successful in 2m18s
Closes #6.

Implements branch support by _only extending_ the existing functionality. Note that the existing functionality DOES NOT implement a `parent` pointer for commits, and as a result branches are also implemented differently to how git implements them.

git branches are simply a named pointer to a commit object (which then has a pointer to its parent commit), here branches store the entire sequence of pointers to commit objects.

### Why different to git?
I made a mistake in the initial implementation by using a `TreeSet` instead of manually implementing a tree for commits. This allowed the ability to insert commits in-between other commits, which isn't normally possible in `git` _without rebasing which changes commits' hashes_.

The specification for this feature stated:
> All functionality that is implemented already should be preserved.

And so since I cannot remove this functionality any more, I have to build on top of it.

Reviewed-on: #7
2024-01-10 12:21:25 +00:00
7233a7e8d1
Cleanup and privatise commit comparator
All checks were successful
Publish Workflow / Publish library (push) Successful in 8m28s
Test Workflow / Lint and test library (push) Successful in 16m51s
2023-12-05 02:00:35 +00:00
6cb4f82fce
Cleanup comments in README and Commit.kt
All checks were successful
Test Workflow / Lint and test library (push) Successful in 16m49s
2023-12-02 20:51:34 +00:00
804778b0fc Improve README - usage (#5)
All checks were successful
Publish Workflow / Publish library (push) Successful in 7m57s
Test Workflow / Lint and test library (push) Successful in 16m38s
Include instructions for usage of library in README, as well as how to view documentation.

Reviewed-on: #5
2023-12-02 03:50:13 +00:00
0e1afa6dc8 Implement toString() for objects (#4)
All checks were successful
Test Workflow / Lint and test library (push) Successful in 17m4s
> Search for specific commit by hash or metadata and print its content.

To satisfy this final requirement, we need to have `toString()` implemented for version manager `Object`s which will allow them to be easily (and nicely) printed if needed (includes tests).

Reviewed-on: #4
2023-12-02 02:50:08 +00:00
14950b2f5f Documentation generation (Dokka HTML) (#3)
All checks were successful
Publish Workflow / Publish library (push) Successful in 8m18s
Test Workflow / Lint and test library (push) Successful in 17m2s
Generate documentation HTML and `javadoc.jar`.

Reviewed-on: #3
2023-12-02 02:05:55 +00:00
8 changed files with 200 additions and 35 deletions

View File

@ -10,6 +10,26 @@ 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,7 @@
plugins { plugins {
kotlin("jvm") version "1.9.21" kotlin("jvm") version "1.9.21"
id("org.jmailen.kotlinter") version "4.1.0" id("org.jmailen.kotlinter") version "4.1.0"
id("org.jetbrains.dokka") version "1.9.10"
`maven-publish` `maven-publish`
} }
@ -23,10 +24,18 @@ kotlin {
jvmToolchain(17) jvmToolchain(17)
} }
val dokkaHtml by tasks.getting(org.jetbrains.dokka.gradle.DokkaTask::class)
val javadocJar: TaskProvider<Jar> by tasks.registering(Jar::class) {
dependsOn(dokkaHtml)
archiveClassifier.set("javadoc")
from(dokkaHtml.outputDirectory)
}
publishing { publishing {
publications.register<MavenPublication>("gpr") { publications.register<MavenPublication>("gpr") {
artifactId = "tinyvm" artifactId = "tinyvm"
from(components["java"]) from(components["java"])
artifact(javadocJar)
pom { pom {
name.set("TeamCity support for Gitea - Test Task - tiny version manager") name.set("TeamCity support for Gitea - Test Task - tiny version manager")
description.set("This is a small project to implement a subset of git's functionality in Kotlin and was " + description.set("This is a small project to implement a subset of git's functionality in Kotlin and was " +

View File

@ -1,29 +1,20 @@
package tinyvm package tinyvm
import java.security.MessageDigest
import java.time.Instant import java.time.Instant
abstract class Object( /**
val type: String, * Commits are a pointer to a 'head' tree with some metadata.
) { */
abstract val data: String
fun hash(): String =
MessageDigest
.getInstance("SHA-1")
.digest("$type ${data.length}\u0000$data".toByteArray())
.toHex()
}
class Commit( class Commit(
val tree: Tree, val tree: Tree,
val author: Author, val author: Author,
val message: String, val message: String,
val timestamp: Instant, val timestamp: Instant,
) : Object("commit") { ) : Object("commit") {
// Use \n\n for end of header in-case additional metadata is implemented in the future.
override val data: String override val data: String
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"
} }
data class Author( data class Author(

View File

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

View File

@ -0,0 +1,17 @@
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,22 +1,59 @@
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'")
class CommitTimeComparator : Comparator<Commit> { private 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()
} }
class Repository { private val commitTimeComparator = CommitTimeComparator()
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.
@ -33,7 +70,7 @@ class Repository {
timestamp = commit.timestamp, timestamp = commit.timestamp,
) )
objects[hash] = newCommit objects[hash] = newCommit
commits.add(newCommit) head.commits.add(newCommit)
return newCommit return newCommit
} }
@ -45,12 +82,12 @@ class Repository {
/** /**
* List all commits. * List all commits.
*/ */
fun listCommits(): List<Commit> = commits.toList() fun listCommits(): List<Commit> = head.commits.toList()
/** /**
* Find commit. * Find commit.
*/ */
fun findCommit(predicate: (Commit) -> Boolean): Commit? = commits.find(predicate) fun findCommit(predicate: (Commit) -> Boolean): Commit? = head.commits.find(predicate)
/** /**
* Dump repository objects * Dump repository objects
@ -89,4 +126,6 @@ class Repository {
*/ */
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

@ -1,7 +1,13 @@
package tinyvm package tinyvm
/**
* Tree nodes are either trees or blobs, represented by this sealed class.
*/
sealed class Node(type: String) : Object(type) sealed class Node(type: String) : Object(type)
/**
* A tree is a set of named nodes.
*/
class Tree(val nodes: Map<String, Node>) : Node("tree") { class Tree(val nodes: Map<String, Node>) : Node("tree") {
// For simplicity just use the hex-formatted hash, not the actual value like git does. // For simplicity just use the hex-formatted hash, not the actual value like git does.
override val data: String override val data: String
@ -9,6 +15,22 @@ 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")
} }
class Blob(override val data: String) : Node("blob") /**
* A blob is a data container.
*/
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,5 +1,6 @@
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
@ -18,22 +19,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 and add dir2/test2.txt", message = "Move test1.txt to dir1 and add dir2/test1.txt",
timestamp = Instant.ofEpochSecond(50), timestamp = Instant.ofEpochSecond(50),
), ),
Commit( Commit(
@ -49,7 +50,7 @@ internal class RepositoryTest {
"dir2" to "dir2" to
Tree( Tree(
mapOf( mapOf(
"test2.txt" to Blob("This is a second file"), "test1.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!"),
@ -117,4 +118,75 @@ 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(),
)
}
} }