
status의 종류는 다음과 같이 생각을 해서 선택해보았다.
내용이 같냐 틀리냐는 SHA-1 값으로 비교
커존, 스존, 커같, 스같 → 커같인데 스존일 수 없음
커존, 스존, 커같, 스다 → 커같인데 스존일 수 없음
커존, 스존, 커다, 스같 → 파일 수정 후 스테이지에 올리고 더 이상 수정하지 않은 경우
커존, 스존, 커다, 스다 → 파일 수정 후 스테이지에 올렸는데 다시 수정한 경우
커존, 스없, 커같, 스같 → 최근 커밋에서 변경되지 않은 파일 → add, status 소용 없음
커존, 스없, 커같, 스다 → 커같인데 스없이면서 스다일 수 없음 (커같이라면 수정된 파일로 인식되지 않아 스없이면 아무 변경 사항도 인식되지 않음)
커존, 스없, 커다, 스같 → 스없인데 커다이면서 스같일 수 없음 (커다라면 수정된 파일로 인식이 되어야 함)
커존, 스없, 커다, 스다 → 파일 수정 후 아직 스테이지에 올리지 않은 경우
커없, 스존, 커같, 스같 → 커같인데 스존일 수 없음
커없, 스존, 커같, 스다 → 커같인데 스존일 수 없음
커없, 스존, 커다, 스같 → 새로운 파일을 추가하여 스테이지에 올리고 더 이상 수정하지 않은 경우
커없, 스존, 커다, 스다 → 새로운 파일을 추가하여 스테이지에 올린 후 다시 수정한 경우
커없, 스없, 커같, 스같 → 커없인데 커같일 수 없음
커없, 스없, 커같, 스다 → 커없인데 커같일 수 없음
커없, 스없, 커다, 스같 → 없는 파일
커없, 스없, 커다, 스다 → 새로운 파일을 추가하고 아직 스테이지에 올리지 않은 경우
여기에 삭제된 파일에 대한 상태 존재
그래서 선정한 상태는 다음과 같다.
새 파일
최근 커밋에 존재하지 않고 Staging Area에는 존재하는 경우
추적하지 않는 파일
최근 커밋에 존재하지 않고 Staging Area에도 존재하지 않는 경우
수정된 파일
최근 커밋에 존재하지만 그 내용(SHA-1 해시값)이 다른 경우
삭제된 파일
최근 커밋에 존재하지만 현재 작업 디렉토리에는 존재하지 않는 경우
변경되지 않은 파일
최근 커밋에 존재하지만 그 내용이 같은 경우 → 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") }
}