From f07e42abfabb0faec657403ff545ff1d76b03d50 Mon Sep 17 00:00:00 2001 From: Arian Date: Sun, 16 Aug 2020 13:11:13 +0200 Subject: [PATCH] Linking HEAD on checkout - symbolic references --- src/main/kotlin/com/github/arian/gikt/Refs.kt | 61 +++++++++++++++---- .../github/arian/gikt/commands/Checkout.kt | 6 +- .../kotlin/com/github/arian/gikt/RefsTest.kt | 46 +++++++++++++- .../arian/gikt/commands/CheckoutTest.kt | 5 +- 4 files changed, 100 insertions(+), 18 deletions(-) diff --git a/src/main/kotlin/com/github/arian/gikt/Refs.kt b/src/main/kotlin/com/github/arian/gikt/Refs.kt index 03197a3..88b650c 100644 --- a/src/main/kotlin/com/github/arian/gikt/Refs.kt +++ b/src/main/kotlin/com/github/arian/gikt/Refs.kt @@ -1,6 +1,7 @@ package com.github.arian.gikt import com.github.arian.gikt.database.ObjectId +import java.nio.file.NoSuchFileException import java.nio.file.Path class Refs(private val pathname: Path) { @@ -11,18 +12,54 @@ class Refs(private val pathname: Path) { private val refsPath = pathname.resolve("refs") private val headsPath = refsPath.resolve("heads") + companion object { + private val SYMREF = "^ref: (.+)$".toRegex() + } + + sealed class Ref { + data class SymRef(val path: Path) : Ref() + data class Oid(val oid: ObjectId) : Ref() + } + fun updateHead(oid: ObjectId) { updateRefFile(headPath, oid) } + fun setHead(revision: String, oid: ObjectId) { + val path = headsPath.resolve(revision) + if (path.exists()) { + val relative = path.relativeTo(headPath.parent) + updateRefFile(headPath, "ref: $relative") + } else { + updateRefFile(headPath, oid) + } + } + fun readHead(): ObjectId? = - readRefFile(headPath) + readSymRef(headPath) - private fun readRefFile(path: Path): ObjectId? = - path - .takeIf { it.exists() } - ?.readText() - ?.let { ObjectId(it.trim()) } + fun readRef(name: String): ObjectId? = + pathForName(name)?.let { + readSymRef(it) + } + + private fun readOidOrSymRef(path: Path): Ref? = + try { + val data = path.readText().trim() + SYMREF.find(data) + ?.destructured + ?.let { (ref) -> Ref.SymRef(path.parent.resolve(ref)) } + ?: Ref.Oid(ObjectId(data)) + } catch (e: NoSuchFileException) { + null + } + + private fun readSymRef(path: Path): ObjectId? = + when (val ref = readOidOrSymRef(path)) { + is Ref.SymRef -> readSymRef(ref.path) + is Ref.Oid -> ref.oid + null -> null + } private fun pathForName(name: String): Path? = listOf(pathname, refsPath, headsPath) @@ -30,11 +67,6 @@ class Refs(private val pathname: Path) { .map { it.resolve(name) } .find { it.exists() } - fun readRef(name: String): ObjectId? = - pathForName(name)?.let { - readRefFile(it) - } - fun createBranch(branchName: String, startOid: ObjectId) { if (!Revision.validRef(branchName)) { @@ -50,9 +82,12 @@ class Refs(private val pathname: Path) { updateRefFile(path, startOid) } - private fun updateRefFile(path: Path, oid: ObjectId) { + private fun updateRefFile(path: Path, oid: ObjectId) = + updateRefFile(path, oid.hex) + + private fun updateRefFile(path: Path, ref: String) { fun update() = Lockfile(path).holdForUpdate { - it.write(oid.hex) + it.write(ref) it.write("\n") it.commit() } diff --git a/src/main/kotlin/com/github/arian/gikt/commands/Checkout.kt b/src/main/kotlin/com/github/arian/gikt/commands/Checkout.kt index ceac8f5..b365818 100644 --- a/src/main/kotlin/com/github/arian/gikt/commands/Checkout.kt +++ b/src/main/kotlin/com/github/arian/gikt/commands/Checkout.kt @@ -11,20 +11,20 @@ class Checkout(ctx: CommandContext) : AbstractCommand(ctx) { private fun checkout(target: String): Nothing { try { - val revision = Revision(repository, target).resolve() + val targetOid = Revision(repository, target).resolve() val currentOid = repository.refs.readHead() ?: throw UnbornHead("You are on a branch yet to be born") val code = repository.index.loadForUpdate { - val treeDiff: TreeDiffMap = repository.database.treeDiff(currentOid, revision) + val treeDiff: TreeDiffMap = repository.database.treeDiff(currentOid, targetOid) val migration = repository.migration(treeDiff) val result = migration.applyChanges(this) if (result.errors.isEmpty()) { writeUpdates() - repository.refs.updateHead(revision) + repository.refs.setHead(target, targetOid) return@loadForUpdate 0 } else { rollback() diff --git a/src/test/kotlin/com/github/arian/gikt/RefsTest.kt b/src/test/kotlin/com/github/arian/gikt/RefsTest.kt index 2bc404c..f77be42 100644 --- a/src/test/kotlin/com/github/arian/gikt/RefsTest.kt +++ b/src/test/kotlin/com/github/arian/gikt/RefsTest.kt @@ -45,7 +45,10 @@ class RefsTest(private val fileSystemProvider: FileSystemExtension.FileSystemPro fun `create valid branch names`() { git.resolve("HEAD").write("abc123") val refs = Refs(git) - refs.createBranch("topic", ObjectId("abc")) + val oid = ObjectId("abcd") + refs.createBranch("topic", oid) + assertEquals("abc123", git.resolve("HEAD").readText()) + assertEquals("${oid.hex}\n", git.resolve("refs/heads/topic").readText()) } @Test @@ -56,4 +59,45 @@ class RefsTest(private val fileSystemProvider: FileSystemExtension.FileSystemPro val e = assertThrows { refs.createBranch("topic", ObjectId("")) } assertEquals("A branch named 'topic' already exists.", e.message) } + + @Test + fun `setHead to a branch name should create a symbolic reference`() { + git.resolve("HEAD").write("abc123") + val oid = ObjectId("abc") + + val refs = Refs(git) + refs.createBranch("topic", oid) + refs.setHead("topic", oid) + + val headText = git.resolve("HEAD").readText() + assertEquals("ref: refs/heads/topic\n", headText) + } + + @Test + fun `setHead to a non-existing name should store the commit ID oid`() { + git.resolve("HEAD").write("abc123") + val refs = Refs(git) + val oid = ObjectId("abcd") + refs.setHead("topic", oid) + assertEquals("${oid.hex}\n", git.resolve("HEAD").readText()) + } + + @Test + fun `readOidOrSymRef commit ID oid`() { + val oid = ObjectId("abcd") + val refs = Refs(git) + refs.updateHead(oid) + assertEquals(oid, refs.readHead()) + } + + @Test + fun `readOidOrSymRef symbolic ref`() { + val oid = ObjectId("abcd") + + val refs = Refs(git) + refs.createBranch("topic", oid) + refs.setHead("topic", oid) + + assertEquals(oid, refs.readHead()) + } } diff --git a/src/test/kotlin/com/github/arian/gikt/commands/CheckoutTest.kt b/src/test/kotlin/com/github/arian/gikt/commands/CheckoutTest.kt index 31a85ac..a85ddba 100644 --- a/src/test/kotlin/com/github/arian/gikt/commands/CheckoutTest.kt +++ b/src/test/kotlin/com/github/arian/gikt/commands/CheckoutTest.kt @@ -8,7 +8,10 @@ import org.junit.jupiter.api.Test internal class CheckoutTest { private val cmd = CommandHelper() - private val TAB = "\t" + + companion object { + private const val TAB = "\t" + } @BeforeEach fun before() {