[Git 만들어보기 - Geet] geet reset 명령어로 스테이지, 작업 디렉토리 초기화 및 HEAD 조정하기

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

2024-02-15 구현

reset 명령어는 보통

reset <옵션> <커밋>

의 형태로 이루어진다.

옵션에는 —soft, —mixed, —hard가 대표적으로 쓰이며, 커밋 위치에는 초기화할 커밋 해시값 자체 또는 가리키는 참조(HEAD…) 등을 넣을 수 있다.

그래서 일단 커맨드라인을 읽어 옵션을 올바르게 입력했는지, 어떤 옵션을 입력했는지 확인하는 절차를 가졌다.

// geet/commands/porcelain/geetReset.kt
package geet.commands.porcelain

import geet.exceptions.BadRequest

data class GeetResetOptions(
    var option: String = "--mixed",  // 기본값 --mixed
    var commitHash: String = "HEAD"  // 기본값 HEAD
)

fun geetReset(commandLines: Array<String>) {
    val geetResetOptions = getGeetResetOptions(commandLines)
    println(geetResetOptions)
}

fun getGeetResetOptions(commandLines: Array<String>): GeetResetOptions {
    if (commandLines.size == 1) {  // reset만 입력한 경우는 기본값으로
        return GeetResetOptions()
    }

    val option = commandLines[1]  // option이 아래 3개가 아니면 에러
    if (option != "--soft" && option != "--mixed" && option != "--hard") {
        throw BadRequest("옵션이 올바르지 않습니다.")
    }

    if (commandLines.size == 2) {  // reset --soft같은 경우에는 옵션만 지정
        return GeetResetOptions(option = option)
    }

    if (commandLines.size == 3) {  // 다 입력한 경우에는 커밋 입력 옵션을 해시로 바꿔서 리턴
				// 에) ek23 -> ek23...3j4 처럼 앞에만 짧게 입력한 해시도 40자의 전체 해시로 바꿔서 리턴
				// HEAD, HEAD^^와 같이 참조로 된 커밋도 40자의 전체 해시로 바꿔서 리턴
				val commitHash = changeToFullHash(commandLines[2])
        return GeetResetOptions(option = option, commitHash = commitHash)
    }

    throw BadRequest("옵션이 올바르지 않습니다.")
}

여기서 사용된 changeToFullHash 함수는 아래 resetUtil.kt에 작성하였다.

// geet/utils/porcelainutil/resetUtil.kt
package geet.utils.commandutil.porcelainutil

import geet.exceptions.BadRequest
import geet.utils.GEET_OBJECTS_DIR_PATH
import java.io.File

// 해시값 일부 또는 참조로 해당 커밋의 해시값 전체를 얻는 함수
fun changeToFullHash(commitString: String): String {
    if (startsWithHeadRef(commitString)) {  // HEAD, HEAD^^^ 등 HEAD를 통한 참조인 경우
        var commitHash = getCurrentRefCommitHash()

        val carrotCount = countCarrot(commitString)  // ^의 수 만큼 부모 커밋 해시값 가져오기
        for (i in 0 until carrotCount) {
						// 커밋으로부터 부모 커밋 해시값을 얻어오는 함수
            commitHash = getParentCommitFromCommitHash(commitHash)
        }

        return commitHash
    }

    if (isHash(commitString)) {  // 해시값 일부인 경우
        val dirName = commitString.substring(0, 2)  // 앞의 두자리는 폴더 이름
        val dir = File("${GEET_OBJECTS_DIR_PATH}/$dirName")
        if (!dir.exists()) {
            throw BadRequest("올바르지 않은 커밋 해시 또는 참조입니다.")
        }

        dir.listFiles()?.forEach { file ->  // 폴더의 파일 중 해시값으로 시작하는 파일이 있으면 리턴
            if (file.name.startsWith(commitString)) {
                return dirName + file.name
            }
        }
    }

    throw BadRequest("올바르지 않은 커밋 해시 또는 참조입니다.")  // 위의 경우가 아니라면 오류
}

fun startsWithHeadRef(commitString: String): Boolean {  // HEAD 참조 형식의 문자열인지
    val pattern = Regex("^HEAD\\^*$")
    return commitString.matches(pattern)
}

fun countCarrot(string: String): Int {  // 문자열에 ^가 몇개 있는지
    val pattern = Regex("\\^")
    return pattern.findAll(string).count()
}

fun isHash(hash: String): Boolean {  // 해시값의 일부 형식인지
    val pattern = Regex("^[0-9a-f]{4,40}$")
    return hash.matches(pattern)
}

fun getParentCommitFromCommitHash(commitHash: String): String {  // 특정 커밋의 부모 커밋 찾기
    val dirName = commitHash.substring(0, 2)
    val fileName = commitHash.substring(2)
    val commitFile = File("${GEET_OBJECTS_DIR_PATH}/${dirName}/${fileName}")
    if (!commitFile.exists()) {
        throw NotFound("커밋 파일을 찾을 수 없습니다.")
    }

    val commitContents = commitFile.readText()
    val commitContentsDecompressed = decompressFromZlib(commitContents)
    val commitContentsSplit = commitContentsDecompressed.split("\n")
		if (commitContentsSplit[1].split(" ")[0] == "parent") {
        return commitContentsSplit[1].split(" ")[1]
    }

    throw BadRequest("부모 커밋을 찾을 수 없습니다.")
}

위와 같은 과정으로 reset 명령어에 대해 입력한 옵션과 가리키는 커밋을 data class인 GeetResetOptions에 담아서 보내준다.

이제 해당 옵션에 대하여 명령어 처리를 구현해주었다.

// geet/utils/commandutil/porcelainutil/resetUtil.kt
fun reset(geetResetOptions: GeetResetOptions) {
    when (geetResetOptions.option) {
        "--soft" -> softReset(geetResetOptions.commitHash)
        "--mixed" -> mixedReset(geetResetOptions.commitHash)
        "--hard" -> hardReset(geetResetOptions.commitHash)
    }
}

fun softReset(commitHash: String) {  // 참조 커밋만 변경
    editCurrentRefContent(commitHash)
}

fun mixedReset(commitHash: String) {  // 참조 커밋 변경, 스테이지 초기화
    editCurrentRefContent(commitHash)

    indexManager.getIndexFileData().stagingArea.clear()
    indexManager.getIndexFileData().lastCommitHash = commitHash
    indexManager.writeIndexFile()
}

fun hardReset(commitHash: String) {  // 참조 커밋 변경, 스테이지와 작업 디렉토리 초기화
    editCurrentRefContent(commitHash)

    indexManager.getIndexFileData().stagingArea.clear()
    indexManager.getIndexFileData().lastCommitHash = commitHash
    indexManager.writeIndexFile()

    val workingDirectory = File(".")  // .geet을 제외한 모든 파일 삭제
    workingDirectory.listFiles()?.forEach { file ->
        if (file.name != ".geet") {
            file.deleteRecursively()
        }
    }

    val commitObjects = getObjectsFromCommit(commitHash)  // 커밋에서 오브젝트들을 가져옴
    commitObjects.forEach { restoreObject(it as GeetBlob) }  // 그 오브젝트들로 파일 복원
}

fun restoreObject(blobObject: GeetBlob) {
    val path = getRelativePath(blobObject.path)
    val content = blobObject.content

		// 부모 폴더가 있다면 부모 폴더도 다시 만들어 줌
    val parentPath = path.split("/").subList(0, path.split("/").size - 1)
    if (parentPath.isNotEmpty()) {  // 경로에 부모 폴더가 있다면
        parentPath.forEachIndexed { index, _ ->
            val parentPathString = parentPath.subList(0, index + 1).joinToString("/")
            val parentDir = File(parentPathString)
            if (!parentDir.exists()) {  // 부모 폴더를 생성해 줌
                parentDir.mkdir()
            }
        }
    }

    val file = File(path)
    if (!file.exists()) {
        file.createNewFile()
    }
    file.writeText(content)
}

이렇게 해서 reset 명령어도 해결함!

0개의 댓글