Skip to content

Commit

Permalink
Linking HEAD on checkout - symbolic references
Browse files Browse the repository at this point in the history
  • Loading branch information
arian committed Aug 16, 2020
1 parent 337815f commit f07e42a
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 18 deletions.
61 changes: 48 additions & 13 deletions 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) {
Expand All @@ -11,30 +12,61 @@ 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)
.asSequence()
.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)) {
Expand All @@ -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()
}
Expand Down
6 changes: 3 additions & 3 deletions src/main/kotlin/com/github/arian/gikt/commands/Checkout.kt
Expand Up @@ -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()
Expand Down
46 changes: 45 additions & 1 deletion src/test/kotlin/com/github/arian/gikt/RefsTest.kt
Expand Up @@ -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
Expand All @@ -56,4 +59,45 @@ class RefsTest(private val fileSystemProvider: FileSystemExtension.FileSystemPro
val e = assertThrows<Refs.InvalidBranch> { 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())
}
}
Expand Up @@ -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() {
Expand Down

0 comments on commit f07e42a

Please sign in to comment.