# 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`가 디렉토리 별 트리 구조로 생성되기 때문