Skip to content

Instantly share code, notes, and snippets.

@Jytesh
Last active January 15, 2022 14:15
Show Gist options
  • Save Jytesh/ab5aca4f216d5b07ee40ce06b9c49e56 to your computer and use it in GitHub Desktop.
Save Jytesh/ab5aca4f216d5b07ee40ce06b9c49e56 to your computer and use it in GitHub Desktop.
package gitinternals
import java.io.File
import java.io.FileInputStream
import java.util.zip.InflaterInputStream
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
typealias Hash = String
fun Hash.getGitObjectPath(gitDirectory: String): String {
this.validateHash()
return "$gitDirectory/objects/${this.take(2)}/${this.drop(2)}"
}
fun log(message: String) {
// System.err is not checked by tests, which is good for logging
System.err.println(message)
}
fun readLine(message: String): String? {
println(message)
return readLine()
}
interface GitObject {
val rawContent: ByteArray
val length: Int
}
enum class UserType {
COMMITTER, AUTHOR
}
class UserInfo(
val name: String, val email: String, val type: UserType, val time: LocalDateTime, val offset: ZoneOffset
) {
companion object {
private val timeFormat: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss xxx")
}
private val timeStamp = time.atOffset(offset).format(timeFormat)
private val timeStampType = when (type) {
UserType.COMMITTER -> "commit timestamp:"
UserType.AUTHOR -> "original timestamp:"
}
override fun toString(): String {
return "$name $email $timeStampType $timeStamp"
}
}
class FileInfo(private val permission: Int, val fileName: String, val properHash: Hash) {
override fun toString(): String {
return "$permission $properHash $fileName"
}
}
class Commit(
override val rawContent: ByteArray, override val length: Int
) : GitObject {
private var commitParser: CommitParser? = CommitParser()
val tree: String by lazy {
commitParser?.tree ?: error("CommitParser failed to parse tree")
}
val parents: List<String> by lazy {
commitParser!!.parents.toList()
}
private val authorInfo: UserInfo by lazy {
commitParser?.authorInfo ?: error("CommitParser failed to parse author")
}
val committerInfo: UserInfo by lazy {
commitParser?.committerInfo ?: error("CommitParser failed to parse committer")
}
val commitMessage: String by lazy {
commitParser?.message ?: error("CommitParser failed to parse commit message")
}
init {
commitParser!!.parse()
tree
parents
authorInfo
committerInfo
commitMessage
commitParser = null
}
override fun toString(): String {
val parentsString = if (parents.isNotEmpty()) {
parents.joinToString(" | ", "parents: ", "\n")
} else {
""
}
return "*COMMIT*\n" + "tree: $tree\n" + parentsString + "author: $authorInfo\ncommitter: " + "$committerInfo\n" + "commit message:\n" + commitMessage
}
private inner class CommitParser {
var tree: String? = null
var parents: MutableList<String> = mutableListOf()
var authorInfo: UserInfo? = null
var committerInfo: UserInfo? = null
var message: String? = null
var currentIndex = 0
fun parse() {
var parsingState = 0
currentIndex = 0
while (currentIndex < length) {
when (parsingState) {
0 -> {
currentIndex += parseTree()
parsingState = 1
}
1 -> {
if (rawContent[currentIndex].toInt().toChar() == 'p') {
currentIndex += parseParent()
} else {
parsingState = 2
}
}
2 -> {
currentIndex += parseUserInfo(UserType.AUTHOR)
parsingState = 3
}
3 -> {
currentIndex += parseUserInfo(UserType.COMMITTER)
parsingState = 4
}
4 -> {
parseMessage()
return
}
}
}
error("unexpected end of commit data from file, length declared on header does not match file length")
}
private fun parseTree(): Int {
val lineSize = 46
val treeLine =
rawContent.drop(currentIndex).take(lineSize).map(Byte::toInt).map(Int::toChar).joinToString("")
if (treeLine.matches("^tree [0-9a-f]{40}\\n$".toRegex())) {
tree = treeLine.substring(5, lineSize - 1)
} else {
error("wrong format for commit while trying to parse tree\n$treeLine")
}
return lineSize
}
private fun parseParent(): Int {
val lineSize = 48
val parentLine =
rawContent.drop(currentIndex).take(lineSize).map(Byte::toInt).map(Int::toChar).joinToString("")
if (parentLine.matches("^parent [0-9a-f]{40}\\n$".toRegex())) {
parents.add(parentLine.substring(7, lineSize - 1))
} else {
error("wrong format for commit while trying to parse parent\n$parentLine")
}
return lineSize
}
private fun parseUserInfo(type: UserType): Int {
val userInfoLine = rawContent.drop(currentIndex).map(Byte::toInt).map(Int::toChar).takeWhile { it != '\n' }
.joinToString("", "", "\n")
val regex = when (type) {
UserType.COMMITTER -> "^committer (\\w+) <([\\w.-]+@[\\w.-]+)> (\\d+) ([+-]\\d{4})\n$".toRegex()
UserType.AUTHOR -> "^author (\\w+) <([\\w.-]+@[\\w.-]+)> (\\d+) ([+-]\\d{4})\n$".toRegex()
}
val matchResult = regex.matchEntire(userInfoLine)
val groupValues = matchResult?.groupValues
?: error("wrong format for commit while trying to parse ${type.name.lowercase()} info\n$userInfoLine")
val offsetTime = ZoneOffset.of(groupValues[4])
val time = LocalDateTime.ofEpochSecond(groupValues[3].toLong(), 0, offsetTime)
when (type) {
UserType.COMMITTER -> committerInfo = UserInfo(
name = groupValues[1], email = groupValues[2], type = type, time = time, offset = offsetTime
)
UserType.AUTHOR -> authorInfo = UserInfo(
name = groupValues[1], email = groupValues[2], type = type, time = time, offset = offsetTime
)
}
return userInfoLine.length
}
fun parseMessage() {
message = rawContent.drop(currentIndex + 1).map(Byte::toInt).map(Int::toChar).joinToString("")
}
}
}
class Tree(override val rawContent: ByteArray, override val length: Int) : GitObject {
private var treeParser: TreeParser? = TreeParser()
val listFileInfo: List<FileInfo> by lazy {
treeParser!!.listFileInfo
}
init {
treeParser!!.parse()
listFileInfo
treeParser = null
}
override fun toString(): String {
return "*TREE*\n" + listFileInfo.joinToString("\n", transform = FileInfo::toString)
}
private inner class TreeParser() {
val listFileInfo = mutableListOf<FileInfo>()
var currentPermission = 0
var currentFileName = ""
var currentImproperHash = ""
var currentProperHash = ""
var currentIndex = 0
fun parse() {
var parsingState = 0
while (currentIndex < length) {
when (parsingState) {
0 -> {
currentIndex += parsePermission()
parsingState = 1
}
1 -> {
currentIndex += parseFileName()
parsingState = 2
}
2 -> {
currentIndex += parseHash()
listFileInfo.add(
FileInfo(
currentPermission,
currentFileName,
currentProperHash
)
)
parsingState = 0
}
}
}
}
fun parsePermission(): Int {
val whiteSpace = 32.toByte()
val permissionAsString = rawContent
.drop(currentIndex)
.takeWhile { it != whiteSpace }
.map(Byte::toInt)
.map(Int::toChar)
.joinToString("")
val permission = permissionAsString.toIntOrNull() ?: error("incorrect permission value while parsing tree:")
currentPermission = permission
return permissionAsString.length + 1
}
private fun parseFileName(): Int {
val nullByte = 0.toByte()
val fileName = rawContent
.drop(currentIndex)
.takeWhile { it != nullByte }
.map(Byte::toInt)
.map(Int::toChar)
.joinToString("")
currentFileName = fileName
return fileName.length + 1
}
private fun parseHash(): Int {
val hashSize = 20
val content = rawContent
.drop(currentIndex)
.take(hashSize)
val improperHash = content
.joinToString("") { "%X".format(it) }
.lowercase()
val properHash = content
.joinToString("") { "%02X".format(it) }
.lowercase()
currentImproperHash = improperHash
currentProperHash = properHash
return hashSize
}
}
}
class Blob(override val rawContent: ByteArray, override val length: Int) : GitObject {
private val content by lazy {
rawContent.map(Byte::toInt).map(Int::toChar).joinToString("")
}
override fun toString(): String {
return "*BLOB*\n$content"
}
}
interface GitCommand {
val gitDirectory: String
fun execute()
}
object UI {
fun getCommandFromInput(): String {
return readLine("Enter command:")?.lowercase() ?: error("failed to receive command")
}
fun getGitDirectoryFromInput(): String {
return readLine("Enter .git directory location:") ?: error("failed to receive directory location")
}
fun getGitObjectHashFromInput(): Hash {
val hash: Hash = readLine("Enter git object hash:") ?: error("failed to receive hash")
return hash.also(Hash::validateHash)
}
fun getGitCommitHashFromInput(): Hash {
val hash: Hash = readLine("Enter commit-hash:") ?: error("failed to receive hash")
return hash.also(Hash::validateHash)
}
fun getBranchFromInput(): String {
return readLine("Enter branch name:") ?: error("failed to receive branch name")
}
}
fun getInflatedRawFileInput(filepath: String): ByteArray {
val file = File(filepath).also {
if (it.exists().not()) error("could not find file with path ${it.path}")
}
val inflaterInputStream = InflaterInputStream(FileInputStream(file))
return inflaterInputStream.readAllBytes()
}
class CatFile(override val gitDirectory: String) : GitCommand {
override fun execute() {
val hash = UI.getGitObjectHashFromInput()
val filePath = "$gitDirectory/objects/${hash.take(2)}/${hash.drop(2)}"
val rawInput = getInflatedRawFileInput(filePath)
val gitObjectParser = GitObjectParser(rawInput)
log("raw content:")
log(gitObjectParser.parsedInput)
val content: GitObject = gitObjectParser.content
println(content)
}
}
class ListBranches(override val gitDirectory: String) : GitCommand {
override fun execute() {
val currentBranch = getCurrentBranch(gitDirectory)
val headsFolder = getHeadsFolder(gitDirectory)
val branches: String =
headsFolder.listFiles { file -> file.isFile }?.map(File::getName)?.sorted()?.joinToString("\n") { name ->
if (name == currentBranch) "* $name" else " $name"
} ?: error("error while retrieving branches")
log("result:\n$branches")
println(branches)
}
}
class Log(override val gitDirectory: String) : GitCommand {
override fun execute() {
val currentBranch: String = UI.getBranchFromInput()
val headsFolder = getHeadsFolder(gitDirectory)
val headFile = headsFolder.resolve(currentBranch).also {
if (it.exists().not()) error("could not find head file at ${it.path}")
else if (it.isFile.not()) error("head file at ${it.path} is not a file")
}
val headFileContent = headFile.bufferedReader().readLines().also {
if (it.size != 1) error("invalid head file content at ${headFile.path}")
(it[0]).validateHash()
}
val headCommitHash = headFileContent[0]
val resultLog = buildLog(headCommitHash)
log(resultLog)
println(resultLog)
}
private tailrec fun buildLog(currentCommit: String, accumulatedLog: StringBuilder = StringBuilder()): String {
val commitObject = appendToLogReturnCommitObject(currentCommit, accumulatedLog)
return if (commitObject.parents.isEmpty()) {
accumulatedLog.toString()
} else if (commitObject.parents.size > 1) {
commitObject.parents.drop(1).reversed().forEach {
appendToLogReturnCommitObject(it, accumulatedLog, isMerged = true)
}
buildLog(commitObject.parents[0], accumulatedLog)
} else {
buildLog(commitObject.parents[0], accumulatedLog)
}
}
private fun appendToLogReturnCommitObject(
currentCommit: String, accumulatedLog: StringBuilder, isMerged: Boolean = false
): Commit {
val path = "$gitDirectory/objects/${currentCommit.take(2)}/${currentCommit.drop(2)}"
val rawInput = getInflatedRawFileInput(path)
val gitObjectParser = GitObjectParser(rawInput)
val commitObject = gitObjectParser.content as Commit
val mergedString = if (isMerged) " (merged)" else ""
val toLog =
"Commit: $currentCommit$mergedString\n" + "${commitObject.committerInfo}\n" + "${commitObject.commitMessage}\n"
accumulatedLog.append(toLog)
return commitObject
}
}
class CommitTree(override val gitDirectory: String) : GitCommand {
override fun execute() {
val commitHash = UI.getGitCommitHashFromInput()
val path = commitHash.getGitObjectPath(gitDirectory)
val rawInput = getInflatedRawFileInput(path)
val gitObjectParser = GitObjectParser(rawInput)
val commitObject = if (gitObjectParser.content is Commit) {
gitObjectParser.content as Commit
} else {
error("was not a commit the object with hash $commitHash")
}
val response = buildTree(commitObject.tree)
log(response)
println(response)
}
private fun buildTree(
currentHash: String,
accumulatedResponse: StringBuilder = StringBuilder(),
currentFolder: String = ""
): String {
val rawInput = getInflatedRawFileInput(currentHash.getGitObjectPath(gitDirectory))
val gitObjectParser = GitObjectParser(rawInput)
val treeObject: Tree = if (gitObjectParser.content is Tree) {
gitObjectParser.content as Tree
} else {
error("was not a tree the object with hash $currentHash")
}
val filesList = treeObject.listFileInfo.map {
val rawInput = getInflatedRawFileInput(it.properHash.getGitObjectPath(gitDirectory))
it to GitObjectParser(rawInput)
}
filesList.filter { it.second.type == "blob" }.forEach {
accumulatedResponse.append("$currentFolder${it.first.fileName}\n")
}
filesList.filter { it.second.type == "tree" }.forEach {
buildTree(it.first.properHash, accumulatedResponse, "$currentFolder${it.first.fileName}/")
}
return accumulatedResponse.toString()
}
}
class GitObjectParser(private val rawInput: ByteArray) {
private val firstLine by lazy {
rawInput.takeWhile { it != 0.toByte() }
.map(Byte::toInt)
.map(Int::toChar)
.joinToString("")
}
val type by lazy {
firstLine.takeWhile { it != ' ' }
}
private val length by lazy {
firstLine.substringAfter("$type ")
.toIntOrNull() ?: error("header does not contain length")
}
private val rawContent by lazy {
rawInput.dropWhile { it != 0.toByte() }.drop(1).toByteArray()
}
val parsedInput by lazy {
rawInput.map(Byte::toInt)
.map(Int::toChar)
.joinToString("")
}
val content: GitObject by lazy {
when (type) {
"blob" -> Blob(rawContent, length)
"commit" -> Commit(rawContent, length)
"tree" -> Tree(rawContent, length)
else -> error("invalid type on header\n$type")
}
}
}
fun getCurrentBranch(gitDirectory: String): String {
val headFile = File("$gitDirectory/HEAD").also {
if (it.exists().not()) error("could not locate file ${it.path}")
}
val headContent = headFile.bufferedReader().readLines().joinToString("")
log("head content: $headContent")
if (headContent.startsWith("ref: refs/heads/").not()) error("invalid HEAD file content")
return headContent.substringAfter("ref: refs/heads/")
}
fun getHeadsFolder(gitDirectory: String): File {
return File("$gitDirectory/refs/heads").also {
if (it.exists().not()) error("could not locate file ${it.path}")
if (it.isDirectory.not()) error("heads file at path ${it.path} should be a folder")
}
}
fun Hash.validateHash() {
if (this.matches("^[0-9a-f]{40}$".toRegex()).not()) error("invalid hash: $this")
}
fun main() {
val gitDirectory = UI.getGitDirectoryFromInput()
val command = UI.getCommandFromInput()
controller(command, gitDirectory)
}
private fun controller(commandStr: String, gitDirectory: String) {
log("command: $commandStr")
log("gitDirectory: $gitDirectory")
when (commandStr) {
"cat-file" -> CatFile(gitDirectory)
"commit-tree" -> CommitTree(gitDirectory)
"list-branches" -> ListBranches(gitDirectory)
"log" -> Log(gitDirectory)
else -> error("unknown command $commandStr")
}.also(GitCommand::execute)
}
@BlitzJB
Copy link

BlitzJB commented Jan 13, 2022

pero

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment