Last active
January 15, 2022 14:15
-
-
Save Jytesh/ab5aca4f216d5b07ee40ce06b9c49e56 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
pero