Gitlet之我见

CS61B—Gitlet 之我见

CS61B 的课程项目有一大特点,便是循序渐进——当我刚刚接受完 Java 基本语法的熏陶之时,Project1 的双端队列于我而言尚有一定难度。在文档中求索,在代码中前行。磕磕绊绊写完 proj1 的时候,鼠鼠内心不由得感到一丝恐慌——听说后面的 Gitlet 是个不得了的大家伙,而这先锋兵将 proj1 已让我汗流浃背(事后诸葛亮来看,应该是 Java 速通太快的缘故)!怀揣着这样忐忑又略显激动的心情,在扎扎实实啃完所有课程以及 lab 之后,我敲开了 Gitlet 的大门——

站在已经写完 Gitlet 的视角来看,它的特点并不是难度高,而是文档冗长以及开发要求众多。2W 字的文档足以好好磨砺你的英文阅读能力。(或许这就是贴近实际开发过程的体现么,hhh)最后加上 EC 部分,总计用时大概是 25h,不算 EC 是 19h,如下:

(这段时间在学球,所以战线拉得很长,大概每天只能写 2-3h。有时候教练不在,狂写一天,爽哉)

总时间(软件:WakaTime)

我自己的实现:Gitlet

证明我写完了 EC:

AG截图

下面是我自己的一些总结,建议,部分命令的思路以及它带给我的收获:

对后来人的建议

  • 在开始写代码之前,尽量看完整个项目文档,在脑海中有一个大致完整的构思框架。这样可以避免一直写一直改之前的窘境,甚至会面临全部推倒重写!
  • 良好的代码规范!给所有变量命名必须讲究到位,增加代码可读性,不然睡一觉起来就不知道自己干了啥了
  • 多多写注释!这并不是浪费时间的习惯,相反,到位明了的注释在你 debug 时将会有良好的引导功能。很多时候我们的 bug 并不是大的逻辑有问题,而是就是在小的地方出了谬误,例如条件判断时的”!”,忘记一些必备基础操作等等,这种 bug 才是最难 de 的!
  • Commit,Blob,Stage,Branch,Remote这些基础概念务必理解到位,**/.gitlet**仓库的结构务必安排好!
  • Utils 里的辅助方法不是一成不变的,你可以自己加一些有助于自己开发的函数进去!

Bug 重灾区&&小技巧

  • 忘记保存文件
  • 忘记修改 HEAD
  • 忘记调整 BRANCH
  • 忘记 add 文件到 Stage
  • 生成 id 的时候不考虑实际因素(卖个关子)
  • 忘记 Java 中对象指针值传递的本质(即浅拷贝)
  • EC 时忘记修改 CWD
  • 新建文件和目录可以在 Utils 里新加函数,避免反复写 try-catch
  • Commit 的时间戳需要修改格式,记得写辅助方法
  • EC 后你的 branches 文件夹可能包含子文件夹,这时候可以修改那个读文件的辅助函数,保证 status 命令不会寄
  • 代码复用,事实上 reset,fetch,push,pull 这四个命令几乎不需要怎么写,逻辑写出来,然后发现可以代码复用,就问你爽不爽吧。这也是之前说要考虑清楚再动笔开始写代码的原因所在

总体思路

/.gitlet 仓库安排

根据文档要求,gitlet 只处理平面文件夹里面的内容,不考虑递归处理子文件夹,那么可以讲 Blob 和 Commit 直接“平摊”存储在对应文件夹下。Stage,Branch,Remote 分别存储在对应目录。这样的安排或许不是最佳,但是思路理解上还是较为中规中矩的,不是太难。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
*  .gitlet/
* - objects/
* - commits/
* - ...(files of commits)
* - blobs/
* - ...(files of blobs)
* - branches/
* - master
* - ...(other branches)
* - HEAD
* - BRANCH
* - Stages/
* - addStage
* - removeStage

其中 HEAD 和 BRANCH 是储存当前提交 id 和分支 name 的文件,随用随取,随取随读,随读随改,随改随存

类的分配

我特别喜欢写类,某种意义上写一个好的类也是代码复用的体现所在,所以有的地方或许可以用一个列表直接拉过去,但是我还是写了相应的类便于理解操作

  • Main:主程序所在地,处理输入命令参数
  • Repository:大部分代码所在地,实际编写处理命令的方法的地方
  • Commit:提交的类,包含 id,文件路径与 BlobId 的映射,Blob 的列表,Message,Time
  • Blob:文件的类,包含文件名字,内容,id
  • Stage:缓存区的类,直接一个 BlobId 和文件路径的映射就完事,再编写一些辅助方法
  • Branch:分支的类,包含分支名以及存储的 CommitId
  • Remote:远程的类,这个类其实可以不要的,但是我强迫症上来了()。包含远程名字和仓库路径

Persistence

对所有需要持久化的类写一个saveToFile()方法就完事,别忘了用就行

(不会有人还没写 Lab 6 吧,不会吧不会吧)

必做部分命令思路

太简单的就不写了罢……

init

虽然说测试中只检测了你的/.gitlet 目录,但是还是有更多工作要做,按照文档一步步来

  • 检测文件夹是否存在,存在就退出

  • 创建一个新的分支 master,这也是初始默认分支

  • 创建初始提交,让 master 指向它

  • 创建各级目录

  • 创建两个空的缓存区

你会发现有的东西涉及到后面的命令,这时候可以把写不了的部分打一个 TODO,后面来补

推荐用时:1-2h,加上理解以及写各类可能会到 4h

后面的每个命令都需要检查文件夹是否存在了,不能不存在

add

虽然说只需要将对应文件添加到缓存区,但是仍然要考虑其他的情况

  • 先检查该检查的
  • 如果文件在 removeStage 里,移出(涉及到 rm 命令,可以留白)
  • 创建新的 Blob(这些对应的类你应该已经写好十之八九了)
  • 添加到 addStage
  • 不要忘了保存

推荐用时:1h

commit

第一个坎,建议写了 rm 后再来写,因为 rm 可以看作是 add 的逆命令,而 commit 和这两个都有关系

  • 构造函数可以写两个,一个用于普通 commit,一个是初始 commit 专用
  • id 生成记得用toString()一下类里的其他成员
  • 先检查
  • 生成新的 commit,这时候它的内容除了时间和它的 parent 是一样的
  • 根据两个 stage 修改内容
  • 修改 HEAD 与当前分支的内容
  • 保存

推荐用时:2h

rm

有了写 add 的经验写这个应该比较快,注意分类讨论:

  • 在 addStage 里,删除之

  • 不在 addStage 里,但在当前 commit 里(即最新 commit),加入 removeStage,commit 的时候删除

  • 是上面第二种情况的话,CWD 中不能存在该文件,即存在就删掉

推荐用时:1h

log && global-log

  • 前者 DFS,后者直接用 plain 那啥的辅助函数

  • 可以考虑给 Commit 类写一个print()的辅助方法

find

这个简单,正式 git 都没有,考你对自己写的类的掌握情况呢

推荐用时:10min

status

这个分类有点多,最后两个还是 ec 的部分,一步一步来即可

  • branch:直接读目录里的文件就行
  • 两个 stage:把 stage 的文件读出来就行,也可为 Stage 类写一个print()的辅助方法
  • 修改未保存:按照文档分的四类一步步写即可,注意 blobId 即可代表两个文件是否相同
  • 未跟踪:读 CWD 文件,一个一个判断即可

推荐用时:20min(无 EC),1h(含 EC)

checkout

三个类型一步步写,其实这里建议在两个 branch 命令之后写

  • 检出文件:建议写一个辅助方法,后面会用到
  • 检出某个 commit 里的某文件:同上
  • 检出某个分支:再写一个辅助方法,完成这里这个命令的同时也为后面 reset 服务

推荐用时:4h(debug 麻了)

branch && rm-branch

虽然 branch 的概念很高大上,复杂,但是不妨碍这两个命令很简单啊!稍加思考即可发现:branch 功能本身的实现是在 merge,checkout 一系列命令的加持下才成型的。对于这两个命令,我们只需要做该做的事即可

  • branch:创建这么一个对象,保存
  • rm-branch:找到这个文件,删掉它
  • 记得检查!
  • 没了

建议用时:30min

reset

提示:用 checkout 的某类情况的辅助方法,实际上这个 reset 也是真实 git 的 checkout 的功能之一(但是不会头分离,最接近 reset –HARD)

建议用时:10min

merge

必做部分的最大 boss,推荐先观看视频,深入了解 merge 的对每个文件处理的八个情况分别是什么,再拟出自己的思路,一步一步写,并且随时 debug,这样可以减轻自己的痛苦时间()

我的总体思路如下:

  • 检查
  • 处理 easy cases
  • 用 BFS 获取 splitCommit
  • 现在手上有三个 commit:分裂点,待合并的 branch 指向点,当前点
  • 获取其中所有文件,一一处理,这一步我的选择是返回一个 result 的 map,因为我还没检查,要检查后才能操作
  • 检查 result 是否合法
  • 检查完毕,根据 result 的指示处理文件(考虑用一个 flag 记录是否发生 conflict)
  • 处理完毕,创建 merge commit
  • 根据处理结果(两个 stage)修改 commit
  • 保存之,若 conflict 则打印相关信息
  • 清空缓存,设置 HEAD,BRANCH 文件,欢呼!

对于文件处理,牢 TA 的分类:

我来给它升个级:

  • 对于每一个文件,它在 splitcommit,branchcommit,currentcommit 均有相应的形态(不存在也是形态之一)
  • 若三者皆不相同,就是 conflict!
  • 若三者皆相同——那讨论个毛线!
  • 若两两相同,进一步分类:
    • s 与另两个之一相同,保留第三者
    • s 与另两个都不同,但另两个相同——保留 current 即可(即不做操作)

一下子就只有三类了,明了了许多对不对

推荐用时:6h,但是 debug 无上限(我自己就几个小 bug,比如忘记写!,忘记 add,却 debug 了五六个小时)

恭喜!1600 分到手!

选做部分命令思路

总体不难,因为有大量代码复用,注意规划好类与文件夹分区即可

总体思路:每个 remote 是一个对象,里面包含了 remote 的名字和远程仓库路径。编写一个改变当前 CWD 的辅助函数用于操作 fetch,push 与 pull。似曾相识的地方就代码复用,必要时小改之前的命令用于抽出辅助函数复用。

具体命令思路:

add-remote && rm-remote

类比 branch 那边,虽然 remote 是一个大的复杂的概念,但是这俩命令不难呀,按自己的思路写好就行,我的思路:

  • add-remote:创建 remote 对象,保存(文档要求不用检查合法性)
  • rm-remote:找到那个 remote 文件,删掉!(记得先检查这个 remote 是否存在)

推荐用时:30min

fetch

我的思路里,核心在于改变 CWD 以及复制 commit 和 blob 们(复制 commit 时我出了超级逆天爆冷大 bug,下面详谈)

fetch 的要求是将远程的分支拽过来但是不合并,合并是 pull 的事情,咱 fetch 不用管,自然有了如下思路:

  • 检查文件夹以及参数是否合法
  • 拿到远程分支,用 BFS 取到它的历史(代码复用 merge 的部分)
  • 回到本地工作目录
  • 开始复制!注意!复制的时候不能只傻傻地把文件拉过来,还要修改内部文件的路径(注意这里并不会影响 id 生成,思考这是如何实现的?),不然你后面 checkout 的时候有好果汁吃,就是这里吃掉我 2h 的 debug 时间
  • 修改本地远程跟踪分支,若不存在则创建,注意文件名不能有/,所以我直接创建的子文件夹
  • 上述过程中注意随时切换 CWD

推荐用时:3h

push

类比为 fetch 的反向特化版,因为它的条件要求更苛刻,并且复制那一部分的逻辑是一样的,思路如下:

  • 先老规矩,检查
  • BFS 获取本地分支历史(和上面不一样吧,但是直接美美复用)
  • 检查远程分支是否存在
    • 若是,获取那边的头部,检查是否在本地历史中
    • 若否,直接进入下一步
  • 向远程仓库复制 commit 和 blob
  • 修改远程分支,不存在则创建,记得保存
  • 没有啦

推荐用时:1h

pull

文档已经写好了:fetch + merge,两行代码搞定!

推荐用时:2 min 30 s (只因代码复用太美)

恭喜!完整版 Gitlet 已完成!

总结与收获

代码量的话,我的 repo 类是 1200 行,不算 ec 是 900 行。如果少啰嗦一点可以更节约

总体难度其实不大,主要是量确实大,以及起步很难,不愣个一天神是不行的

收获真的很大,最重要一点就是克服了之前就有的项目恐惧症——

  • 总觉得 61A 的项目太小儿科,对自己的码力没有自信
  • 面对大项目就发怵,心里没底
  • 写了一堆代码后很害怕 debug,不知道它会藏在哪里暗杀我

除此之外便是一次完整的超级 mini 版的开发体验,用 git 做版本控制工具写 gitlet,真是奇妙的体验呀!

记录自身成长的同时,也希望能够帮助到你!谢谢!


Gitlet之我见
http://example.com/2024/08/21/gitlet/
作者
思源南路世一劈
发布于
2024年8月21日
许可协议