Gitlet 项目文档
仓库目录结构
.gitlet --|
|- staging # 暂存区序列化文件 (Staging 对象)
|- HEAD # 记录当前分支名
|- refs/
| |- heads/
| |- master # 分支文件,内容为所指 commit 的 SHA-1
| |- ... # 其他分支
|- Objects/
|- commits/ # 提交对象文件 (以 SHA-1 命名)
|- ... # 文件 Blob 备份 (以 SHA-1 命名)
- staging : 存储
Staging对象的序列化数据,包含暂存区映射表和删除标记集合。 - HEAD : 文本文件,内容为当前分支名(如
master)。 - refs/heads/: 每个分支对应一个文件,文件名为分支名,内容为该分支最新 commit 的 SHA-1。
- Objects/commits/: 每个 commit 对象以 SHA-1 值作为文件名存储。
- Objects/: 其余文件为被追踪文件的 Blob 备份,同样以 SHA-1 值命名。
类的职责与结构
Main(驱动类)
Main 是整个程序的入口,负责解析命令行参数并分发到 Repository 的对应静态方法。
核心逻辑:
args[0]作为命令名,通过switch分发到init/add/commit/rm/log/global-log/find/status/checkout/branch/rm-branch/reset/merge。checkout命令单独处理,根据参数个数 (2/3/4) 区分三种 checkout 模式。- 每个命令执行前会调用
checkGitLet()检查.gitlet目录是否存在(init除外)。 - 参数个数校验通过
validateNumArgs完成。
Repository(仓库核心)
Repository 包含所有 Gitlet 命令的实现逻辑和辅助方法。它是一个纯静态类,持有 .gitlet 目录下所有路径的 File 引用。
核心常量:
| 常量 | 路径 | 用途 |
|---|---|---|
CWD |
当前工作目录 | 用户文件的读写目标 |
GITLETDIR |
.gitlet |
仓库根目录 |
OBJECT |
.gitlet/Objects |
存储 Blob 和 commits 子目录 |
BRANCHDIR |
.gitlet/refs/heads |
分支文件目录 |
HEADFILE |
.gitlet/HEAD |
HEAD 指针文件 |
COMMITS |
.gitlet/Objects/commits |
提交对象目录 |
STAGINGFILE |
.gitlet/staging |
暂存区序列化文件 |
静态状态: stage --- 程序启动时从 staging 文件反序列化的 Staging 对象,全局唯一。
Commit(提交对象)
Commit 实现了 Serializable,代表仓库中的一次提交。
实例字段:
| 字段 | 类型 | 说明 |
|---|---|---|
message |
String |
提交信息 |
time |
String |
提交时间(格式: EEE MMM d HH:mm:ss yyyy Z) |
parent |
String |
第一父提交的 SHA-1(null 表示初始提交) |
secondParent |
String |
第二父提交的 SHA-1(仅 merge commit 非 null) |
mergeFiles |
List<String> |
合并的父提交 SHA-1 前 7 位列表 |
trackedFiles |
Map<String, String> |
文件名 → SHA-1 的映射表 |
关键方法:
saveCommit(): 序列化自身 → 计算 SHA-1 → 写入COMMITS目录 → 返回 SHA-1。
Staging(暂存区)
Staging 实现了 Serializable,代表暂存区状态。
实例字段:
| 字段 | 类型 | 说明 |
|---|---|---|
stagingMap |
TreeMap<String, String> |
暂存区文件名 → SHA-1 映射 |
removeSet |
TreeSet<String> |
标记为删除的文件名集合 |
关键方法:
clear(): 清空 stagingMap 和 removeSet。addFile(name, sha1): 将文件加入暂存区映射。resetFile(name): 从 stagingMap 和 removeSet 中同时移除文件。isEmpty(): stagingMap 和 removeSet 都为空时返回 true。
命令详解
init
功能: 在当前目录创建一个新的 Gitlet 版本控制系统仓库。这是所有其他命令的前置操作。
用法: java gitlet.Main init
实现逻辑:
- 若
.gitlet已存在,报错 "A Gitlet version-control system already exists in the current directory."。 - 创建
.gitlet、Objects、refs/heads、Objects/commits目录。 - 创建初始
Commit对象(message = "initial commit",parent = null,trackedFiles 为空 TreeMap)。 - 保存初始 commit,将 SHA-1 写入
refs/heads/master。 - 将 HEAD 指向
"master"。 - 序列化空
Staging对象到staging文件。
add
功能: 将工作目录中的文件添加到暂存区(staging area),为下一次 commit 做准备。如果文件内容与当前 HEAD commit 中的版本一致,则取消暂存。
用法: java gitlet.Main add [file name]
实现逻辑:
- 检查工作目录下目标文件是否存在,不存在则报错 "File does not exist."。
- 计算当前工作区文件的 SHA-1 (
getFileSha1)。 - 若文件内容与 HEAD commit 中追踪的版本一致 → 调用
resetFile移除暂存区和删除标记 → 持久化暂存区后返回。 - 若与暂存区中版本不同 → 将文件内容作为 Blob 存入
Objects目录 (saveFile) → 调用addFile更新暂存区映射。 - 持久化
Staging到staging文件。
commit
功能: 将暂存区中的更改(新增、修改、删除)保存为一个新的提交快照,追加到当前分支的历史中。提交后暂存区被清空。
用法: java gitlet.Main commit [message]
实现逻辑:
- 若暂存区为空(stagingMap 和 removeSet 都空),报错 "No changes added to the commit"。
- 以 HEAD commit 的 trackedFiles 为底稿(TreeMap 拷贝)。
- 将暂存区 stagingMap 全部
putAll到底稿。 - 遍历 removeSet,从底稿中
remove对应文件。 - 创建新
Commit对象(parent = HEAD SHA-1),调用saveCommit保存。 - 将新 commit 的 SHA-1 写入当前分支文件。
- 调用
stage.clear()清空暂存区,持久化。
rm
功能: 从暂存区或版本控制中移除文件。如果文件在暂存区,直接从暂存区删除;如果文件被当前 commit 追踪,则标记删除并从工作目录中删除该文件。
用法: java gitlet.Main rm [file name]
实现逻辑:
- 检查文件是否在暂存区 stagingMap 或 HEAD commit 中存在,都不存在则报错 "No reason to remove the file."。
- 从 stagingMap 中移除该文件(若存在)。
- 若文件在 HEAD commit 中被追踪:将其加入 removeSet → 删除工作目录下的文件 (
deleteCwdFile)。 - 持久化暂存区。
log
功能: 从 HEAD commit 开始,沿父提交链回溯,显示当前分支的提交历史。
用法: java gitlet.Main log
实现逻辑:
- 获取 HEAD commit 的 SHA-1。
- 循环沿
parent链回溯:反序列化当前 commit → 调用outputCommitLog打印 → 更新 SHA-1 为 parent。
outputCommitLog 输出格式:
===
commit <SHA-1>
Merge: <parent1前7位> <parent2前7位> (仅 merge commit)
Date: <时间戳>
<提交信息>
global-log
功能: 显示仓库中所有 commit 的信息,不局限于当前分支,包括其他分支和被遗弃的提交。
用法: java gitlet.Main global-log
实现逻辑:
- 获取
COMMITS目录下所有文件名(即所有 commit 的 SHA-1)。 - 对每个 SHA-1 反序列化 commit 并调用
outputCommitLog打印。
find
功能: 根据提交信息(commit message)查找匹配的 commit,打印其完整 SHA-1。支持多词搜索。
用法: java gitlet.Main find [commit message]
实现逻辑:
- 遍历所有 commit,比较 message 是否与目标字符串相等。
- 匹配则打印完整 SHA-1。
- 无匹配则报错 "Found no commit with that message"。
status
功能: 显示当前仓库的完整状态,包括:所有分支(当前分支加 * 标记)、暂存区文件、已标记删除的文件、已修改但未暂存的文件、未被追踪的文件。
用法: java gitlet.Main status
依次打印以下五个部分:
| 部分 | 数据来源 |
|---|---|
| Branches | getMarkingsBranches() --- 所有分支字典序排列,当前分支加 * 前缀 |
| Staged Files | getStagingFiles() --- stagingMap 的文件名列表(字典序) |
| Removed Files | stage.getRemoveSet() --- 删除标记集合 |
| Modifications Not Staged | getModifyNotStagingFiles() + getDeleteFiles() --- 修改未暂存和手动删除 |
| Untracked Files | getNotTrackingFiles() --- 未被追踪的文件 |
关键辅助函数:
getModifyNotStagingFiles: 遍历工作目录文件,若在 stagingMap 中存在但 SHA-1 不同,或在 HEAD commit 中存在但 SHA-1 不同,则标记为 "modified"。getDeleteFiles: HEAD commit 追踪但既不在 removeSet 中又不在工作目录中的文件,标记为手动删除 "deleted"。getNotTrackingFiles: 工作目录中存在但不在getTrackingFiles()返回集合中的文件。getTrackingFiles: HEAD commit 文件 ∪ stagingMap 文件 − removeSet 文件。
checkout
功能: 提供三种恢复/切换能力:(1) 将单个文件恢复到 HEAD commit 的版本;(2) 将单个文件恢复到指定 commit 的版本;(3) 切换到指定分支,更新工作目录所有文件。
用法:
java gitlet.Main checkout -- [file name]
java gitlet.Main checkout [commit id] -- [file name]
java gitlet.Main checkout [branch name]
checkout -- [file] (回滚文件到 HEAD 版本): 调用 checkoutID(getHeadSha1(), fileName),将文件回滚到 HEAD commit 版本。
checkout [commit id] -- [file] (回滚文件到指定提交):
- 调用
completeSha1补全缩写的 SHA-1。 - SHA-1 不存在或不唯一 → "No commit with that id exists"。
- 文件不在该 commit 中 → "File does not exist in that commit"。
- 从 commit 的 trackedFiles 获取文件 Blob SHA-1,调用
overwriteFile覆写工作区文件。
checkout [branch] (切换分支):
- 分支不存在 → "No such branch exists"。
- 切换到当前分支 → "No need to checkout the current branch"。
- 存在未追踪文件且会被覆盖/删除 → "There is an untracked file in the way..."。
- 删除工作目录中目标分支不追踪的文件。
- 将目标分支 commit 中的所有文件覆写到工作目录。
- 清空暂存区,HEAD 指向目标分支。
branch
功能: 在当前 HEAD commit 的位置创建一个新分支,分支名由用户指定。创建后不会自动切换到新分支。
用法: java gitlet.Main branch [branch name]
实现逻辑:
- 若分支已存在 → "A branch with that name already exists."。
- 在
refs/heads/下创建新文件,内容为当前 HEAD 的 SHA-1。
rm-branch
功能: 删除指定名称的分支。不能删除当前所在分支。
用法: java gitlet.Main rm-branch [branch name]
实现逻辑:
- 分支不存在 → "A branch with that name does not exist"。
- 删除的是当前分支 → "Cannot remove the current branch"。
- 调用
notCwdFileDelete删除分支文件。
reset
功能: 将当前分支回退到指定的 commit,同时将工作目录的所有文件替换为该 commit 的版本,并清空暂存区。这是一个"硬重置"操作。
用法: java gitlet.Main reset [commit id]
实现逻辑:
completeSha1补全 SHA-1,校验存在性。- 存在未追踪文件会被覆盖/删除 → "There is an untracked file in the way..."。
- 删除工作目录中目标 commit 不追踪的文件。
- 将目标 commit 的所有文件覆写到工作目录。
- 清空暂存区,将当前分支文件更新为目标 commit 的 SHA-1。
merge
功能: 将给定分支的更改合并到当前分支。自动处理多种合并情况(A-G),在发生冲突(情况 H)时生成冲突文件并提示用户手动解决。合并后自动提交。
用法: java gitlet.Main merge [branch name]
合并规则(8 种情况,基于 LCA 的三方对比):
| 情况 | 条件 | 操作 |
|---|---|---|
| A | given 变了,now 未变 | 覆写为 given 版本,自动暂存 |
| B | now 变了,given 未变 | 保持现状 |
| C | 两个分支以相同方式修改 | 保持不变 |
| D | 分割点无,仅 now 有 | 保持现状 |
| E | 分割点无,仅 given 有 | 检出 given 版本,暂存 |
| F | 分割点有,now 未变,given 删除 | 删除文件并取消追踪 |
| G | 分割点有,given 未变,now 删除 | 保持删除状态 |
| H | 两个分支以不同方式修改 | 冲突 |
执行流程:
- 分支不存在 → "A branch with that name does not exist."。
- 合并自身 → "Cannot merge a branch with itself."。
- 存在未追踪文件会被覆盖 → "There is an untracked file in the way..."。
- 暂存区非空 → "You have uncommitted changes."。
- 分割点 == given 分支 → given 是当前分支的祖先,无需合并。
- 分割点 == 当前分支 → fast-forward,等价 checkout 到 given 分支。
- 调用
fileProcessing遍历处理所有文件,冲突时生成冲突文件(格式:<<<<<<< HEAD\n...\n=======\n...\n>>>>>>>\n)并暂存。 - 有冲突打印 "Encountered a merge conflict."。
- 自动提交(message: "Merged {given} into {current}."),记录两个父提交的前 7 位 SHA-1。
LCA 查找 (getLCA):
- 第一步: 从当前分支 HEAD 开始 BFS,收集所有祖先节点(沿 parent 和 secondParent 两条链)。
- 第二步: 从给定分支 HEAD 开始 BFS,找到第一个在祖先集合中的 commit,即为最近公共祖先。
通用辅助函数
completeSha1 --- SHA-1 缩写补全:
- 输入 null → 返回 null。
- 长度 > 40 → 返回 null。
- 长度 == 40 → 直接检查文件是否存在。
- 长度 < 40 → 遍历
COMMITS下所有文件名,找到以该前缀开头的文件,唯一则返回完整 SHA-1,否则返回 null。
overwriteFile --- 文件覆写: 从 Objects/<sha1> 读取文件内容(字节数组),写入到 CWD/<fileName>。
trackingCheck --- 未追踪文件检测:
- 普通版: 调用
getNotTrackingFiles().isEmpty(),检查是否有未追踪文件。 - merge 版: 遍历未追踪文件,检查是否会被 given 分支的操作覆盖/删除,若有则返回 false。
数据流总结
用户命令 → Main.main() → Repository.<command>()
↓
getStaging() / saveStaging()
getCommit() / commit.saveCommit()
getHeadCommitTrackedFilesMap()
saveFile() / overwriteFile()
deleteCwdFile()
↓
操作 .gitlet/ 和 CWD 文件系统
- 序列化/反序列化:
Commit和Staging均实现Serializable,通过Utils.writeObject/Utils.readObject持久化。 - SHA-1 计算:
Utils.sha1(Object)基于序列化后的字节数组计算。 - 文件备份: Blob 以 SHA-1 为文件名存储在
Objects/下,相同内容自动去重。