[Git 만들어보기 - Geet] add 명령어로 스테이지에 파일 추가

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

Git 만들어보기 - Geet

목록 보기
19/21
post-thumbnail

2024-03-04 ~ 2023-03-05 구현

add 명령어로 파일을 개체로 만들어 스테이지에 저장할 수 있다.

이 스테이지의 개체들로 나중에 커밋 개체를 만들게 됨

add 명령어를 구현하면서 필요한 여러 가지 것들이 있었다.

  • 개체 생성
  • index 파일
  • 무시되는 파일(.geetignore)

우선, 개체가 생성되는 위치인 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으로 압축하여 파일 내용으로 저장한다.

0개의 댓글