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[])。