
add 명령어로 파일을 개체로 만들어 스테이지에 저장할 수 있다.
이 스테이지의 개체들로 나중에 커밋 개체를 만들게 됨
add 명령어를 구현하면서 필요한 여러 가지 것들이 있었다.
우선, 개체가 생성되는 위치인 object 폴더를 관리하는 ObjectManager 클래스부터 추가해 주었다.
// geet/manager/ObjectManager.kt
package geet.manager
import geet.geetobject.GeetBlob
import geet.geetobject.GeetObjectWithFile
import geet.geetobject.GeetTree
import geet.util.getRelativePathFromRoot
import geet.util.toZlib
import java.io.File
class ObjectManager {
val objectDir = File(".geet/objects")
fun saveBlob(file: File): GeetBlob { // Blob 개체 저장
val blob = GeetBlob(content = file.readText(), filePath = getRelativePathFromRoot(file))
val blobDir = File(objectDir, blob.hash.substring(0, 2))
val blobFile = File(blobDir, blob.hash.substring(2))
if (!blobDir.exists()) {
blobDir.mkdirs()
}
blobFile.writeText(blob.content.toZlib())
return blob
}
fun saveTree(file: File): GeetTree { // Tree 개체 저장
val tree = mutableListOf<GeetObjectWithFile>()
file.listFiles()?.forEach { it ->
if (it.isDirectory && !it.listFiles().isNullOrEmpty()) {
tree.add(saveTree(it))
} else {
tree.add(saveBlob(it))
}
}
val treeObject = GeetTree(filePath = getRelativePathFromRoot(file), tree = tree)
val treeDir = File(objectDir, treeObject.hash.substring(0, 2))
val treeFile = File(treeDir, treeObject.hash.substring(2))
if (!treeDir.exists()) {
treeDir.mkdirs()
}
treeFile.writeText(treeObject.content.toZlib())
return treeObject
}
}
그 후 index 파일을 관리하는 IndexManager도 추가해 주었다.
// geet/manager/IndexManager.kt
package geet.manager
import geet.enums.StageObjectStatus
import geet.geetobject.GeetBlob
import geet.util.fromZlibToString
import geet.util.toZlib
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import java.io.File
import java.time.LocalDateTime
@Serializable // Json 파싱을 위한 Serializable 어노테이션 추가
data class StageObject( // 스테이지에 존재하는 개체를 나타내는 data class
val blob: GeetBlob, // 저장된 blob 개체
val slot: Int, // slot(0 정상, 1-3 컨플릭트)
val status: StageObjectStatus, // 상태(새 파일, 수정된 파일, ,,)
val lastUpdateTime: String // 마지막 업데이트 시간
)
@Serializable
data class IndexData(
val stageObjects: MutableList<StageObject>, // 스테이지
val lastCommitObjects: List<GeetBlob> // 최근 커밋 개체들
)
class IndexManager {
val indexFile = File(".geet/index")
val indexData: IndexData
init { // indexData 초기화
if (indexFile.exists()) {
indexData = Json.decodeFromString(IndexData.serializer(), indexFile.readText().fromZlibToString())
} else {
indexData = IndexData(mutableListOf(), listOf())
}
}
// 스테이지에 개체 추가, 일단 blob 개체로 생성 후 나중에 커밋할 때 폴더 별 tree 개체를 만들 예정
fun addToStage(blob: GeetBlob, deleted: Boolean = false, slot: Int = 0) {
var status: StageObjectStatus
when (true) {
deleted -> status = StageObjectStatus.DELETED
(searchObjectFromLastCommit(blob.filePath) == null) -> status = StageObjectStatus.NEW
else -> status = StageObjectStatus.MODIFIED
}
val stageObject = StageObject(
blob = blob,
slot = slot,
status = status,
lastUpdateTime = LocalDateTime.now().toString()
)
indexData.stageObjects.add(stageObject)
}
fun removeFromStage(filePath: String) { // 스테이지에서 제거
indexData.stageObjects.removeIf { it.blob.filePath == filePath }
}
fun searchObjectFromStage(filePath: String): StageObject? { // 스테이지에서 같은 경로의 개체 찾기
return indexData.stageObjects.find { it.blob.filePath == filePath }
}
fun searchObjectFromLastCommit(filePath: String): GeetBlob? { // 최근 커밋에서 같은 경로의 개체 찾기
return indexData.lastCommitObjects.find { it.filePath == filePath }
}
fun writeIndex() { // 인덱스 파일에 정보 저장
indexFile.writeText(Json.encodeToString(IndexData.serializer(), indexData).toZlib())
}
}
이제 .geetignore 파일을 읽어 무시하는 파일들을 읽어오는 IgnoreManager도 추가해 주었다.
// geet/manager/IgnoreManager.kt
package geet.manager
import geet.util.getRelativePathFromRoot
import java.io.File
class IgnoreManager {
val ignoreFile = File(".geetignore")
val ignoreSet: Set<String> // 무시되는 파일 경로 집합
get() = if (ignoreFile.exists()) {
val ignoreSet = mutableSetOf<String>()
ignoreFile.readLines().forEach {
if (it.isNotBlank() && !it.startsWith("#")) {
ignoreSet.add(getRelativePathFromRoot(File(it)))
}
}
ignoreSet.add(".geet")
ignoreSet
} else {
setOf(".geet")
}
fun isIgnored(file: File): Boolean { // 특정 파일이 무시되는 파일인지 검사
val relativePath = getRelativePathFromRoot(file)
return ignoreSet.any { it == relativePath }
}
}
이제 구현한 add 명령어는 다음과 같다.
// geet/command/geetAdd.kt
package geet.command
import geet.exception.BadRequest
import geet.util.const.*
import geet.util.getRelativePathFromRoot
import java.io.File
fun geetAdd(commandLines: Array<String>): Unit {
if (commandLines.size != 2) { // 'add <파일 경로>' 형식이어야 함
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
}
// 존재하지 않는 파일이라면 최근 커밋 개체들에 있는지 검사 후 없으면 에러, 있으면 삭제된 파일이라고 판단하여 스테이지에 추가
val objectInLastCommit = indexManager.searchObjectFromLastCommit(filePath)
?: throw BadRequest("파일이 존재하지 않습니다.: ${red}${filePath}${resetColor}")
indexManager.addToStage(objectInLastCommit, deleted = true)
indexManager.writeIndex()
}
fun addFileToStage(file: File) { // 스테이지에 파일 추가
if (ignoreManager.isIgnored(file)) {
return
}
val filePath = getRelativePathFromRoot(file)
val blob = objectManager.saveBlob(file)
val samePathObjectInStage = indexManager.searchObjectFromStage(filePath)
if (samePathObjectInStage != null) {
indexManager.removeFromStage(filePath)
}
indexManager.addToStage(blob)
}
fun addAllFilesInDirectory(directory: File) { // 폴더 내의 파일들 스테이지에 추가
if (ignoreManager.isIgnored(directory)) {
return
}
directory.listFiles()?.forEach { file ->
if (file.isDirectory) {
addAllFilesInDirectory(file)
} else {
addFileToStage(file)
}
}
}
이렇게 구현을 하고 명령어를 실행해 보면 아래와 같이 잘 저장된 모습을 볼 수 있다.

위는 확인을 위해 zlib 압축 과정을 잠시 없앤 모습
원래는 저렇게 Json 형식의 문자열을 zlib으로 압축하여 파일 내용으로 저장한다.