[Git 만들어보기 - Geet] add, status 명령어 다시 개발

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

Git 만들어보기 - Geet

목록 보기
21/21
post-thumbnail

2024-03-06 ~ 2023-03-14

status를 개발하고 여러 케이스를 다시 한 번 더 생각하다 보니 개발이 미숙한 점이 좀 많았다.

일단 StatusResult 데이터 클래스는 아래와 같았다.

data class StatusObject(
    val newFiles: MutableList<String> = mutableListOf(),
    val modifiedFiles: MutableList<String> = mutableListOf(),
    val deletedFiles: MutableList<String> = mutableListOf()
)

data class StatusResult(
    val staged: StatusObject,
    val unstaged: StatusObject,
    val untracked: List<String>
)

생각해보니 unstaged에는 newFiles가 존재할 수 없음(스테이지에 없다면 untracked임)

→ unstaged의 newFiles는 괜히 메모리 낭비

그리고 삭제된 파일을 얻는 방법을 다시 생각하다 보니 구현한 add 명령어에 여러 문제점이 있었음

지금은 아래처럼 최근 커밋에서 사용자가 입력한 경로와 같은 경로를 가지는 개체를 찾아보고, 없으면 에러, 있으면 삭제 상태로 스테이지에 올림

val objectInLastCommit = indexManager.searchObjectFromLastCommit(filePath)
    ?: throw BadRequest("파일이 존재하지 않습니다.: ${red}${filePath}${resetColor}")
indexManager.addToStage(objectInLastCommit, deleted = true)
indexManager.writeIndex()

그러나 만약 사용자가 입력한 커맨드가 폴더라면?

현재 index 파일에서 최근 커밋에 대한 개체 정보들은 Blob 개체만 저장하는데, 폴더를 입력하면 구할 수 없음

→ 최근 커밋 정보에 Tree 개체도 반영하여 폴더로 따로 저장해야 겠다고 생각을 함

결국 add, status 다시 수정하기로 마음 먹음

일단 add 명령어부터 다시 구현하기로 함

IndexManager부터 수정하기로 하여, index 파일에 저장되는 data class부터 수정하기로 함(커밋 부분)

// geet/command/geetAdd.kt
package geet.command

import ..

fun geetAdd(commandLines: Array<String>): Unit {
    if (commandLines.size != 2) {  // 커맨드 입력 검사
        throw BadRequest("add 명령어에 대한 옵션이 올바르지 않습니다. ${yellow}'add <file-path>'${resetColor} 형식으로 입력해주세요.")
    }

		// 파일 불러오고, 루트(".")폴더 기준으로 상대경로 추출
    val commandFile = File(commandLines[1])
    val filePath = getRelativePathFromRoot(commandFile)

    if (commandFile.exists()) {  // 파일이 존재한다면
		    // 만약 무시되는 파일이라면 에러
        if (ignoreManager.isIgnored(commandFile)) {
            throw BadRequest(".geetignore에 의해 무시되는 파일입니다.: ${red}${filePath}${resetColor}")
        }

				// 파일이라면 스테이지에 추가
        if (commandFile.isFile) {
            addFileToStage(commandFile)
        } else {  //  폴더라면 그 폴더의 모든 파일들 추가
            addAllFilesInDirectory(commandFile)
        }

        indexManager.writeIndex()  // 인덱스 저장
        return
    }

		// 만약 파일이 존재하지 않는다면 최근 커밋에 있는지 검사
		// 검사 결과 개체가 존재하지 않으면 에러
		// blob 개체가 존재하면 삭제 상태로 스테이지에 추가
		// tree 개체가 존재하면(폴더라면) 그 트리 안의 모든 blob 개체 삭제된 상태로 추가
    when (val objectInLastCommit = indexManager.searchObjectFromLastCommit(filePath)
        ?: throw NotFound("파일이 존재하지 않습니다.: ${red}${filePath}${resetColor}")) {
        is GeetBlob -> indexManager.addToStage(objectInLastCommit, deleted = true)
        is GeetTree -> objectInLastCommit.getAllBlobObjectsOfTree().forEach {
            indexManager.addToStage(it, deleted = true)
        }
    }
    indexManager.writeIndex()
}

// 파일을 스테이지에 추가하는 함수
fun addFileToStage(file: File) {
    if (ignoreManager.isIgnored(file)) {
        return
    }

    val filePath = getRelativePathFromRoot(file)
    val samePathObjectInStage = indexManager.searchObjectFromStage(filePath)
    val samePathObjectInLastCommit = indexManager.searchObjectFromLastCommit(filePath)

    if (samePathObjectInStage == null) {  //  만약 스테이지에 없으면
        if (samePathObjectInLastCommit == null) {  // 커밋에도 없으면 새로운 파일로 추가
            val blob = objectManager.saveBlob(file)
            indexManager.addToStage(blob, status = NEW)
        } else if (samePathObjectInLastCommit.content != file.readText()) {
		        // 커밋과 내용이 다르면 수정된 파일로 추가
            val blob = objectManager.saveBlob(file)
            indexManager.addToStage(blob, status = MODIFIED)
        }
        return
    }

		// 만약 스테이지에 있지만 그 내용이 다르면 스테이지의 상태와 같은 상태로 추가
    if (samePathObjectInStage.blob.content != file.readText()) {
        indexManager.removeFromStage(samePathObjectInStage.blob.filePath)
        val blob = objectManager.saveBlob(file)
        indexManager.addToStage(blob, status = samePathObjectInStage.status)
    }
}

// 폴더 안의 모든 파일을 스테이지에 추가하는 함수
fun addAllFilesInDirectory(directory: File) {
    if (ignoreManager.isIgnored(directory)) {  // 무시되는 폴더라면 리턴
        return
    }

		// 최근 커밋과 비교해 폴더에서 삭제된 파일들 스테이지에 추가
    indexManager.addDeletedFilesInDir(getRelativePathFromRoot(directory))
    // 폴더 안의 파일들로 루프
    directory.listFiles()?.forEach { file ->
        if (file.isDirectory) {  // 폴더면 재귀
            addAllFilesInDirectory(file)
        } else {  // 파일이면 추가
            addFileToStage(file)
        }
    }
}

위처럼 코드를 구현하였고, 이에 필요한 함수들을 IndexManager에 따로 추가해주었다.

// geet/manager/IndexManager.kt
package geet.manager

import ..

@Serializable
data class StageObject(
    val blob: GeetBlob,
    val slot: Int,
    val status: StageObjectStatus,
    val lastUpdateTime: String
)

@Serializable
data class IndexData(
    val stageObjects: MutableList<StageObject>,
    val lastCommitObjects: List<GeetObjectWithFile>
)

class IndexManager {

    val indexFile = File(".geet/index")
    private val indexData: IndexData

    init {
        if (indexFile.exists()) {
            indexData = Json.decodeFromString(IndexData.serializer(), indexFile.readText().fromZlibToString())
        } else {
            indexData = IndexData(mutableListOf(), listOf())
        }
    }

    fun getStageObjects(status: StageObjectStatus? = null): List<StageObject> {
        when (status) {
            NEW -> return indexData.stageObjects.filter { it.status == NEW }
            MODIFIED -> return indexData.stageObjects.filter { it.status == MODIFIED }
            DELETED -> return indexData.stageObjects.filter { it.status == DELETED }
            else -> return indexData.stageObjects
        }
    }

		// tree 개체 안의 개체 중 작업 디렉토리에서 삭제된 blob 개체들 얻는 함수
    fun getDeletedObjects(tree: GeetTree? = null): List<GeetBlob> {
        val deletedObjects = mutableListOf<GeetBlob>()  // 결과를 담을 리스트
        var objects: List<GeetObjectWithFile>  // tree 개체 안의 개체들
        if (tree == null) {  // 만약 파라미터로 들어온 tree 개체가 없다면 최근 커밋으로
            objects = indexData.lastCommitObjects
        } else {  // 있다면 tree 개체 안의 개체들
            objects = tree.tree
        }

        objects.forEach {
            if (it is GeetBlob) {  // 개체가 blob이고, 삭제된 상태라면 추가
                if (!File(it.filePath).exists()) {
                    deletedObjects.add(it)
                }
                return@forEach
            }

						// 개체가 tree라면 재귀
            deletedObjects.addAll(getDeletedObjects(it as GeetTree))
        }
        return deletedObjects
    }

		// 스테이지에 개체 추가 함수
    fun addToStage(blob: GeetBlob, status: StageObjectStatus, slot: Int = 0) {
        val stageObject = StageObject(
            blob = blob,
            slot = slot,
            status = status,
            lastUpdateTime = LocalDateTime.now().toString()
        )
        indexData.stageObjects.add(stageObject)
    }
	
		// 최근 커밋과 비교하여 특정 폴더 안의 삭제된 파일들 스테이지에 삭제 상태로 추가
    fun addDeletedFilesInDir(filePath: String) {
        var treeObject: GeetTree
        if (filePath == ".") {  // 만약 "."라면 최근 커밋과 비교
            treeObject = GeetTree(filePath = filePath, tree = indexData.lastCommitObjects)
        } else {  // 개체를 찾아 없거나 tree 개체가 아니라면 리턴
            val lastCommitObject = searchObjectFromLastCommit(filePath) ?: return
            if (lastCommitObject !is GeetTree) return
            treeObject = lastCommitObject
        }
		     // tree 개체로 삭제된 파일 가져옴
        val deletedObjects = getDeletedObjects(treeObject)
        deletedObjects.forEach {  // 스테이지에 추가
            addToStage(it, deleted = true)
        }
    }

    fun removeFromStage(filePath: String) {
        indexData.stageObjects.removeIf { it.blob.filePath == filePath }
    }

    fun searchObjectFromStage(filePath: String): StageObject? {
        return indexData.stageObjects.find { it.blob.filePath == filePath }
    }
		
		// 최근 커밋에서 해당 경로의 tree 또는 blob 개체 찾기
    fun searchObjectFromLastCommit(filePath: String): GeetObjectWithFile? {
        if (filePath.contains(File.separatorChar)) {  // "/"이 있으면 tree를 조회해야 함
            val filePathSplit = filePath.split(File.separatorChar)
            var treeObject: GeetObjectWithFile = indexData.lastCommitObjects.find { it.filePath == filePathSplit[0] }
                ?: return null
            filePathSplit.subList(1, filePathSplit.size - 1).forEach { splitPath ->
                treeObject = (treeObject as GeetTree).tree.find {
                    it.filePath.split(File.separatorChar).last() == splitPath
                } ?: return null
            }
            return (treeObject as GeetTree).tree.find { it.filePath == filePathSplit.last() }
        }

        return indexData.lastCommitObjects.find { it.filePath == filePath }
    }

    fun writeIndex() {
        indexFile.writeText(Json.encodeToString(IndexData.serializer(), indexData).toZlib())
    }
}

이번엔 status 명령어를 다시 구현

status 결과로 다시 생각한 케이스는 다음과 같음


# stage - new
최근 커밋에 없고, 스테이지엔 있음(NEW)

# stage - modified
최근 커밋에 있고, 스테이지에도 있음(삭제 제외) / 최근 커밋에 없고, 스테이지에도 있고, 스테이지와 작업 디렉토리 해시값 다름

# stage - deleted
최근 커밋에 있고, 스테이지에도 있음(삭제)

# unstage - modified
최근 커밋에 있고, 스테이지엔 없고, 최근 커밋과 작업 디렉토리 해시값 다름 / 최근 커밋 상관 없음, 스테이지에 있고, 스테이지와 작업 디렉토리 해시값 다름

# unstage - deleted
최근 커밋에 있고, 스테이지엔 없으며, 작업 디렉토리에서 파일이 삭제됨

# untracked
최근 커밋에 없고, 스테이지에도 없고, 작업 디렉토리엔 존재

그렇게 구현한 코드는 아래와 같다.

// geet/command/geetStatus.kt
package geet.command

import ..

data class StatusResult(  // status 결과를 담은 data class
    val stagedNewFiles: MutableSet<String> = mutableSetOf(),
    val stagedModifiedFiles: MutableSet<String> = mutableSetOf(),
    val stagedDeletedFiles: MutableSet<String> = mutableSetOf(),
    val unstagedModifiedFiles: MutableSet<String> = mutableSetOf(),
    val unstagedDeletedFiles: MutableSet<String> = mutableSetOf(),
    val untrackedFiles: MutableSet<String> = mutableSetOf(),
)

fun geetStatus(commandLines: Array<String>): Unit {
    if (commandLines.size != 1) {
        throw BadRequest("status 명령어에 대한 옵션이 올바르지 않습니다. ${yellow}'status'${resetColor} 명령어는 옵션을 필요로 하지 않습니다.")
    }
    val statusResult = StatusResult()

    val files = getAllFilesInDir(File("."))  // 폴더 내의 모든 파일
    val deletedFileNames = indexManager.getDeletedObjects().map { it.filePath }  // 모든 삭제된 파일 경로
    files.forEach { file ->
        val filePath = getRelativePathFromRoot(file)
        val samePathObjectInStage = indexManager.searchObjectFromStage(filePath)
        val samePathObjectInLastCommit = indexManager.searchObjectFromLastCommit(filePath)

        if (samePathObjectInStage == null) {
            when (true) {  // 각 조건에 따라 추적하지 않는 파일 / 스테이지되지 않는 파일(수정 또는 삭제) 추가
                (samePathObjectInLastCommit == null) -> statusResult.untrackedFiles.add(filePath)
                (samePathObjectInLastCommit.content != file.readText()) -> statusResult.unstagedModifiedFiles.add(filePath)
                (filePath in deletedFileNames) -> statusResult.unstagedDeletedFiles.add(filePath)
                else -> return@forEach
            }
            return@forEach
        }

				// 그 외 스테이지에 있는 경우는 status 프로퍼티를 확인하여 추가
        when (samePathObjectInStage.status) {
            NEW -> statusResult.stagedNewFiles.add(filePath)
            MODIFIED -> statusResult.stagedModifiedFiles.add(filePath)
            DELETED -> statusResult.stagedDeletedFiles.add(filePath)
        }
    }

    printStatusResult(statusResult)
}

// 결과 출력 함수
fun printStatusResult(statusResult: StatusResult): Unit {
    println("** 스테이지된 변경 사항 **${green}")
    statusResult.stagedNewFiles.forEach { println("  새로운 파일: $it") }
    statusResult.stagedModifiedFiles.forEach { println("  수정된 파일: $it") }
    statusResult.stagedDeletedFiles.forEach { println("  삭제된 파일: $it") }
    println(resetColor)

    println("** 스테이지되지 않은 변경 사항 **${yellow}")
    statusResult.unstagedModifiedFiles.forEach { println("  수정된 파일: $it") }
    statusResult.unstagedDeletedFiles.forEach { println("  삭제된 파일: $it") }
    println(resetColor)

    println("** 추적하지 않는 파일 **${red}")
    statusResult.untrackedFiles.forEach { println("  $it") }
}

일단 아래와 같이 잘 진행되는 모습 ㅎ

4개의 댓글

comment-user-thumbnail
2024년 3월 18일

다음 프로젝트부터는 geet으로 버전관리 해주시죠 🙌

1개의 답글
comment-user-thumbnail
2024년 3월 20일

git을 만들 수도 있군요. 갈 길이 멉니다!

1개의 답글