CS61B GITLET PROJECT
简化版Git,深入探索Git的底层结构
Project 2 Getting Started(Lab6)
Preperation
首先使用git submodule update --init --recursive命令将21sp的library更新,再一并复制过来。记得再当前操作系统中设置REPO_DIR环境变量为所有projA、HW的根目录。切记要注意看文档, 以及其中给出的设置来实现相应的功能, 最后才发现FAQ也给了非常好的提示,另外比如说在我的机器上使用python而不是python3,make check是跑不通的,需要找到Makefile的第25行, 修改为PYTHON = python。
1 | java capers.Main story "Once upon a time, there was a beautiful dog." |
准备开始Gitlet:
Gitlet
参考资料
- Git pro book
Note
- 往年课程的Slide很好地以图片的形式介绍了
Gitlet中的各个命令的实现。 - Gitlet Persistence
- 这里有Git比较详细的图解。
- 仔细阅读
gitlet.Utils中封装好的方法。 - 编写设计文档,规格和示例
- Git的内部结构
- blob(Binary Large Object): 文件的保存内容。一个文件可能对应多个blob:每个blob在不同的
Commit中被跟踪。 - Tree: 映射文件名称(name)到blob的reference,或者是映射文件名称到其他tree(子目录)的引用。
- commits: 日志消息,其他元数据(提交日期、作者等), 对树的引用和对Parent Commit的引用。
repository还维护了分支头到提交引用的映射(以便某些重要的提交具有符号名称)
- blob(Binary Large Object): 文件的保存内容。一个文件可能对应多个blob:每个blob在不同的
- Gitlet简化的部分
- 将树合并到Commit中而不处理子目录
- 只能两个父级合并
- 元数据仅包含时间戳和日志消息,因此Commit将由
日志消息,时间戳,文件名到Blob引用的映射,父引用, (用于Merge的)第二个父引用来组成。
- 每个blob和每个commit都有一个唯一的整数id,用作该对象的引用。
- 当使用SHA-1哈希一个Commit时,会包括所有的数据和引用。
- 区分
Commit和Blob的哈希值,一种方法是可以在两个类中各自定义一个属性字段来实现。 - 不应该在Main中做完所有事情,而是将需要实现的内容封装成一个函数到
Repository类中去。 - 序列化最好使用
TreeMap而不是HashMap。 Gitlet不会出现deteched head(分离头部)的状态。指的是当前HEAD指向的Commit不表示任何一个分支。
注意事项
- 在
terminal下出现了Esc无法退出insert mode而且还进入了Itellij的编辑界面。解决方案看此处 - 暂存区包含两个区域,
stage for addition和stage for removal。 gitlet中一条命令对应运行一次程序,因此需要将用到的数据结构序列化为字节流后保存到文件中。- 在实验一开始给的
Utils类中, 不需要使用createNewFile()来创建文件, 因为writeObject()中会调用WriteContent(),若文件不存在则会自动创建,或者文件存在则覆盖掉它。 - 注意要在当前目录下执行
make check,测试文件可能跑通(可能需要操作src/目录中的文件)。 - 注意每次更新数据时都需要序列化对象和反序列化文件, 保证更新的数据能够及时存储到文件中。
- 也需要将
blobs的映射集合序列化存储起来(同样用TreeMap),checkout命令要使用(用SHA-ID(文件名+文件内容)来作为文件名)。在add命令将blob和文件名映射放入stage for addition的同时,将blob和文件内容的映射放入blobs集合。 blob生成blobId(sha-1 id)的时候,需要通过文件名+文件内容(字节流)生成。若只用文件内容生成blobId,不同名的两个文件内容都为空时,生成的blobId是一致的, 使用文件名+文件对象生成的sha-1哈希也是同样的结果。mkdirs()方法可以一同创建之前未存在的父目录。- 注意写文件内容时用
writeContent()而不是writeObject()。注意到writeObject会在调用writeContent前将第二个参数先序列化, 若将字节数组再序列化,可能会在内容中写入额外的信息。 Untracked file,既没有被当前commit跟踪,也没有被放到stage area里。或者是已经被stage for removal但是又重新创建了。(考虑到我这里rm情况下没有将其从暂存区删除,仅仅是将它从stage for removal删除了而已)。- 保证分支头只有一个,
get(0)可以直接取(这里我将分支做所处的位置做成了一个目录,目录中是用分支名来命名的文件, 方便直接取头Commit)。 - 暂存区使用
blobId映射文件对象不可取(序列化后提取的文件内容还是的却决于当前工作目录下的内容,不符合预期),试试映射文件内容(字符串readContentsAsString)。经过调试发现必须得映射字节流,因为前面生成blobId中需要文件名+文件内容(字节流)。 - 注意
Utils中的restrictedDelete()方法,是如果文件存在则删除, 如果文件对象不存在则删除未成功返回false, 给我们实现rm指令来删除当前工作目录中的文件提供了一个很好的帮助。 - 需要在每次
commit命令执行时都更新分支头目录中的数据以及其对应branches目录中的数据。 - 除了
init之外剩下的命令在处理之前都需要检测当前.gitlet目录是否存在, 若不存在则输出报错信息。 - 在开始项目任何一个功能前一定要有一个
Big picture和整体思路,确保在实现过程中能够顺利进行。
测试
- 查看
tester.py文档中一些相应的操作来对应测试文件。测试文件可以从AG中获取。
init
- 创建
.gitlet目录,并在其中新建一些文件来存储序列化信息。 init会自动创建一个包含initial commit信息的Commit开始
add
- 暂存已经暂存过的文件,会用新内容来覆盖先前的内容。
- 若当前需要暂存的文件与Commit跟踪的文件版本一致(包括其修改之后又还原内容的文件,blobId都是一致的),则不对它进行处理。
- 若当前添加文件在
Stage for removal中,则将它从里面删除。 - gitlet一次只能添加一个文件。
- 维护一个
TreeMap来存放stage for addition文件。
commit
- 需要考虑在当前跟踪的文件可能在新的Commit中未被跟踪,与rm指令将文件
Stage for removal相关。 - 当前Commit跟踪的blob需要加上其父Commit跟踪的blob(前提是父Commit中跟踪的blob未被当前Commit跟踪的文件覆盖过)。
commmit后将stage area清空,也就是stage for add和stage for removal。- 若没有文件被暂存,或没有提交信息则中止。
rm
- 若当前文件正在
stage for addition,则将它从里面移除。 - 若文件在当前
Commit中被跟踪,则将其放入stage for removal暂存区中, 同时将本地目录文件删除。 - 只考虑被当前Commit跟踪的情况下
- 维护一个
TreeMap来存放stage for removal文件。
log
- 注意显示的是太平洋标准时间而不是UTC。
- 当前操作系统显示时间得调成英文显示,否则打印log会出现中文字样
find
- 通过
Utils类中给的方法plainFilenamesIn, 直接遍历commit Id所处的目录,若出现commit对象不同但提交信息一致的打印情况,分行打印各自的commit id。
status
TreeMap数据结构会按照字典序来排序。Untrack file,既未跟踪也未暂存。未跟踪将当前目录下的文件名和当前最新Commit中track的文件名做对比。若未包含则说明未跟踪。同样包括已经准备删除但又重新创建的文件(说明其还在stage for removal里,因为stage area只在commit时删除)。思考该该怎么标记? rm之后add的文件?modified, 同样与Commit中track对比,若文件名相同,但BlobId不同则说明被修改过。- 遍历当前跟踪的文件,使用
exist判断文件对象是否存在以及是否存在于stage for removal,就能判断文件是否被delete。
checkout
- 有三类
checkuout:- java gitlet.Main checkout – [file name]
- java gitlet.Main checkout [commit id] – [file name]
- java gitlet.Main checkout [branch name]
- 若
checkout切换分支,且当前目录下文件有文件未被跟踪,则打印提示信息并退出。 - 注意切换完成,将新分支放入
heads目录的同时,从heads目录中删除先前的分支。 checkout filename将当前文件从head commit中拿到当前工作目录。- 切换到另一个分支要将暂存区清空。要将当前commit中跟踪了但切换到的分支commit中没有跟踪的文件删除(如果有则覆盖)。
- 考虑两种情况:
checkout的目标分支跟踪的文件比当前目录的文件多,直接写就行了checkout的目标分支跟踪的文件比当前目录的文件少,需要删除本地目录中多余的文件- 这两中情况都需要遍历当前目录中的所有文件名来确定。
- 一个错误的策略是切换之前先将当前当前文件下所有的都删除,然后再将切换到的分支commit跟踪的文件写进CWD当前工作目录, 在后续将跟踪文件写入当前工作目录时,取不到文件对象了。
checkout branchName时需要将暂存区清空。- 后面还有一个
checkout shortid的测试,需要包括通过commitid的前8就能切换到不同的分支。和commitId一样序列化到磁盘上的文件就行。 - gitlet不允许删除提交。
reset
- 神似
checkout branchName, 只是最后是将当前branch回退而不是切换分支。
branch
- 只新建一个分支(即在branches目录中新建一个序列化的分支Commit文件), 但是并未切换到新建的分支。
merge
- 将给定分支名为branchName的文件合并到当前分支中。
- 如何找到
split point? 考虑到gitlet只支持两个分支合并,因此可以把新建分支的commit作为split point?! hhh笑死ucb学生视频里第一个提问和我想得一模一样。我感觉这种方法在gitlet中可行,但在多分支合并时就不行了。还是得去过一遍图的遍历这一节的slide。 - Latest common ancestor其实指的也就是
split point。 - 如果给定分支是
split point则不做任何处理。 - 无论是否发生冲突,
merge结束后新建一个Commit。 - 如果当前分支是
split point, 则切换到给定分支。 - 发生冲突且修改的内容不同只写当前分支的冲突文件。
- 需要复用
add和rm以及commit命令。 - git提交后的分布可以看作是个
direct graph。 - 可以通过将从
init commit到当前分支经过的commit放到一个list中,同时将要给定分支的路径也放到一个set中,再通过按指定顺序遍历(由当前分支开始)当前分支的list,使用contains方法来确定遍历经过的commit是否在分支路径的set中。需要使用BFS来遍历。 - 之前的策略是在切换分支(checkout, reset)时才将branches目录中的当前分支信息更新,这在这一步中获取当前分支对象是危险的,因此需要调整策略,在每一次commit更新HEAD目录时和reset时,都更新Branches中当前分支信息。这样就能保证当前分支在Head目录和Branches目录中的信息是同步一致的了。
- 注意Java中
null在字符串比较时不能用equals()来比较。 - 别忘了改写冲突文件时需要生成新的
blobId放入暂存区中在新的合并Commit中加入当前目录。
处理case

- 想到了一个比较情况的好方法,将当前分支,给定分支以及split point三个集合中含有的文件名的并集放到集合里。比较时需要各个集合的BlobId和文件名的映射,以及BlobId和content的映射。
- 由两个值来区分所有情况分别是
isPresent和IsModified。 Modifed只需要通过当前分支分支或给定分支, 与split point blobId比较来区分就行了, 程序里我将不存在的文件的blobId赋值为空串。- 当前分支和给定分支都modifed也就是case3还需要细分发生冲突,还是不发生冲突。直接通过比较当前分支和给定分支
commit Id即可。 git pro中的原话, 此时 Git 做了合并,但是没有自动地创建一个新的合并提交。 Git 会暂停下来,等待你去解决合并产生的冲突。 你可以在合并冲突后的任意时刻使用 git status 命令来查看那些因包含合并冲突而处于未合并(unmerged)状态的文件。这点与Gitlet还是不同的。- 发生冲突的内容差异会自动写入冲突文件(按合理性来说只需要写入当前分支头即可)如, 需要解决冲突后,需要打印提示信息,合并的提交跟踪冲突文件:
1
2
3
4
5<<<<<<< HEAD
contents of file in current branch
=======
contents of file in given branch
>>>>>>> - 处理好每个情况后将每个文件对应的result添加到暂存区等待新的Commit。result的内容为当前分支的内容也需要
stage for addition, 但不需要放入blobs集合中。 getBytes方法可以将字符串转化为字节数组(byte[])。
