[Git 만들어보기 - Geet] geet hash-object 명령어로 Blob 개체 생성

송준섭 Junseop Song·2024년 3월 18일
post-thumbnail

2024-01-09 구현

geet hash-object 명령어는 여러 옵션들을 받을 수 있음

원래 parseCommandLine으로 입력된 명령어들의 옵션들도 다 정리를 하려고 했음

그러나 옵션마다 다음으로 입력되는 값이 필요한 옵션도 있고 아닌 것도 있고 복잡해서 그냥 사용하던 parseCommandLine 함수를 없애고 args 자체를 넘겨주기로 수정함

// Main.kt
import geet.processGeet

fun main(commandLines: Array<String>) {
	processGeet(commandLines)
}

그리고 다음 과정에서 그냥 args 자체를 읽어서 그에 따라 처리하기로 결정

processGeet도 다음과 같이 수정함

// geet/ProcessGeet.kt
package geet

import ..

fun processGeet(commandLines: Array<String>): Unit {
    if (commandLines.isEmpty()) {  // 입력된 값이 아무것도 없는 경우 guideGeet() 호출하고 리턴
        guideGeet()
        return
    }

    when (commandLines[0]) {  
        "init" -> geetInit()
        "hash-object" -> geetHashObject(commandLines)  // 입력된 커맨드라인들 자체를 넘겨줌
        else -> throw NotSupportedCommand(commandLines[0])
    }
}

hash-object 명령어를 처리하는 geetHashObject 함수는 아래와 같음

// geet/commands/plumbing/GeetHashObject.kt
package geet.commands.plumbing

import ..

data class GeetHashObjectOptions(  // 사용자가 입력한 커맨드라인으로부터 받아온 옵션 정보 저장하는 데이터 클래스
    var type: String = "blob",  // 아무것도 입력하지 않으면 기본값 blob
    var write: Boolean = false,  // 아무것도 입력하지 않으면 기본값 false
    var path: String = ""  // 아무것도 입력하지 않으면 ""
)

fun geetHashObject(commandLines: Array<String>) {
    val options = getHashObjectOptions(commandLines)  // 커맨드라인으로부터 요청 옵션 가져오기
    createHashObject(options)  // 옵션을 토대로 개체 생성
}

fun getHashObjectOptions(commandLines: Array<String>): GeetHashObjectOptions {
    val options = GeetHashObjectOptions()

    var index: Int = 1
    while (index < commandLines.size) {
        when (commandLines[index]) {
            "-t" -> {
								// 만약 사용자가 요청한 개체 타입(-t 다음에 입력되어야 함)이 blob, tree, commit, tag가 아닌 경우
                if (!isGeetObjectType(commandLines[index + 1])) {
                    println("'-t' 옵션에 대하여 올바른 개체 타입이 지정되지 않았습니다.: ${commandLines[index + 1]}")
                    // TODO: 에러 처리
                }

                options.type = commandLines[index + 1]  // 개체 타입
                index += 2  // 개체 타입까지 건너 뛰어 인덱스 2개 증가
            }
            "-w" -> {
                options.write = true  // 저장 옵션 설정
                index += 1
            }
            else -> {
								// 만약 이미 파일명이 지정이 되었다면
                if (options.path != "") {
                    println("지정할 수 없는 옵션입니다.: ${commandLines[index]}")
                    // TODO: 에러 처리
                }

                options.path = commandLines[index]  // 파일명 지정
                index += 1
            }
        }
    }

    if (options.path == "") {  // 파일명이 입력이 되지 않은 경우
        println("파일 경로가 지정되지 않았습니다.")
        // TODO: 에러 처리
    }

    return options  // 옵션 반환
}

여기서 사용한 isGeetObjectType 함수나 createHashObject 함수는 다시 쓰일 가능성이 있어 GeetUtil.kt에 저장해두었다.

// geet/util/GeetUtil.kt
package geet.util

import ..

// 해시화에 쓰일 인스턴스
val messageDigest: MessageDigest = MessageDigest.getInstance("SHA-1")

// 문자열이 올바른 개체 타입인지
fun isGeetObjectType(type: String): Boolean {
    val typeLowerCase = type.lowercase()
    return typeLowerCase == "blob" ||
        typeLowerCase == "tree" ||
        typeLowerCase == "commit" ||
        typeLowerCase == "tag"
}

// 개체 생성 함수
fun createHashObject(options: GeetHashObjectOptions) {
    val file = File(options.path)
    if (!file.exists()) {  // 만약 파일이 존재하지 않다면
        println("파일이 존재하지 않습니다.: ${options.path}")
        // TODO: 에러 처리
    }

    val hashString = getHashString(options, file)  // 파일 내용 및 개체 타입으로 해시(SHA-1) 생성
    val directoryName = hashString.substring(0, 2)  // 저장될 폴더 이름
    val fileName = hashString.substring(2)  // 저장될 파일 이름
    val compressedContents = compressToZlib(file.readText())  // 파일 내용 zlib으로 압축

    File(".geet/objects/$directoryName").mkdirs()  // 폴더 생성
    File(".geet/objects/$directoryName/$fileName").writeText(compressedContents)  // 파일 생성

    println(hashString)
    println("개체가 저장되었습니다. : .geet/objects/$directoryName/$fileName")
}

// SHA-1 값을 리턴하는 함수
fun getHashString(options: GeetHashObjectOptions, file: File): String {
    val content = file.readText()
    val header = "${options.type} ${content.length}\u0000"  // ex) "blob 12\u0000", \u0000은 널문자
    val store = header + content

    val hash = messageDigest.digest(store.toByteArray())
    return hash.joinToString("") {
        String.format("%02x", it)
    }
}

// 문자열 zlib으로 압축하는 함수
fun compressToZlib(contents: String): String {
    val inputData = contents.toByteArray()
    val outputStream = ByteArrayOutputStream()
    val deflater = Deflater()
    val deflaterOutputStream = DeflaterOutputStream(outputStream, deflater)

    deflaterOutputStream.write(inputData)
    deflaterOutputStream.close()

		// 원래 ByteArray로 압축, 압축 해제를 하지만 파일 내용으로 저장해야 하므로 String으로 변환
		// 그냥 toString()을 사용하면 내용 손실의 우려가 있어 Base64 Encoder 및 Decoder를 이용하여 변환
    return Base64.getEncoder().encodeToString(outputStream.toByteArray())
}

// zlib 압축 해제하는 함수
fun decompressFromZlib(zlibContents: String): String {
    val decodedByteArray = Base64.getDecoder().decode(zlibContents)
    val inputStream = ByteArrayInputStream(decodedByteArray)
    val inflater = Inflater()
    val inflaterInputStream = InflaterInputStream(inputStream, inflater)
    val outputStream = ByteArrayOutputStream()

    val buffer = ByteArray(1024)
    var length: Int
    while (inflaterInputStream.read(buffer).also { length = it } > 0) {
        outputStream.write(buffer, 0, length)
    }

    return outputStream.toString()
}

test.txt를 저장하니 잘 되는 모습!

0개의 댓글