
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 명령어도 해결함!