[CS61B pro2](https://sp21.datastructur.es/materials/proj/proj2/proj2)
由于大部分内容是在编写完程序后很长一段时间完成的,时间久远,故仅做简要描述,且可能一些描述会出现偏差(以后在写程序的时候一定要完善好文档!!!)
经过这个项目的编写,极大提高了使用Java编写程序的能力,增加了对Java的了解,同时第一次从头到尾写了一千多行的代码量的程序,对编程能力有了极大的提升,也充分体会到了需求分析、系统分析、系统设计的重要性。由于在编写程序前没有进行详尽的分析,导致一些代码的功能出现了一定的重复,未能比较好的增强代码的复用性,同时在一些功能的编写过程中,由于未进行非常深入的分析,出现了一定的困难,花费了大量时间才完成了该功能的编写,浪费了大量时间。并且,在缺少足够建模分析的情况下,debug过程及其不友好。
Gitlet的代码完成情况:
除了Utils类中提供了各种读写文件的IO接口,其他部分大部分是独立完成的,少部分由于对Git本身功能没有充分的了解,借助了一些资料
实现了init、add、commit、rm、log、global-log、find、status、checkout、branch、rm-branch、reset、merge
## 设计思路
如何保存版本信息?
在根目录中建立.gitlet文件夹,保存相关信息
文件如何保存?
作为一个文件版本控制系统,其目的就是为了对每个版本的文件进行管理,显然,为了保持系统的简化,不可能将每个版本的每个文件都进行存储,只需要保存内容不同的文件即可,而对于不同版本的相同文件,只需要同时指向一个文件即可。而为了能够比较各个文件的异同,可以采用Hashcode来标记每个文件,即使可能出现不同文件Hashcode相同的情况,不过其可能性微乎其微,可以忽略这种情况,也就是只要两个文件的内容是不同的,那么这两个文件的Hashcode是不同的
如何记录版本?
每个版本都具有追踪上个版本的能力,因此将各个版本以链表形式存储下来,建立一个Commit类,该类存有指向该版本所有文件的HashMap,还有指向上一个版本的指针,还有一些基本信息,包括本次提交的信息、提交时间以及该提交的Hashcode
当出现多个分支时如何记录各个分支的信息?
在.gitlet文件夹里再建立一个文件夹存各个分支的Hashcode信息
如何追踪当前指向哪个版本?
将当前版本的Hashcode以文件信息保存
## Internal Structures
- 文件夹blob:存储每个文件的各个版本,文件名为Hashcode
- 文件夹branch:存储每个分支的头节点,内容为节点的Hashcode,文件名为分支名
- 文件夹commit:存储每次提交的信息,文件名为Hashcode
- 文件夹remove:存储删除文件的信息
- 文件夹stage:存储add后的信息
- 文件HEAD:存储当前节点的Hashcode
- 文件HEAD_BRANCH:存储当前分支名
## Dealing with Files
This project requires reading and writing of files. In order to do these operations, you might find the classes `java.io.File` and `java.nio.file.Files` helpful.
## 关于Utils类提供的一些处理文件的方法
```
String sha1(List<Object> vals)
```
返回vals的哈希值
```
boolean restrictedDelete(String file)
```
如果file存在并且不是目录,则删除文件。如果文件被删除返回True否则False
file所在的目录必须还包含.gitlet目录,才能删除成功,也就是在使用该函数时,文件必须在初始化后的文件夹内
```
String readContentsAsString(File file)
```
将文件file的内容以字符串的形式返回
```
void writeObject(File file, Serializable obj)
```
把obj写入file
```
List<String> plainFilenamesIn(String dir)
```
以列表形式返回dir中所有的plain files的名字(以字典序),如果目录中无文件,返回null
```
File join(File first, String... others)
```
将first和others连接
```
GitletException error(String msg, Object... args)
```
```
/** Return a GitletException whose message is composed from MSG and ARGS as
* for the String.format method. */
```
```
/** Print a message composed from MSG and ARGS as for the String.format
* method, followed by a newline. */
static void message(String msg, Object... args)
```
## Gitlet
命令失败案例:
- 如果用户未输入任何参数,则打印消息 `Please enter a command.`并退出。
- 如果用户输入不存在的命令,则打印消息`No command with that name exists.`并退出。
- 如果用户输入的命令带有错误的操作数数量或格式,则打印消息`Incorrect operands.`并退出。
- 如果用户输入的命令需要在初始化的 Gitlet 工作目录(即包含`.gitlet`子目录的目录)中,但不在这样的目录中,则打印消息`Not in an initialized Gitlet directory.`
## 具体功能编写
### initO(1)
这是Gitlet初始化功能,将文件夹创建一个仓库
**Failure cases**:已经存在版本控制系统后,不能重写已经存在的系统,应该打印报错信息:A Gitlet version-control system already exists in the current directory.
- 首先需要建立文件夹:.gitlet、blob、commit、stage、branch
- 随后提交一个init_commit
- init_commit不包括任何文件,提交信息为init_commit,时间为默认时间
- 将HEAD指向init_commit,写入HEAD文件
- 初始化branch文件夹,创建master分支
### add
目前只支持单个文件的add
**Failure cases**:文件若不存在,`File does not exist.`,不做任何改变
对于add操作,分以下情况讨论:
- 如果add的文件与当前commit存的文件相同
- 不会将文件添加到暂存区
- 若暂存区已经有该文件的其他版本,删除该文件的其他版本
- add的文件与当前commit存的文件不同
- 当add的文件暂存区不存在,直接将该文件加入暂存区
- 当add的文件在暂存区有副本,
- 覆盖暂存区的文件
add会将文件以Stage类的形式存入暂存区,存有文件内容,文件名字,Hashcode信息,以原文件名字作为暂存区文件名
### commit
**Failure cases**:
- 没有文件staged,`No changes added to the commit.`
- 没有message`Please enter a commit message.`
commit带有message、时间、父节点、BlobNode的hashmap、commit的hashcode的信息
在blob的hashmap中,存有文件名到文件hashcode的映射,以便于能立刻通过文件名找到当前commit对应的文件版本
在赋值commit父节点时,直接把HEAD值赋上
在处理BlobNode时,先取到父节点的BlobNode,将父节点的BlobNode与暂存区文件对比,进行处理。
将暂存区文件以Stage形式读取,再以Blob形式存于Blob文件夹中
更新HEAD、branch的信息
新建一个文件,add commit rm后
再新建相同文件,add,commit的时候发现问题,parent应该size为0,但是实测为1
### rm
情况讨论:
文件刚被`add`进`addstage`而没有`commit`,直接删除`addstage`中的Blob就可以。
文件被当前Commit追踪并且存在于工作目录中,那么就将及放入`removestage`并且在工作目录中删除此文件。在下次`commit`中进行记录。
文件被当前Commit追踪并且不存在于工作目录中,那么就将及放入`removestage`并即可(`commit`之后手动删除文件,然后执行`rm`,第二次`rm`就对应这种情况)。
被master追踪,未被other追踪,checkout后文件不应该删除
### logO(n)
输出内容格式:
```
===
commit a0da1ea5a15ab613bf9961fd86f010cf74c7ee48
Date: Thu Nov 9 20:00:05 2017 -0800
A commit message.
===
commit 3e8bf1d794ca2e9ef8a4007275acf3751c7170ff
Date: Thu Nov 9 17:01:33 2017 -0800
Another commit message.
===
commit e881c9575d180a215d1a636545b8fd9abfb1d2bb
Date: Wed Dec 31 16:00:00 1969 -0800
initial commit
```
比较简单,直接从当前节点一路遍历到最初始节点,把遍历过的每个节点信息格式化打印。需要注意的是由于merge操作的存在,会出现一个节点有两个父节点的情况,在这种情况下,选择parentList第一个节点
### global-logO(n)
不按顺序地打印所有Commit信息,直接读取Commit文件夹里的所有Commit,并打印出来
### findO(n)
```
find [commitmessage]
```
**Failure cases**:如果输入message的Commit不存在,则打印 `Found no commit with that message.`
打印出所有和输入message相同的Commit的Hashcode,遍历所有Commit文件,打印出和输入message相同Commit,功能和global-log相似,可以部分复用其代码
### status
Displays what branches currently exist, and marks the current branch with a `*`.
```
=== Branches ===
*master
other-branch
=== Staged Files ===
wug.txt
wug2.txt
=== Removed Files ===
goodbye.txt
=== Modifications Not Staged For Commit ===
junk.txt (deleted)
wug3.txt (modified)
=== Untracked Files ===
random.stuff
```
后两项功能未实现
第一个读取branch,就可以获取所有分支名,通过HEAD_BRANCH即可获取当前分支名
第二个第三个直接读取stage文件夹和remove文件夹即可
### checkout
有三种情况:
>java gitlet.Main checkout -- [file name]
如果当前提交追踪的文件包含filename,则将其写入到工作目录:如果同名文件存在,overwrite它,如果不存在,直接写入。
**Failure cases**:当前的Commit中不存在filename的文件,打印`File does not exist in that commit.`
>java gitlet.Main checkout [commit id] -- [file name]
从commit id对应的Commit追踪名为file name的文件执行和第一种情况类似操作
**Failure cases**:如果commit不存在,则`No commit with that id exists.`如果这个文件在输入的Commit中不存在,处理方式和第一种情况一样
>java gitlet.Main checkout [branch name]
切换到对应的branch,工作目录文件全变为新branch所包含的文件,涉及到比较复杂的工作目录文件处理
原先为Commit1,经过checkout指向Commit2
工作区文件处理:
1. 文件名被Commit1和Commit2追踪,如果两个Commit中该文件的Hashcode一致,不做任何更改,若不一致,用Commit2的文件替代Commit1的文件
2. 文件名被Commit1追踪,不被Commit2追踪,删除该文件
3. 仅被Commit2追踪,将该文件放入工作目录。对于该情况有特殊情况要处理:如果该文件的文件名在工作目录存在,只是还未提交到,那么这个文件不能覆盖,中止操作并输出`There is an untracked file in the way; delete it, or add and commit it first.`
**Failure cases**:如果branch name不存在,`No such branch exists.`如果branch name是当前分支,`No need to checkout the current branch.`
### branch
>branch [branchname]
新建一个branch,在branch文件夹中加入新的branch
### rm-branch
>rm-branch [branchname]
删除branch
**Failure cases**:如果删除的branch为当前分支,`Cannot remove the current branch.`如果branchname不存在,`A branch with that name does not exist.`
### reset
>reset [commitID]
将版本回到commitID,将工作区文件进行更新,HEAD指向commitID,工作区更新和checkout类似,复用checkout
**Failure cases**:commitID不存在,`No commit with that id exists.`还有一种无法重写文件的错误,和checkout面对的情况一样
### merge
第一个问题是需要找到分裂点
对于分裂点的寻找方法,如果用循环暴力寻找,则有n^2的时间复杂度,不满足要求。
可以通过HashMap的特定,将当前分支和给定分支的父节点放在两个Map中,从一个分支的Map里遍历,查看遍历到的节点是否在第二个Map也存在,如果存在,则是分裂点。利用了HashMap的性质,降低了时间复杂度
1. 文件a在分裂点为A,HEAD分支为A,在给定分支为!A,合并后结果为!A,从HEAD的A变成!A,stage for addition
2. 文件a在分裂点为A,HEAD分支为!A,给定分支为A,结果!A,不暂存
3. 文件a在分裂点为A,HEAD不存在,给定分支不存在,
1. modified in OTHER but not HEAD --> OHTER (stage other for addition)
2. modified in HEAD but not OTHER -->HEAD
3. modified in OTHER and HEAD
1. in same way --> same as the HEAD and OTHER
2. in diff way
4. not in split nor OTHER but in HEAD --> HEAD
5. not in split nor HEAD but in OTHER --> OTHER
6. unmodified in HEAD but not present in OTHER --> REMOVE
7. unmodified in OTHERbut not present in HEAD--> REMAIN REMOVE
| FILE | SPLIT | HEAD | OTHER | RESULT | STAGE |
| ---- | ----- | ---- | ----- | ------ | --------------------- |
| a - | A | A | !A | !A | stage !A for addition |
| b - | B | !B | B | !B | NO |
| c - | C | NULL | NULL | NULL | NO |
| d - | D | D | NULL | NULL | stage D for removal |
| e - | E | NULL | E | NULL | NO |
| f - | NULL | NULL | F | F | stage F for addition |
| G - | NULL | G | NULL | G | NO |