---
# System prepended metadata

title: Day13-14 미션(소스 저장 오브젝트Ⓜ️)

---

# Day13 미션(소스 저장 오브젝트Ⓜ️)
56-B S054, S059

## 체크리스트
- [x] Git 개체에 대해 학습 및 정리하기
- [x] Git 명령어에 따른 동작 방식 이해하기
- [x] 사용자 입력에 대해서 검증하는 객체 및 로직 구현하기
- [x] init 명령어 동작 구현하기
- [x] add 명령어 동작 구현하기
- [x] status 명령어 동작 구현하기
- [x] commit 명령어 동작 구현하기
- [ ] log 명령어 동작 구현하기
- [ ] restore 명령어 동작 구현하기

## 문제 해결 과정

### 전개 과정
1. Git의 작동 과정 및 원리와 Git 개체에 대한 이해가 필요했기 때문에, 각자 간단한 학습 과정을 거쳐서 짝 프로그래밍 시작
2. 요구사항 분석 및 체크리스트 작성
3. 드라이버와 내비게이터를 번갈아 가며 프로그램 시작부터 명령어에 대한 처리들을 차례대로 구현하여 미션 수행


## 구현 내용

### 타입 설명

#### HashContent
```swift
/// hash: sha를 통해 얻은 해쉬값
/// content: 해쉬값을 얻을 때 사용된 변경된 파일의 내용
struct HashContent: Hashable {
    let hash: String
    let content: String
}
```
#### Commend
```swift
/// 입력 가능한 명령어 정의
enum Commend: String {
    case `init`
    case add
    case status
    case commit
    case log
    case restore
}
```

#### InputError

```swift
/// 잘못된 입력값이 들어왔을 때 방출할 Error 타입
enum MitError: LocalizedError {
    case nilInput
    case invalidCommend
    case nonChanged
    
    var errorDescription: String? {
        switch self {
        case .nilInput:
            "입력값이 잘못되었습니다"
        case .invalidCommend:
            "유효하지 않는 명령어입니다"
        case .nonChanged:
            "변경사항이 없습니다."
        }
    }
}
```

### 프로그램 실행 함수 run()
```swift
var hashMap = [HashContent: [String]]()

func run() {
    while let input = readLine() {
        if input == ":wq" { break }
        do {
            guard checkMitCommend(input: input) else { continue }
            try splitCommend(input: input)
        } catch {
            print(error.localizedDescription)
        }
        
    }
}

func checkMitCommend(input: String) -> Bool {
    return input.hasPrefix("mit ")
}

func splitCommend(input: String) throws {
    let splittedInput = input.split(separator: " ").map { String($0) }
    guard let commend = Commend(rawValue: splittedInput[1]), splittedInput.count >= 3 else { throw InputError.invalidCommend }
    switch commend {
    case .`init`:
        try mitInit(directory: splittedInput[2])
    case .add:
        try mitAdd(directory: splittedInput[2])
    case .status:
        mitStatus(directory: splittedInput[2])
    case .commit: try mitCommit(directory: splittedInput[2])
    case .log: break
    case .restore: break
    }
}

```
- while문을 통해서 사용자로부터 명령어를 반복적으로 입력을 받도록 한다. readLine()은 옵셔널 문자열이기 때문에, while let을 통해서 바인딩을 함께 해주었다.
    - 종료에 대한 명령어도 필요하다고 판단하여 `:wq`를 입력한 경우, 프로그램을 종료하도록 한다.
- 명령어는 mit로 시작하는 문자열이기 때문에, `hasPrefix` 함수를 이용하여 입력값이 mit로 시작하는지를 확인한다. 또한  `“ “`을 기준으로 입력값을 쪼개고, mit 다음에 오는 문자열이 열거형으로 정의한 명령어 셋에 포함되는지를 열거형의 원시값을 이용해 확인한다.
- 각 명령어마다 해당하는 함수들을 호출하여 명령어 별 동작을 수행한다.

### init 명령어 동작 구현
```swift
func mitInit(directory: String) throws {
    let fileManager = FileManager.default
    try fileManager.createDirectory(atPath: "\(fileManager.currentDirectoryPath)/\(directory)/.mit/objects/", withIntermediateDirectories: true)
    try fileManager.createDirectory(atPath: "\(fileManager.currentDirectoryPath)/\(directory)/.mit/index/", withIntermediateDirectories: true)
}
```
- FileManager를 통해서 현재 디렉토리 경로에 입력된 디렉토리명에 해당하는 디렉토리와 `./mit` 디렉토리 구조`.mit/objects/`와 `.mit/index/`를 생성한다.

### add 명령어 동작 구현
```swift
func mitAdd(directory: String) throws {
    /// 디렉토리에서 전체 파일 목록을 탐색하고, 각 파일 내용에 대한 sha256 해시 값을 비교한다.
    /// commit이 없거나 직전 commit 이후 해시값이 달라진 파일 목록을 저장한다
    
    let desktopPath = "\(FileManager.default.currentDirectoryPath)/\(directory)"
    
    let files = try FileManager.default
        .subpathsOfDirectory(atPath: desktopPath)
        .filter {
            return !$0.hasPrefix(".")
        }
        .filter { content in
            let splitContent = content.split(separator: ".")
            if splitContent.count == 2 {
                return true
            } else {
                return false
            }
        }
    
    files.forEach { file in
        guard let content = try? String(contentsOfFile: "\(FileManager.default.currentDirectoryPath)/\(directory)/\(file)") else { return }
        
        guard let data = content.data(using: .utf8) else { return }
        let sha256 = SHA256.hash(data: data)
        let shaData = sha256.compactMap{String(format: "%02x", $0)}.joined()
        let hashContent = HashContent(hash: shaData, content: content)
        hashMap[hashContent] = (hashMap[hashContent] ?? []) + [file]
    }
}
```
- 현재 경로에 입력된 디렉토리명을 더하여 경로를 만든다.
- subpathsOfDirectory를 통해서 해당 경로에 존재하는 모든 디렉토리 및 파일들을 읽고, 파일인 것만 추려낸다.
- 파일들을 forEach문으로 순회하면서 파일의 내용에 대해 sha256 해시 값을 생성하고 hashMap의 키로 하여 파일의 내용들을 저장한다.

### status 명령어 동작 구현
`add`를 통해 hashMap에 저장된 변경 사항을 통해 변경된 파일의 경로와 파일명 출력

```swift
func mitStatus(directory: String) throws {
    if hashMap.isEmpty { throw MitError.nonChanged }
    for files in hashMap.values {
        files.forEach { file in
            let splitFile = file.split(separator: "/").map { String($0) }
            print("\n경로: \(FileManager.default.currentDirectoryPath)/\(directory)/\(file)") // 경로
            print("파일명: \(splitFile[splitFile.count - 1])")
        }
    }
}
```

### commit 명령어 동작 구현

#### commit 동작 순서
1. blobObject 생성
    1-1. `add` 명령어로 `hashMap` 딕셔너리에 추가된 데이터로 blob object 생성
    1-2. tree object에 저장될 blob 정보 저장
2. treeObject 생성
    2-1. blob 정보를 통해 tree object 생성
    2-2. tree Object의 해쉬값을 commit Object의 저장
3. commitObject 생성
    3-1. commits에서 상단을 읽어온다 => commits 없으면, 4로 부터 수행
    3-2. 읽어온 commit 해쉬값을 통해 commit 오프젝트를 찾는다.
    3-3. commit 오브젝트에서 현재 tree의 해쉬값을 가져온다.
    3-4. 가져온 tree 해쉬값을 이전에, 2-2에서 전달받은 값을 현재에 저장
    3-5. 새로운 commit 해쉬값 생성
    3-6. commits 상단에 업데이트 => 없으면 생성
4. hashMap 초기화

```swift
func mitCommit(directory: String) throws {
    let path = "\(FileManager.default.currentDirectoryPath)/\(directory)/.mit"
    var treeContent = ""
    for (key, value) in hashMap {
        /// blob 만들기
        let blobDirectoryName = key.hash.prefix(8)
        let blobObject = key.hash.map { String($0) }[8...].joined()
        let content = key.content
        let data = NSData(data: Data(content.utf8))
        let compressedData = try data.compressed(using: .zlib) as Data
        try FileManager.default.createDirectory(atPath: "\(path)/objects/\(blobDirectoryName)", withIntermediateDirectories: false)
        FileManager.default.createFile(atPath: "\(path)/objects/\(blobDirectoryName)/\(blobObject).zlib", contents: compressedData)
        
        let blobHash = key.hash
        let fileLength = compressedData.count
        
        for fileName in value {
            treeContent += "\(blobHash), \(fileLength), \(fileName)\n"
        }
    }
    
    /// tree object 생성
    guard let treeData = treeContent.data(using: String.Encoding.utf8) else { return }
    let sha256 = SHA256.hash(data: treeData)
    let treeHash = sha256.compactMap{String(format: "%02x", $0)}.joined()
    let treeObject = treeHash.map { String($0) }[8...].joined()
    FileManager.default.createFile(atPath: "\(path)/objects/\(treeObject).txt", contents: treeData)

    /// commit Object 생성 과정
    ///1. commits에서 상단을 읽어온다 => commits 없으면, 4로 부터 수행
    ///2. 읽어온 commit 해쉬값을 통해 commit 오프젝트를 찾는다.
    ///3. commit 오브젝트에서 현재 tree의 해쉬값을 가져온다.
    ///4. 가져온 tree 해쉬값을 이전에, `treeHash` 값을 현재에 저장
    ///5. 새로운 commit 해쉬값 생성
    ///6. commits 상단에 업데이트
    
    // 1
    var beforeTreeObject = ""
    if FileManager.default.fileExists(atPath: "\(path)/index/commits.txt") {
        //2
        let commitsContent = try String(contentsOfFile: "\(path)/index/commits.txt")
        let commitObject = commitsContent.split(separator: "\n").map { String($0) }[0]
        //3
        if let treeObjects = try? String(contentsOfFile: "\(path)/objects/\(commitObject).txt") {
            beforeTreeObject = treeObjects
                .split(separator: ", ")
                .map { String($0) }.last ?? ""
        }
    }
    //4 "이전, 현재"
    let commitContent = "\(beforeTreeObject), \(treeObject)"
    guard let commitData = commitContent.data(using: .utf8) else { return }
    let commitHash = SHA256.hash(data: commitData).compactMap{String(format: "%02x", $0)}.joined()
    let commitObject = commitHash.map { String($0) }[8...].joined()
    //5
    FileManager.default.createFile(atPath: "\(path)/objects/\(commitObject).txt", contents: commitData)
    
    //6
    //6-1 이미 작성된 commits를 읽어와서 상단에 `commitName` 추가
    if let commitsContent = try? String(contentsOfFile: "\(path)/index/commits.txt") {
        let newCommitsContent = "\(commitObject)\n\(commitsContent)"
        try newCommitsContent.write(toFile: "\(path)/index/commits.txt", atomically: true, encoding: .utf8)
    } else {
        let data = "\(commitObject)\n".data(using: .utf8)
        FileManager.default.createFile(atPath: "\(path)/index/commits.txt", contents: data)
    }
    
    hashMap = [:]
}

```

## 학습 메모

- [Git 개체](https://git-scm.com/book/ko/v2/Git의-내부-Git-개체)

Git 에서 변경사항을 추적하기 위해 사용한다.

#### (1) Blob 파일

- Binary Large Object.
- 각 버전의 파일이 blob 파일이다. 소스 코드, 이미지 등 다양한 파일의 데이터를 저장한다. 파일의 메타 데이터를 저장하지 않고 데이터 자체만을 저장한다. (파일명은 메타 데이터로, 저장되지 않음)
- 따라서 동일한 소스 코드를 가진 파일이 여러 개 있더라도 하나의 blob 파일만 생성된다.

#### (2) Tree 파일

- Blob에서 실제 파일의 데이터들을 저장하는 것과 다르게, Tree에는 파일 식별자, 파일 데이터의 해시값, 파일명이 저장된다.
- 폴더가 파일과 폴더로 구성되는 것처럼, tree는 blob과 또 다른 tree로 구성된다. 즉, blob 혹은 다른 tree를 참조한다.
- 파일 식별자는 100644(읽기 파일(blob)), 100755(실행 파일(blob)), 040000(디렉터리(tree)) 세 가지로만 구성된다.

#### (3) Commit 파일

- 각각의 커밋별로 하나의 커밋 파일로 저장된다. git으로 관리되는 가장 바깥 tree의 해시값, author, commiter, 커밋 메시지의 정보가 저장된다.
- parent에는 직전 커밋의 해시값이 저장되고, Linked List의 형태로 커밋들이 구성된다.

# day14 미션(소스 저장 오브젝트Ⓜ️) 개선하기
## 개선 포인트
- [ ] 기존 tree에 변경사항만 저장하는 것이 아닌 커밋 순간의 파일 정보를 저장
    - 선정 이유: tree의 변경사항만 저장하는 경우 `add` 또는 `restore` 명령어를 수행하기 위해 커밋 순서로 모든 tree의 정보를 읽어와야 함으로 효율성이 떨어질 수 있다고 판단
- [ ] day13에서 구현하지 못한 요구사항 구현
    - 선정 이유: 요구사항을 구현하며, 학습한 git 동작 원리를 구체화 할 수 있다고 판단
- [ ] tree를 디렉토리 계층 별로 생성
    - 선정 이유: git 방식에서 `treeObject`가 디렉토리 별 트리 구조로 생성되기 때문