[Git 만들어보기 - Geet] geet status 명령어로 현재 상태 출력하기

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

2024-01-30 구현

status의 종류는 다음과 같이 생각을 해서 선택해보았다.

내용이 같냐 틀리냐는 SHA-1 값으로 비교

  1. 커밋에 존재하느냐?
  2. 스테이지에 있냐?
  3. 커밋과 내용이 같냐?
  4. 스테이지와 내용이 같냐?

커존, 스존, 커같, 스같 → 커같인데 스존일 수 없음

커존, 스존, 커같, 스다 → 커같인데 스존일 수 없음

커존, 스존, 커다, 스같 → 파일 수정 후 스테이지에 올리고 더 이상 수정하지 않은 경우

커존, 스존, 커다, 스다 → 파일 수정 후 스테이지에 올렸는데 다시 수정한 경우

커존, 스없, 커같, 스같 → 최근 커밋에서 변경되지 않은 파일 → add, status 소용 없음

커존, 스없, 커같, 스다 → 커같인데 스없이면서 스다일 수 없음 (커같이라면 수정된 파일로 인식되지 않아 스없이면 아무 변경 사항도 인식되지 않음)

커존, 스없, 커다, 스같 → 스없인데 커다이면서 스같일 수 없음 (커다라면 수정된 파일로 인식이 되어야 함)

커존, 스없, 커다, 스다 → 파일 수정 후 아직 스테이지에 올리지 않은 경우

커없, 스존, 커같, 스같 → 커같인데 스존일 수 없음

커없, 스존, 커같, 스다 → 커같인데 스존일 수 없음

커없, 스존, 커다, 스같 → 새로운 파일을 추가하여 스테이지에 올리고 더 이상 수정하지 않은 경우

커없, 스존, 커다, 스다 → 새로운 파일을 추가하여 스테이지에 올린 후 다시 수정한 경우

커없, 스없, 커같, 스같 → 커없인데 커같일 수 없음

커없, 스없, 커같, 스다 → 커없인데 커같일 수 없음

커없, 스없, 커다, 스같 → 없는 파일

커없, 스없, 커다, 스다 → 새로운 파일을 추가하고 아직 스테이지에 올리지 않은 경우

여기에 삭제된 파일에 대한 상태 존재

그래서 선정한 상태는 다음과 같다.

  1. 새 파일

    최근 커밋에 존재하지 않고 Staging Area에는 존재하는 경우

    • 스테이지에만 있는 경우
    • 스테이지에 올린 후 다시 수정한 경우
  2. 추적하지 않는 파일

    최근 커밋에 존재하지 않고 Staging Area에도 존재하지 않는 경우

  3. 수정된 파일

    최근 커밋에 존재하지만 그 내용(SHA-1 해시값)이 다른 경우

    • 스테이지에만 있는 경우
    • 스테이지에 올린 후 다시 수정한 경우
    • 아직 스테이지에 올리지 않은 경우
  4. 삭제된 파일

    최근 커밋에 존재하지만 현재 작업 디렉토리에는 존재하지 않는 경우

    • 스테이지에 삭제 사항을 추가해준 경우
    • 스테이지에 삭제 사항을 추가하지 않은 경우
  5. 변경되지 않은 파일

    최근 커밋에 존재하지만 그 내용이 같은 경우 → add를 해도 올라가지 않으며, status에 출력되지 않음

이것을 기반으로 data class를 생성

// geet/utils/commandutil/statusUtil.kt
data class StagingData(
    val stagedFiles: MutableList<String> = mutableListOf(),
    val unstagedFiles: MutableList<String> = mutableListOf(),
)

data class GeetStatusResult(
    val modifiedFiles: StagingData = StagingData(),
    val newFiles: StagingData = StagingData(),
    val removedFiles: StagingData = StagingData(),
    val untrackedFiles: MutableList<String> = mutableListOf(),
)

그리고 구현한 내용은 다음과 같다.

// geet/commands/porcelain/geetStatus.kt
package geet.commands.porcelain

import ..

fun geetStatus(commandLines: Array<String>): Unit {
		// .geetignore에 명시되지 않는 파일들 가져오기
    val notIgnoreFiles = getNotIgnoreFiles(startDir = File("."))  
		// status 결과 가져오기
    val geetStatusResult = getGeetStatusResult(notIgnoreFiles = notIgnoreFiles)
		// 그 결과 출력
    printGeetStatus(geetStatusResult)
}
// geet/utils/geetUtil.kt
fun getIgnoreFiles(): List<String> {  // .geetignore 파일에 적힌 무시하는 파일들 읽어오기
    val ignoreFile = File(GEET_IGNORE_FILE_PATH)  // File("./.geetignore")
		// .geetignore 파일이 없다면 [".geet"]만 반환 (.geet 폴더는 항상 무시)
    if (!ignoreFile.exists()) {  
        return listOf(getRelativePath(GEET_DIR_PATH))
    }

		// .geetignore 파일을 읽고 줄마다 split하여 
    val ignoreFiles = ignoreFile.readText().split("\n").map { getRelativePath(it).trim() }
    return ignoreFiles + listOf(getRelativePath(GEET_DIR_PATH))
}

fun getNotIgnoreFiles(startDir: File): List<File> {  // 무시하지 않는 파일들 얻어오기
    if (!startDir.isDirectory) {  // 무시하지 않는 파일을 찾고자 하는 시작 폴더가 폴더가 아닌 경우
        throw BadRequest("디렉토리가 아닙니다.")
    }

    val files = mutableListOf<File>()
    startDir.listFiles()?.forEach {
        if (getRelativePath(it.path) in getIgnoreFiles()) {  // 무시하는 파일이라면 넘김
            return@forEach
        }

        if (it.isFile) {  // 파일이라면 추가
            files.add(it)
        } else if (it.isDirectory) {  // 폴더라면 재귀
            getNotIgnoreFiles(it).forEach { file ->
                files.add(file)
            }
        }
    }
    return files
}

fun getRelativePath(path: String): String {
    val rootPath = File(".").canonicalPath
    val filePath = File(path).canonicalPath

		// 파일 시스템 구분자("/") 기준으로 파싱
    val rootTokens = rootPath.split(File.separator)
    val fileTokens = filePath.split(File.separator)

		// 공통된 토큰의 길이 계산
    val commonPrefixLength = rootTokens.zip(fileTokens).takeWhile { it.first == it.second }.count()

		// .. 토큰을 현재 작업 디렉토리의 토큰 개수에서 공통 접두어의 길이를 뺀 만큼 추가/하고, 그 뒤에는 주어진 경로의 토큰들을 추가
    val relativeTokens = List(rootTokens.size - commonPrefixLength) { ".." } +
            fileTokens.drop(commonPrefixLength)

    return relativeTokens.joinToString(File.separator)
}
// geet/utils/commandutil/statusUtil.kt
fun getGeetStatusResult(notIgnoreFiles: List<File>): GeetStatusResult {
    val geetStatusResult = GeetStatusResult()
    notIgnoreFiles.forEach { file ->  // 무시하지 않는 파일들로 반복
        val relativePath = getRelativePath(file.path)  // 상대 경로 얻기
        val blobObject = GeetBlob(path = relativePath, content = file.readText())

        if (!indexManager.isIn(where = LAST_COMMIT, blobObject)) {  // 만약 최근 커밋에 없다면
            if (indexManager.isIn(where = STAGING_AREA, blobObject)) {  // 만약 스테이지에 추가되어 있다면
                geetStatusResult.newFiles.stagedFiles.add(relativePath)  // 스테이지의 새로운 파일로 추가

								// 현재 작업 디렉토리 내용이 스테이지에 올라온 내용과 다르다면 스테이지에 올린 후 다시 수정한 것
                if (!indexManager.isSameWith(where = STAGING_AREA, blobObject)) {
                    geetStatusResult.newFiles.unstagedFiles.add(relativePath)
                }
            } else {  // 최근 커밋에도 없고 스테이지에도 없다면 추적하지 않는 파일
                geetStatusResult.untrackedFiles.add(relativePath)
            }
        } else {  // 만약 최근 커밋에 있다면
            if (indexManager.isIn(where = STAGING_AREA, blobObject)) {  // 스테이지에 추가되었다면
								// 최근 커밋에 있는데 스테이지에도 있다는 것은 수정된 파일이라는 뜻
                geetStatusResult.modifiedFiles.stagedFiles.add(relativePath)
								
								// 스테이지에 올린 후 다시 수정하였다면
                if (!indexManager.isSameWith(where = STAGING_AREA, blobObject)) {
                    geetStatusResult.modifiedFiles.unstagedFiles.add(relativePath)
                }
            } else {  // 커밋에 있지만 스테이지엔 없다면
                geetStatusResult.modifiedFiles.unstagedFiles.add(relativePath)
            }
        }
    }

    return geetStatusResult
}

fun printGeetStatus(geetStatusResult: GeetStatusResult) {
    println("-- 스테이지에 존재하는 변경 사항들 --")
    println("스테이지에 존재하는 변경 사항들은 커밋을 하려면 \"geet commit\" 명령어를 사용하세요.")
    println("스테이지에 존재하는 변경 사항들은 스테이지에서 제거하려면 \"geet reset HEAD <file>\" 명령어를 사용하세요.\n")
    geetStatusResult.newFiles.stagedFiles.forEach { println("\t\u001B[32m새로 추가됨 : ${it}\u001B[0m") }
    geetStatusResult.modifiedFiles.stagedFiles.forEach { println("\t\u001B[32m수정됨: ${it}\u001B[0m") }
    geetStatusResult.removedFiles.stagedFiles.forEach { println("\t\u001B[32m삭제됨: ${it}\u001B[0m") }

    println("\n\n-- 스테이지에 존재하지 않는 변경 사항들 --")
    println("스테이지에 존재하지 않는 변경 사항들은 스테이지에 추가하려면 \"geet add <file>\" 명령어를 사용하세요.\n")
    geetStatusResult.newFiles.unstagedFiles.forEach { println("\t\u001B[33m새로 추가 후 다시 수정됨: ${it}\u001B[0m") }
    geetStatusResult.modifiedFiles.unstagedFiles.forEach { println("\t\u001B[33m수정됨: ${it}\u001B[0m") }
    geetStatusResult.removedFiles.unstagedFiles.forEach { println("\t\u001B[33m삭제됨: ${it}\u001B[0m") }

    println("\n\n-- 추적하지 않는 파일들 --")
    println("추적하지 않는 파일들은 스테이지에 추가하려면 \"geet add <file>\" 명령어를 사용하세요.\n")
    geetStatusResult.untrackedFiles.forEach { println("\t\u001B[31m${it}\u001B[0m") }
}

0개의 댓글