# 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)。

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` 生成合併提交。