CS61B | gitlet

核心类

Commit.java

  • 功能: 记录一次提交的元数据和状态
  • 字段
    • String message : 提交信息
    • List<String> parents : 父提交(size <= 2)(gitlet的限制,合并的分支最多是两个)
    • Map<String, String> pathToBlobID : filePath->Blob对象的Hash值 的映射
    • String id : 当前提交Hash值
  • 主要方法
    • Constructer
      • Commit() 空提交
      • Commit(String message, List<String> parents, Map<String, String> pathToBlobID) : 普通提交
    • String dateToTimeStamp(Date date) : 把Date类型的时间转换成指定表达形式的时间戳
    • String generateID() : 把关键信息组合起来生成唯一哈希值,确保唯一性
    • void save() : 把提交对象保存到文件系统
    • Commit fromFile(String id) : 从文件系统中加载对象
    • Getters :
      • String getMessage()
      • String getTimeStamp()
      • String getParents()
      • Map<String, String> getPathToBlobId()
      • String getId()

Blob.java

  • 功能 :Blob对象用于存储某个版本文件的内容快照(最大发挥SHA-1内容可寻址的优势
  • 字段
    • String id : Blob对象的唯一表示ID
    • byte[] contents : 以字节数组的形式存储文件内容,保留文件的原始二进制数据
    • String filePath : 对应文件在工作区的路径
  • 主要方法
    • Constructor :
      • Blob(File file) : 通过文件对象创建Blob快照
    • void save() : 把Blob对象持久化存储到文件系统里面
    • Blob fromFile(String blobId) : 从文件中反序列化加载Blob对象
    • Getters:
      • Srting getId()
      • String getFilePath()
      • byte[] getContents

StagingArea.java

  • 功能 : 暂存区类,用于记录即将提交的文件变更
  • 字段
    • Map<String, String> added : 新增文件, 存储 filePath->blobId 的唯一映射
    • Set<String> removed : 待删除文件, 提交后这些文件将在Commit里面不存在
  • 主要方法
    • Constructor :
      • StagingArea() : 初始化一个空的暂存区
    • void save() : 将暂存区对象序列化写入指定文件中
    • StgingArea load() : 从文件中反序列化提取暂存区对象
    • void unStageFile(String filePath) : 取消文件的所有暂存状态,从新增区和待删除区都去掉
    • void stageAdd(String filePath, String blobId) : 将文件添加到新增暂存区,从待删除暂存区删除
    • void stageRemove(String filePath) : 从待删除区中添加文件,从新增暂存区中删除文件
    • void clear() : 清空暂存区
    • boolean isEmpty() : 检查暂存区是否为空
    • Getters :
      • Map<String, String> getAdded()
      • Set<String> getRemoved()

Repository.java

.gitlet文件结构

1
2
3
4
5
6
7
8
9
10
11
12
.gitlet
|-- objects
| |-- commits
| |--...
| |-- bolbs
| |--...
|-- refs
| |-- heads
| |-- master
| |-- ...
| |-- HEAD
|-- staging

init()

  • 功能 : 在当前目录创建一个新的Gitlet版本控制系统
  • 实现方法 :
    • 创建目录结构
    • 创建初始commit
    • 设置master分支,写入最初的commit Id
    • 设置HEAD指向当前当前分支的路径
    • 初始化暂存区

add(String fileName)

  • 功能 : 将文件当前状态的副本添加到暂存区
  • 实现方法
    • 校验仓库是否初始化
    • 判断文件是否存在于当前工作目录
    • 根据文件生成Blob对象
    • 获取当前分支的最新提交
    • 尝试在当前的最新提交的快照中找到指定文件的Blob ID
    • 如果两者相同,那么就取消文件的任何暂存标记
    • 如果两者不同,那么就把文件添加到新增暂存区
    • 保存暂存区

commit(String message)

  • 功能 :保存当前提交和暂存区的文件快照,以便后续恢复,同时创建一个新的提交
  • 实现方法
    • 校验仓库是否初始化
    • 校验信息是否为空
    • 加载暂存区,若暂存区为空,说明没有需要提交的对象
    • 获取当前分支的引用文件和提交对象
    • 以当前提交为基础创建新的提交对象(包括更新父提交,继承内容快照)
    • 依靠暂存区的变更,更新内容快照
    • 创建新的提交并保存
    • 更新分支指针的内容为新的提交
    • 清空并保存暂存区

rm(String fileName)

  • 功能 : 如果文件已经呗暂存添加,则取消暂存;如果文件在当前提交被跟踪,则将其标记为待删除,并且删除在工作目录上的文件
  • 实现方法
    • 校验仓库初始化
    • 加载暂存区和最新的提交信息
    • 如果在新增暂存区存在该文件,那就取消暂存
    • 如果文件被当前提交跟踪,那么就标记为待删除,并且在工作区中移除该文件
    • 如果并非以上两种情况,则命令失效,报出错误信息

log()

  • 功能 : 从当前头提交开始,沿着提交树逆向显示每个提交的信息,直至初始提交,遵循第一父提交链接,忽略合并中发现的任何第二父提交
  • 实现方法
    • 校验仓库初始化
    • 获取当前的最新提交
    • 遍历父提交链打印,直至初始提交

global-log()

  • 功能 :会显示所有的提交信息,提交的顺序无所谓
  • 实现方法
    • 校验仓库初始化
    • 利用utils里的函数获取commits文件夹里面的所有提交
    • 逐条打印

find(String message)

  • 功能 : 打印具有给定提交信息的提交ID
  • 实现方法 :
    • 校验仓库初始化
    • 利用utils里的函数获取commits文件夹里面的所有提交
    • 遍历,寻找是否有相同的信息即可
    • 若找不到,则打印错误信息

status()

  • 功能 :描述当前的所有分支,用*标记当前所在的分支;同时显示暂存添加和待删除的文件,(extra:显示未修改的暂存和未跟踪的文件,推荐实现,方便后续debug)
  • 实现方法 :
    • 校验仓库初始化
    • Branches
      • 获取所有分支的名称
      • 获取当前分支的名称
      • 按字典序打印分支列表,注意标注当前分支
    • stagedFile
      • 加载暂存区
      • 获取新增暂存区的文件并打印
    • rmmovedFile
      • 加载暂存区
      • 获取待删除暂存区的文件并打印
    • modificationsNotStagedForCommit
      • 加载暂存区,当前的提交
      • 检查所有当前提交跟踪的文件,打印满足条件的文件并添加对应标记
        • 在新增暂存区里
          • 工作目录的文件被删除
          • 工作目录的文件内容与新增暂存区的文件内容不一致
        • 未在删除暂存区里
          • 工作目录的文件被删除
          • 工作目录的文件内容与被提交跟踪的文件内容不一致
    • untrackedFiles
      • 加载暂存区,当前提交
      • 打印存在于工作目录上,但既未背暂存添加也未被提交跟踪的文件

checkout

  • 功能1
    • checkout -- [fileName] : 将文件在头提交的版本中取出并且放入工作目录。
  • 实现方法
    • 获取最新提交
    • 检查文件在最新提交里是否存在
    • 获取blob id加载文件内容,写入工作区
  • 功能2
    • checkout [commit id] -- [fileName] :从给定提交中获取文件并将其写在工作目录里
  • 实现方法
    • 解析提交id(可能传入的是提交id的前缀),恢复成完整版,如果不存在那么报错退出
    • 加载指定提交
    • 检查文件在指定提交中是否存在
    • 获取blob id加载文件内容,写入工作区
  • 功能3
    • checkout [branch name] : 将给定分支头部提交的所有文件取出放在工作目录中。并将给定分支视为当前分支。清空暂存区
  • 实现方法
    • 检查分支是否存在
    • 检查是否是当前分支
    • 加载目标分支的最新提交
    • 检查未跟踪的文件是否会被覆盖
      • 加载当前分支跟踪的文件和目标分支跟踪的文件以及工作区的文件
      • 检查在工作区的文件,如果在当前分支中未被跟踪而且在目标分支中存在,则会被覆盖
    • 更新工作区的文件
      • 写入目标分支的所有文件
      • 删除当前分支存在但是目标分支没有的文件
    • 更新HEAD指向目标分支
    • 清空暂存区并保存

branch(String branchName)

  • 功能 :创建一个具有指定名称的分支,并且指向当前分支的头提交。该命令不会立刻切换到新建立的分支
  • 实现方法
    • 检查仓库是否存在
    • 检查是否有同名branch
    • 获取当前最新的提交id
    • 在分支中写入当前的提交id

rmBranch(String branchName)

  • 功能 :删除指定名称的分支,仅删除与分支关联的指针
  • 实现方法 :
    • 检查仓库是否存在
    • 检查是否存在这个分支
    • 检查是否属于当前分支
    • 删除分支指针文件

reset(String commitId)

  • 功能 : 检出给定提交的所有文件,移除该提交中不存在的已跟踪文件,同时将该分支的头部移到该提交节点,清空暂存区
  • 实现方法
    • 检查仓库
    • 解析完整commit id
    • 检查是否会有未被跟踪的文件被覆盖
    • 更新工作区到目标提交状态
    • 更新当前分支的指针到目标提交上
    • 清空暂存区
  • 与git里面checkout [commit id]的辨析:
    操作 分支指针 暂存区 应用场景
    checkout [commit id] HEAD脱离(Detached HEAD) 保留 临时浏览,实验旧版本
    reset [commit id] 指向当前提交 清空 回退提交(硬回退), 丢弃历史和改动

merge(String branchName)

  • 功能 :将给定分支的文件合并到当前分支
  • 实现方法
    • 检查是否满足合并条件
      • 校验仓库初始化
      • 校验给定分支是否存在
      • 校验是否合并自身
      • 校验是否有未提交的修改
      • 校验是否会有未被跟踪的文件会被合并分支覆盖
    • 获取当前分支的最新提交信息和给定分支的提交信息
    • 寻找最近公共祖先
      • 收集当前分支的所有祖先,不只是第一父链
      • bfs查找给定分支的祖先,最先出现在当前分支祖先里面的就是最近公共祖先
    • 处理特殊合并场景
      • 如果最近公共祖先是给定提交,不需要进行任何操作
      • 如果最近公共祖先是当前提交,直接将工作区更新为给定分支的提交,并且更新HEAD文件为给定提交
    • 处理所有文件的合并
      • 获取三方所有文件路径的集合,并遍历
        序号 split curr given 情况
        1 存在 不修改 修改 given覆盖curr
        2 存在 不修改 删除 直接删除curr
        3 存在 不修改 不修改 不动
        4 存在 修改 不修改 不动
        5 存在 修改 修改 修改得一致,不动;修改得不一致,冲突
        6 存在 修改 删除 冲突
        7 存在 删除 不修改 不动
        8 存在 删除 修改 冲突
        9 存在 删除 删除 不动
        10 不存在 新增 新增 新增内容不同,冲突:内容相同, 不动
        11 不存在 新增 不新增 不动
        12 不存在 不新增 新增 given 覆盖curr
        13 不存在 不新增 不新增 不动
      • 冲突情况 (5,6, 8, 10)
        • 读取给定分支的该文件与当前分支的该文件
          • 判断文件内容最后是否需要增加空行(特判空文件,要注意)
          • 生成冲突标记内容,在工作区重新写入该文件
          • 新增暂存区添加冲突文件,保存
          • 标记已产生冲突
      • given覆盖curr情况(1, 12)
        • 读取给定分支的该文件
        • 直接在工作区重新写入该文件
        • 新增暂存区添加该文件,保存
      • 删除curr情况(2)
        • 在工作区找到该文件并删除
        • 在待删除暂存区中添加该文件并保存
    • 创建合并提交
      • 生成合并信息
      • 生成合并提交的父提交(2个)
      • 生成新快照
      • 把当前提交中不再删除暂存区的文件添加到新快照上(对应不动的文件和删除的文件)
      • 把新增暂存区的文件也添加到新快照上(对应冲突文件和given覆盖curr的文件)
      • 创建并保存合并提交
      • 更新当前分支的指针指向合并提交
      • 清空暂存区并保存
    • 打印冲突提示

debug的坑点和一些注意事项

  • 文件操作的顺序不要写反了,比如reset里面应当是先把给提交的文件更新到工作区,然后再把HEAD文件换成给定提交的HASH值,顺序搞反了很难绷
  • merge函数里面找split的时候,应该采用bfs遍历所有父提交,而不是单纯沿着第一父链去寻找
  • 处理冲突文件的时候注意换行符,被删除的文件要进行特判
  • 待补充……