Gitlet 项目

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

实现逻辑:

  1. .gitlet 已存在,报错 "A Gitlet version-control system already exists in the current directory."。
  2. 创建 .gitletObjectsrefs/headsObjects/commits 目录。
  3. 创建初始 Commit 对象(message = "initial commit",parent = null,trackedFiles 为空 TreeMap)。
  4. 保存初始 commit,将 SHA-1 写入 refs/heads/master
  5. 将 HEAD 指向 "master"
  6. 序列化空 Staging 对象到 staging 文件。

add

功能: 将工作目录中的文件添加到暂存区(staging area),为下一次 commit 做准备。如果文件内容与当前 HEAD commit 中的版本一致,则取消暂存。

用法: java gitlet.Main add [file name]

实现逻辑:

  1. 检查工作目录下目标文件是否存在,不存在则报错 "File does not exist."。
  2. 计算当前工作区文件的 SHA-1 (getFileSha1)。
  3. 若文件内容与 HEAD commit 中追踪的版本一致 → 调用 resetFile 移除暂存区和删除标记 → 持久化暂存区后返回。
  4. 若与暂存区中版本不同 → 将文件内容作为 Blob 存入 Objects 目录 (saveFile) → 调用 addFile 更新暂存区映射。
  5. 持久化 Stagingstaging 文件。

commit

功能: 将暂存区中的更改(新增、修改、删除)保存为一个新的提交快照,追加到当前分支的历史中。提交后暂存区被清空。

用法: java gitlet.Main commit [message]

实现逻辑:

  1. 若暂存区为空(stagingMap 和 removeSet 都空),报错 "No changes added to the commit"。
  2. 以 HEAD commit 的 trackedFiles 为底稿(TreeMap 拷贝)。
  3. 将暂存区 stagingMap 全部 putAll 到底稿。
  4. 遍历 removeSet,从底稿中 remove 对应文件。
  5. 创建新 Commit 对象(parent = HEAD SHA-1),调用 saveCommit 保存。
  6. 将新 commit 的 SHA-1 写入当前分支文件。
  7. 调用 stage.clear() 清空暂存区,持久化。

rm

功能: 从暂存区或版本控制中移除文件。如果文件在暂存区,直接从暂存区删除;如果文件被当前 commit 追踪,则标记删除并从工作目录中删除该文件。

用法: java gitlet.Main rm [file name]

实现逻辑:

  1. 检查文件是否在暂存区 stagingMap 或 HEAD commit 中存在,都不存在则报错 "No reason to remove the file."。
  2. 从 stagingMap 中移除该文件(若存在)。
  3. 若文件在 HEAD commit 中被追踪:将其加入 removeSet → 删除工作目录下的文件 (deleteCwdFile)。
  4. 持久化暂存区。

log

功能: 从 HEAD commit 开始,沿父提交链回溯,显示当前分支的提交历史。

用法: java gitlet.Main log

实现逻辑:

  1. 获取 HEAD commit 的 SHA-1。
  2. 循环沿 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

实现逻辑:

  1. 获取 COMMITS 目录下所有文件名(即所有 commit 的 SHA-1)。
  2. 对每个 SHA-1 反序列化 commit 并调用 outputCommitLog 打印。

find

功能: 根据提交信息(commit message)查找匹配的 commit,打印其完整 SHA-1。支持多词搜索。

用法: java gitlet.Main find [commit message]

实现逻辑:

  1. 遍历所有 commit,比较 message 是否与目标字符串相等。
  2. 匹配则打印完整 SHA-1。
  3. 无匹配则报错 "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] (回滚文件到指定提交):

  1. 调用 completeSha1 补全缩写的 SHA-1。
  2. SHA-1 不存在或不唯一 → "No commit with that id exists"。
  3. 文件不在该 commit 中 → "File does not exist in that commit"。
  4. 从 commit 的 trackedFiles 获取文件 Blob SHA-1,调用 overwriteFile 覆写工作区文件。

checkout [branch] (切换分支):

  1. 分支不存在 → "No such branch exists"。
  2. 切换到当前分支 → "No need to checkout the current branch"。
  3. 存在未追踪文件且会被覆盖/删除 → "There is an untracked file in the way..."。
  4. 删除工作目录中目标分支不追踪的文件。
  5. 将目标分支 commit 中的所有文件覆写到工作目录。
  6. 清空暂存区,HEAD 指向目标分支。

branch

功能: 在当前 HEAD commit 的位置创建一个新分支,分支名由用户指定。创建后不会自动切换到新分支。

用法: java gitlet.Main branch [branch name]

实现逻辑:

  1. 若分支已存在 → "A branch with that name already exists."。
  2. refs/heads/ 下创建新文件,内容为当前 HEAD 的 SHA-1。

rm-branch

功能: 删除指定名称的分支。不能删除当前所在分支。

用法: java gitlet.Main rm-branch [branch name]

实现逻辑:

  1. 分支不存在 → "A branch with that name does not exist"。
  2. 删除的是当前分支 → "Cannot remove the current branch"。
  3. 调用 notCwdFileDelete 删除分支文件。

reset

功能: 将当前分支回退到指定的 commit,同时将工作目录的所有文件替换为该 commit 的版本,并清空暂存区。这是一个"硬重置"操作。

用法: java gitlet.Main reset [commit id]

实现逻辑:

  1. completeSha1 补全 SHA-1,校验存在性。
  2. 存在未追踪文件会被覆盖/删除 → "There is an untracked file in the way..."。
  3. 删除工作目录中目标 commit 不追踪的文件。
  4. 将目标 commit 的所有文件覆写到工作目录。
  5. 清空暂存区,将当前分支文件更新为目标 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 两个分支以不同方式修改 冲突

执行流程:

  1. 分支不存在 → "A branch with that name does not exist."。
  2. 合并自身 → "Cannot merge a branch with itself."。
  3. 存在未追踪文件会被覆盖 → "There is an untracked file in the way..."。
  4. 暂存区非空 → "You have uncommitted changes."。
  5. 分割点 == given 分支 → given 是当前分支的祖先,无需合并。
  6. 分割点 == 当前分支 → fast-forward,等价 checkout 到 given 分支。
  7. 调用 fileProcessing 遍历处理所有文件,冲突时生成冲突文件(格式: <<<<<<< HEAD\n...\n=======\n...\n>>>>>>>\n)并暂存。
  8. 有冲突打印 "Encountered a merge conflict."。
  9. 自动提交(message: "Merged {given} into {current}."),记录两个父提交的前 7 位 SHA-1。

LCA 查找 (getLCA):

  • 第一步: 从当前分支 HEAD 开始 BFS,收集所有祖先节点(沿 parent 和 secondParent 两条链)。
  • 第二步: 从给定分支 HEAD 开始 BFS,找到第一个在祖先集合中的 commit,即为最近公共祖先。

通用辅助函数

completeSha1 --- SHA-1 缩写补全:

  1. 输入 null → 返回 null。
  2. 长度 > 40 → 返回 null。
  3. 长度 == 40 → 直接检查文件是否存在。
  4. 长度 < 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 文件系统
  • 序列化/反序列化: CommitStaging 均实现 Serializable,通过 Utils.writeObject / Utils.readObject 持久化。
  • SHA-1 计算: Utils.sha1(Object) 基于序列化后的字节数组计算。
  • 文件备份: Blob 以 SHA-1 为文件名存储在 Objects/ 下,相同内容自动去重。