# Gitlet [Project requirment](https://sp21.datastructur.es/materials/proj/proj2/proj2) [Github link](https://github.com/t0matoOtk/Gitlet-CS61B) 實做主要撰寫在 `Project2/Repository.java` ### Gitlet 是如何運作的? Gitlet 的本質是一個**持久化的數據結構**。它利用文件系統來模擬記憶體中的物件: 1. **Content-Addressable Storage**: * **Blobs**:文件內容。Gitlet 不關心文件名,只關心內容。內容相同,SHA-1 雜湊值就相同,存成同一個檔案。 * **Commits**:包含metadata(時間、訊息、父節點 ID)和一個 `Map`(文件名 $\rightarrow$ Blob ID)。 ![image](https://hackmd.io/_uploads/SJZafvG2Zl.png) 2. **Serialization**: 為了永久保存這些資料,你必須要將 Java 物件(Commit, Staging Area)轉化為 Byte Stream 並存入 `.gitlet` 目錄中。 這種設計方法可以很大程度上減少所需要儲存的檔案數量,沒有更改檔案內容的部份,多個commit 可以共用同一份 blob 就好了 ### 實做的核心函式 | 函式 | 功能| | :--- | :--- | | `init` | 初始化 `.gitlet` 目錄與建立first commit | | `add` | 將文件快照存入 `objects` 並紀錄在暫存區 | | `commit` | 將暫存區打包成一個永久的快照 (Commit) | | `rm` | 取消暫存或標記文件為「待刪除」 | | `log` / `global-log` | 沿著父節點鏈條打印歷史紀錄 | | `checkout` | 恢復文件或切換分支 (最容易出 Bug) | | `status` | 顯示當前分支、暫存區與未追蹤文件狀態 | | `branch` / `rm-branch` | 建立或刪除指向提交的指標 (Pointer) | | `reset` | 強制切換到某個提交並更新工作目錄 | | `merge` | 合併兩個分支的變更 | --- ## 函式實作 * **`init()`** * 建立 `.gitlet`、`.blobs`(存內容)與 `.commits`(存提交物件) * 建立一個 `firstCommit`,時間強制設為 1970/01/01(Epoch 0),訊息為 "initial commit"。 * 建立新的 `Info` 物件,將 `master` 分支指向這個 `firstCommit` 的 ID,並將 `HEAD` 設為 `master` * 最後調用 `info.save()` 將整個狀態寫入 `INFO` 文件 * **`add(String fileName)`** * 確認 CWD(工作目錄)是否存在該檔案 * 計算該檔案目前的 SHA-1 (`newBlobId`),並取得當前提交 (`headCommit`) 中該檔案的 ID * **狀態切換**: * 如果檔案內容與 `headCommit` **完全相同**,則從 `info.staged` 中移除(如果有的話)。 * 如果檔案**有變動**,則加入 `info.staged` 集合中 * 這確保 `commit` 時只會處理真正有變化的檔案 * **`remove(String fileName)`** - 如果檔案在 `staged` 區,直接從 `staged` 移除(取消暫存) - 如果檔案已被當前提交追蹤 (`inCommit`),則將其加入 `info.removed` 集合,這表示在下一個提交中要「取消追蹤」此檔案 * **`makeCommit(String message)`** * **前置檢查**:若 `staged` 與 `removed` 兩個集合皆為空,代表沒變更,印出錯誤並中止。 * **快照更新**: 1. 獲取當前 `activeCommit`(即父節點) 2. 遍歷 `info.removed`,從 Commit 的檔案映射表中移除這些檔名 3. 遍歷 `info.staged`,將新檔案的對應關係加入映射表 * **生成新節點**:設定父節點 ID,計算新 Commit 的雜湊值並存入 `.commits` * **指標移動**:更新當前分支指向新 Commit,呼叫 `info.clear()` 清空暫存區後存檔 * **`log()`** - 從當前的 `headCommit` 開始,利用 `while` 迴圈不斷透過 `getFirstParentCommit()` 往回追蹤其父節點 * **`global-log()`** - 不理會分支路徑,直接調用 `plainFilenamesIn(COMMITS_DIR)` 讀取 `.commits` 資料夾下的所有檔案,逐一還原成 Commit 物件並印出資訊 * **`status()`** - **Branches**:遍歷 `info.branches`,在 `info.head` 對應的名稱前加上 `*` - **Staged/Removed**:直接從 `info.staged` 與 `info.removed` 讀取並排序印出 - **Modifications/Untracked**:比對 CWD 實體檔案、暫存區與當前提交的 SHA-1,找出狀態差異 * **`find(String findMessage)`** * 全掃描 `.commits` 資料夾,只要 Commit 的 `message` 與輸入相符,就印出該 ID。 * **`checkoutFile(String fileName)`** * 調用 `checkoutCommit`,目標 ID 設為目前的 `headCommitId`。從 Commit 中找到對應的 Blob 並覆蓋 CWD 檔案。 * **`checkoutBranch(String branchName)`** * 檢查工作目錄中是否有「未被追蹤」的檔案會被此次切換覆蓋(透過 `isUntracked` 輔助函式)。 * 讀取目標分支的所有檔案映射,調用 `checkoutCommit` 逐一還原檔案。 * 將 `info.head` 更新為新分支名稱。 * **`reset(String commitId)`** * 類似於 `checkoutBranch`。它會還原指定 `commitId` 的所有檔案,並強制讓目前的分支指標(例如 `master`)跳轉到該 `commitId` 上。 * **`createBranch(String branchName)`** * 在 `info.branches` (TreeMap) 中新增一筆資料,Key 為新名稱,Value 為目前 `headCommitId`。 * **`removeBranch(String branchName)`** * 從 `info.branches` 中移除對應的 Key。特別檢查不能刪除正在使用的分支 (`info.head`)。 * **`merge(String branchName)`** * **尋找共同祖先 (LCA)**:使用 `LCAcommit` 函式 **BFS** 找到兩分支的最近共同點。 * **策略判斷**: 1. **Fast-forward**:若 LCA 就是當前分支,直接切換到目標分支。 2. **3-way Merge**:比對 LCA、Head、Merged 三方內容。 * 若目標分支變更而當前沒動:自動更新檔案並暫存。 * 若兩邊都變更且內容不一:觸發 **Conflict**。 * **衝突處理**:手動組合 `<<<<<<< HEAD` 標記的字串,寫入實體檔案並加入暫存。 * **自動提交**:合併結束後自動調用 `makeCommit` 生成合併提交。