# java_git
**Repository Path**: xx199976/java-course-assignment
## Basic Information
- **Project Name**: java_git
- **Description**: 基于Java的Git实现
- **Primary Language**: Java
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 1
- **Forks**: 0
- **Created**: 2021-10-23
- **Last Updated**: 2022-04-12
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# Java_Git课程项目文档
### 项目概述
基本完成了三次JAVA三次项目布置的内容,实现的思路基本参考了git的实现,在编码的过程中尽量做到还原本味。一共实现了13个jit命令以及一部分子命令:jit init,jit add,jit rm,jit status,jit cat-file,jit ls-files,jit log,jit commit,jit reset,jit branch,jit checkout,jit diff,jit merge。其中jit cat-file,jit ls-files的实现都不复杂,但是是对验证操作的正确性很有帮助。我们依然不能避免一部分的bug,当前仍存在部分问题被记录在[当前存在问题部分](#当前存在问题)。
### 小组分工详表
| 姓名 | 学号 | gitee账号 | 分工(已完成任务) |
| ------ | ---------- | ---------- | ------------------------------------------------------------ |
| 蔡扬航 | 2100022724 | yhcai | JitMerge、JitLog、JitRM[(default), --cached, -f]、Commit、Repository、JitHash、测试、CLI部分方法 |
| 范文斌 | 2100022723 | FANWB | JitDiff[--cached, (branch), (commit), (file)]、MyersDiff、Branch、JitBranch[-d]、JitCheckout[-b]、JitCommit、Index、IndexElement、JitAdd、Tree、测试、CLI部分方法 |
| 章江虹 | 2100022786 | ZhangJH | JitStatus、JitReset[--hard, --soft, --mixed]、JitCatFile、JitLsFile、JitObject、Blob、Tree部分、测试、CLI部分方法、两次汇报 |
### 收获心得总结
- 蔡扬航
由于对Java和Git不够熟悉,一开始接触这一实践项目可谓困难重重。在老师、助教的指导以及小组成员共同学习的影响下,我对Java和Git有了更深入的了解。在这一项目中,主要有以下几方面的收获和感悟:
首先,编程时应该考虑数据结构的效率。实现同一功能的方式五花八门,但不同的数据结构效率差异明显。例如在本项目的JitMerge类,使用HashMap存储文件名、hash码键值对,能够较高效地判断某个文件是否存在于某次commit中。
其次,编程时除了关注程序逻辑和数据结构的效率外,还应该在一定程度上关注用户体验。在git之类的命令行交互项目中,用户体验某种意义上也是代码质量的体现。例如,在没有commit记录的情况下输入jit log指令,按照编程逻辑应该抛出异常。若从用户体验角度考虑,还应该捕获异常并输出相应的文字提示,而非直接抛出异常并终止运行。
此外,使用调试工具能够提高debug的效率。起初,我通过直接在程序中加入输出语句的方式,根据输出信息判断程序的bug所在。在简单的项目中,这种”输出调试法“较为灵活,但在程序逻辑稍复杂的项目里,这种方式效率较低。
此外,本次项目实践也培养了我从整体架构的角度看待项目的能力,对团队协作和文档撰写也有了更加深刻的认知。
- 范文斌
本学期Java课程中的Jit项目实践具有很强的综合性,在经历了学习、设计、实现、测试及文档编写等一系列软件开发流程之后,我在多个方面都得到巨大的收获。
首先,实际的项目开发使我对Java课上所学的各类知识和理论都有了更加深刻的理解。本次实践当中,面向对象的封装、继承、多态等核心理论都有不同程度的运用,集合容器、文本IO、异常处理、抽象类等知识也得到了充分的巩固。在Jit项目的开发过程中,真正做到学以致用是我的最大收获,也给了我更强的学习动力。
其次,本次的实践项目让我对Git版本控制工具的内部原理有了全面系统的了解,同时在开发过程中也始终在使用Git工具与小组成员进行团队协作,这样的结合让我更直观地感受到了本项目的实际意义,也使我认识到团队协作的重要意义,并对如何更加高效地进行协作有了新的思考。
最后,在实操层面,规范的编码风格、有效的代码注释以及详尽的项目开发文档的重要性都在项目进程中得到了生动体现,这些手段不仅能够切实提升代码可读性,同时也能让团队协作过程中的沟通效率得到显著提升。
- 章江虹
经过这个学期的git项目实战,我对git有了更加深入的理解,这不仅限于原理方面的理解,也有对团队协作方面的认识。
当然开发git项目就要先对git有一个自己的认识,所以前期的工作主要还集中在对git原理和java编码的学习上。后续部分主要是编码实现,编码实现的过程非常锻炼心态,一是自己认识到理解别人的代码思路是相当困难的,二是有时候bug是不能很快解决的。以上第一种情况让我认识到面向黑盒编程的可行性,第二种情况让我认识到放平心态以及高效代码调试的必要性。过程中也意识到了文档存在的道理,文档的存在能使得项目更好地被理解,这里也希望自己编写的文档足够详尽。
### 项目设计与亮点
#### 程序目录结构
#### 设计亮点
> 实现思路放在[功能与操作部分](#功能&操作)
>
- 实现jit status,方便查看当前的工作区、暂存区以及版本库之间的差异
- 实现jit reset的多种方法,能对版本更改更加灵活
- myers diff算法实现jit diff,支持分支之间的比较,两次提交之间的比较以及两个文件之间的比较
- jit merge,实现将指定的分支合并到当前分支,并更新工作区内容。
- 前两次的任务的junit单元测试
- jit commit生成树复用没有变更过的blob及tree
#### 类设计
##### gitObject
###### GitObject
Blob,Tree,Commit三个类继承了GitObject类,功能大多在继承类里有重写,需要说明的是GitObject类实现了**Serializable**接口,gitObject下的所有存储均使用序列化操作。此外为GitObject类也写了一个反序列化的方法,以实现**cat-file**方法(查看文件内容和类型),以及为程序运行中判断是哪一种gitObject类型提供便利。
###### Blob
- key
根据源文件内容生成,在**.jit/objects**目录下保存。保存形式为**dc/22740...**(前2位作为目录名,后38位为文件名)
- value
原工作区文件内容
###### Tree
- key
根据value保存内容生成
- value
```bash
100644 blob 615b6d2cb6a134935b1f206a0f8da796a367b435 1.txt
040000 tree 08cbbbbf5ea6a5ac5f5625cf2127f06f4573067d a
```
###### Commit
- key
根据value保存内容生成
- value
```bash
tree d3db93911d8f648bae907af342882fe38ca4512d
parent [parent commit]
author $name $email $time_stamp +0800
committer $name $email $time_stamp +0800
```
- commit生成树复用没有变更过的blob及tree示例
对比两次提交,version2比version1在dir文件夹下多了一个d.txt文件,两棵commit树的没有变动过的blob内容是相同的,sdir(dir的子文件夹)的hash值也没有改变,可以通过cat-file -p 功能查看验证。
------
##### **branch**
###### Branch
定义并重载构造方法,可使用分支名及commit ID新建分支对象,也可使用已有分支名创建分支对象
- branchName
保存分支名
- commitID
保存当前分支所指向的commit ID
- changeHead方法
改写HEAD文件内容,使其指向当前分支
- exist方法
判断分支是否已存在
##### stage
###### Index
> 单例模式实现,主要是为了语义方面的理解,一个仓库只能有一个暂存区
存储结构:**LinkedList indexFileList**
string数组存储内容:
| mode | hash | 文件名 | 时间戳 |
| ------ | ---------------------------------------- | -------------- | ------------- |
| 100644 | b0c0d077305ca4224d439f7689e3699536d901c6 | test\file1.txt | 1638320538312 |
| 100644 | 15b6036fbe419af81d6b3d5f60377ca30bc18b8c | test\file2.txt | 1638320538313 |
###### IndexElement
- 使用IndexElement存储commit时重建的IndexTree中的元素,在其中定义Hashmap类型的indexTree字段用以模拟文件夹的层级结构:
- 若对象为文件,则其indexTree值为空;
- 若对象为文件夹,则使用其indexTree存储其内部子文件夹的层级结构
- | type | field |
| --------- | ---------------------------------- |
| file | String fileName、String blobID、Hashmap indexTree = null |
| directory | String fileName、String blobID、Hashmap indexTree |
------
##### utils
###### setOperation
主要包含一个取两个HashMap差集并返回HashSet的方法,在**jit status**, **jit reset**和**jit checkout**方法中都有使用,便于比较工作区,暂存区,版本库的文件差异。
##### test
> 单元测试文件夹
包含五个类:TestBlob(测试Blob类),TestTree(测试Tree类),TestIndex(测试Index类),TestJitAdd_RM(测试jit add方法类和jit rm,TestRepository(测试创建版本库功能)
### 功能&操作
> 以下所有用法均为在idea下terminal中的使用方法。Idea里运行需要切换到项目文件夹下的out\production\SimpleGit^1.0\目录下运行
#### jit init
- **用法**: Java commander.CLI jit init
- **描述**:在当前目录中新建一个新的jit版本控制系统,提示用户输入`user name`(用户名不能为空)和`user email`(正则匹配一般邮箱的规范)用于`commit`提交中显示`Author`信息,用户信息保存在**config**文件中。
- **二次创建版本库的处理**:用户在当前存在版本仓库的情况下,新建仓库并选择删除原来的版本库,此时原版本库删除,若选择保留原有版本库则退出创建仓库的流程。
- **初始仓库结构图**
```bash
$ tree .jit
.jit
|-- COMMIT_EDITMSG
|-- HEAD
|-- config
|-- description
|-- hooks
|-- info
| `-- exclude
|-- objects
| |-- info
| `-- pack
`-- refs
|-- heads
`-- tags
8 directories, 5 files
```
- 失败情况:无
------
#### jit add
- **用法**: Java commander.CLI jit add \ [\...]
- **描述**:将工作区中的一个或多个文件添加至暂存区。若暂存区保存有相同的文件名,则比较文件hash值,若文件hash值一致忽略此次添加。仅文件内容一致但文件名不一致的文件添加不受影响。
- **示例**:
` Java commander.CLI jit add dir/text.txt a.doc`
` Java commander.CLI jit add .`
- **失败情况**:如果文件不存在,打印错误信息并退出,不做任何改变。
------
#### jit rm
- **用法**: Java commander.CLI jit rm [ --cached | -f] fileName
- **描述**: 参考git的功能,jit rm支持三种模式:(default) / --cached /-f。 在不带参数的默认模式下,先判断暂存区中的文件是否被修改,若已被修改,则提醒不允许删除,否则将文件同时从工作区和暂存区删除。在--cached模式下,将指定文件中从暂存区删除。在-f模式下,将指定文件同时从工作区和暂存区删除,不考虑文件修改情况。
- **示例**:
` Java commander.CLI jit test1 ` : 默认模式
` Java commander.CLI jit --cached test1 `:--cached模式
` Java commander.CLI jit -f test1 `:-f模式
- **失败情况**:
- 若工作区不存在指定的文件,打印信息提醒用户不存在该文件。
- 若暂存区不存在指定的文件,打印信息提醒用户没有匹配的文件。
------
#### jit log
- **用法**: Java commander.CLI jit log
- **描述**: 输出当前分支的commit日志。查找当前commit,并依次查找当前commit的parent commit,打印输出。
- **示例**:
` Java commander.CLI jit log `
- **失败情况**:如果不存在commit记录,输出信息提醒用户不存在commit记录
------
#### jit ls-files
- **用法**: Java commander.CLI jit ls-files [-s]
- **描述**: 打印当前暂存区跟踪的文件,加**-s**参数将输出文件的详细信息,不加参数只显示文件名。
- **示例**:
` Java commander.CLI jit ls-files -s`
` Java commander.CLI jit ls-files `
执行`ls-files -s`打印结果如下
```bash
100644 ae8186228d9074d34726dff81e08bbe22891ae4f 0 dir\sdir\b.txt
100644 0e6bb19b0a22564ec2101caeba73189fdfc2b8b8 0 test1.txt
100644 cea3ff47e1e18a630636ab0c361d268e050f7b66 0 dir\sdir\e.txt
```
- **失败情况**:无
------
#### jit cat-file
- **用法**: Java commander.CLI jit cat-file [ -t | -p | -s]
- **描述**:给定一个4-40位的hash,查看保存在objects目录下的blob,tree或commit信息。
- **-t** 查看类型,输出blob,tree或commit
- **-p** 反序列化输出文件内容
- **-s** 输出原始文件大小(UTF-8)
- **示例**:
` Java commander.CLI jit cat-file -p 486e ` : 查看对应文件内容
` Java commander.CLI jit cat-file -t 486e`:查看对应文件类型
` Java commander.CLI jit cat-file -s 486e`:查看对应文件大小
- **失败情况**:给定一个位数小于四位的hash 或在objects目录下无法匹配到文件,打印` Not a valid object name ${hash}`退出。
------
#### jit commit
- **用法**: Java commander.CLI jit commit -m [message]
- **描述**:提交当前暂存区的文件至版本库。
执行commit打印结果如下:
```bash
[master faab73b] 1st commit
```
- **实现思路**:执行commit时需要根据暂存区信息生成index tree,该tree指向工作区中的blob及tree对象。由于暂存区中仅保存blob对象信息,故需要根据文件名中的路径信息重建暂存区文件的层级结构。定义类indexElement保存暂存区对象的信息,其数据域包含文件名、文件哈希值及哈希表。主要使用哈希表来建立对象间的层级结构,其中键为文件名,值为内部IndexElement对象。若对象为文件,则其哈希表为空,若对象为文件夹,其哈希表保存文件夹内部的子文件及子文件夹信息。
执行commit命令时,对暂存区信息逐一进行字符串切分,递归建立index tree,并读取config文件中保存的userName、userEmail等信息,结合commit message创建commit对象。
- **示例**:
`Java commander.CLI jit commit -m "1st commit"`
- **失败情况**:若用户未提交commit信息 -m [message], 提示并获取用户输入,执行本次提交
------
#### jit status
- **用法**: Java commander.CLI jit status
- **描述**:比较工作区,暂存区以及仓库的文件差异
执行jit status应当显示
- 当前分支名称
- **index**树与当前**commit**树的差异(显示在Changes to be committed下)
- **index**树与**workTree**树的差异
- 工作区中修改的文件但没有重新执行**jit add**添加到暂存区中的显示在Changes not staged for commit下
- 工作区中新增的文件显示在Untracked files下
- 其他提示信息(eg. 当前没有commit节点提示:"No commits yet")
执行`jit status`打印结果格式如下:
```bash
on branch master
Changes to be committed:
new file: dir\b.txt
Changes not staged for commit:
(use "jit add ..." to update what will be committed)
modified: dir\a.txt
deleted: dir\c.txt
Untracked files:
(use "jit add ..." to include in what will be committed)
dir\sdir\aa.txt
```
- **失败情况**:无
------
#### jit reset
- **用法**: Java commander.CLI jit reset [--mixed | --soft | --hard] [\]
- **描述**:jit reset有三个模式分别为--mixed(默认),--soft,--hard模式。
- 改变commit指向
- --soft仅改变指针指向
- --mixed(或不加参数)比较targetCommit下的文件与当前currentCommit下文件差异,改变指针指向并将**暂存区**恢复为与targetCommit同步的状态
- --hard比较targetCommit下的文件与当前currentCommit下文件差异,改变指针指向并将**暂存区及工作区**恢复为与targetCommit同步的状态
- 不改变commit指向
- --mixed(或不加参数)撤销`jit add`的操作
- --hard 撤销`jit add` (添加到暂存区)的操作并将工作区所有的变更回退到最新Commit节点(注意,这个操作将会把所有没有提交commit的文件全部删除,所做更改也会消失)
- **实现思路:**因为每次提交后工作区,暂存区和版本库都是保持一致的,所以切换commit指向可以比较**currentCommit**以及**targetCommit**的区别,但是在不改变commit指向时无法实现,故比较**targetCommit**与**currentInedx**的差别。实现过程中需要考虑没有提交commit时的边界情况。以及向后切换commit节点时的commit的匹配问题(匹配commit的hash会先顺着commit的parent查找,没有找到再去objects目录下查找)。
可以通过观察工作区reset前后差异,以及通过ls-files -s 命令对比暂存区差异,以及head文件的指向来验证reset的正确性。
- **示例**:
`Java commander.CLI jit reset --hard 45e81`:切换到指定版本commit,可以往前也可以往后切换
`Java commander.CLI jit reset --mixed head^^`:回滚两次
`Java commander.CLI jit reset --soft head~3`:回滚三次
`Java commander.CLI jit reset` :撤销本次添加到暂存区的操作
- **失败情况**:
- 给定\<**commit**\>为一个不存在的提交
- 包括当前用户回滚版本数大于当前commit提交产生的版本数
- 给定hash值在**objects**目录下存在,但是类型为blob或者tree
- 或给定\<**commit**\>的hash值位数小于4(此时可能因为hash值位数太少无法匹配正确的commit)
------
#### jit diff
- **用法**: Java commander.CLI jit diff [--cached] | [branchName-1 branchName-2] | [-f fileName-1 fileName-2] | [-c commitID-1 commitID-2]
- **描述**:
- jit diff --cached 显示当前索引区和版本库之间的更改
- jit [branchName-1] [branchName-2] 显示两条分支之间的更改
- jit diff -f [fileName-1] [fileName-2] 显示两个文件之间的差异
- jit diff -c [commitID-1] [commitID-2] 两次提交之间的更改
执行jit diff命令显示:
- 进行对比的文件名
- 文件修改前后的哈希值及模式
- 源文件与目标文件名
- 源文件与目标文件中修改起始行数及显示总行数
- 文件更改情况
执行`jit diff` 打印结果格式如下:
```bash
diff --git a/test1 b/test1
index 06af77..631a1b 100644
--- a/test1
+++ b/test1
@@ -2,4 +2,5 @@
A
- B
C
D
+ E
+ F
```
- **实现思路**:
- 总体基于Myers Diff算法实现。
- 定义抽象类PathNode,其数据域包含节点坐标i、j及前驱节点Pathnode prev,PathNode类中定义抽象方法isSnake, 判定节点是否属于snake。
- 定义Snake类和DiffNode类继承PathNode类,并分别重写isSnake方法。
- 定义MyersDiff类,其中:buildPath方法实现Diff算法,寻找最优路径,输入源文件及目标文件内容构成的字符串列表orig、rev, 方法返回最优路径终点位置上的PathNode类型节点。buildDiff方法根据PathNode节点中存储的前驱节点信息获取完整的最优路径及diff结果,并将其存储在ArrayList中。printDiff方法用于打印diff结果。getChange方法用于统计diff结果中的增删情况。
- **算法实现步骤**:
- 原队列长度为N,目标队列长度为M
- 数组diagonal[]用于存储每一步每个k能到达的最优位置,因为k有正负,因此取size = 2 * ( M + N )
- 外层循环步数d(最大为M+N)
- 内层循环偏移k(步长为2)
- 遵循先删除后添加的原则
- 判断两个被比较的数组中,当前位置数据是否相同,若相同,则去到对角线位置
- 当到达目标位置时,该节点及其所有前驱节点即为当前图搜索的最优路径解 
- **示例**:
`Java commander.CLI jit diff --cached`:对比当前暂存区和版本库
` Java commander.CLI jit diff branch1 branch2`:对比两个分支
`Java commander.CLI jit diff -c commit1 commit2`: 对比两次提交
` Java commander.CLI jit diff -f file1 file2`:对比工作区的两个文件
- **失败情况**:无
------
#### jit branch
- **用法**:Java commander.CLI jit branch [branch-name] | [-d branch-name]
- **描述**:实现查看所有分支及当前所处分支,创建分支,删除分支的功能。
执行`jit branch`打印结果格式如下:
```bash
master
* dev1
dev2
```
- **示例**:
`Java commander.CLI jit branch`
` Java commander.CLI jit branch dev`
` Java commander.CLI jit branch -d dev`
------
#### jit checkout
- **用法**:Java commander.CLI jit checkout [branch-name] | [-b branch-name]
- **描述**:切换至目标分支,创建并切换至目标分支,将工作区内容同步为目标分支的最新提交状态
- **示例**:
`Java commander.CLI jit checkout`
` Java commander.CLI jit checkout -b dev`
------
#### jit merge
- **用法**:Java commander.CLI jit merge branchName
- **描述**:将指定的分支合并到当前分支,并更新工作区内容。
- **实现思路:**
1. 查找当前分支与指定分支最近公共祖先LCA。
2. 根据LCA与当前分支、指定分支的关系,执行不同的操作:
(1) LCA是指定分支:无需操作,提示合并完成。如下图,在dev合并merge:
(2) LCA是当前分支:将当前分支指向指定分支,同步更新工作区,输出变更文件数量,增/减行数。
如下图,在master合并dev:
(3) LCA既不是指定分支也不是当前分支,则比较指定分支、LCA、当前分支的文件差异(判断准则见下一节)。若存在冲突文件则输出冲突文件列表,不执行其他操作;若不存在冲突,则将变更添加到暂存区,新增commit完成合并,并输出变更的文件数量、增/减行数。如图,在dev合并master:
- **文件关系判断规则**:记当前分支为current,指定分支为another,最近公共祖先为LCA。
1. 对于指定分支another中的每个文件:
- 若存在于lca:
(1)LCA与another相同:即another分支中文件未发生变更,无需处理。
(2)LCA与another不同:
若current与another相同,即other分支与current分支对该文件的修改一致,无需处理;
若current与another不同:若current与lca相同,即该文件仅在another分支中修改而未在current分支修改,则视为变更处理。若current与lca不同,即other分支与cur分支对该文件的修改不一致,标记为矛盾文件。
- 若不存在于LCA,存在于current:
(1)another于current相同,即other分支与cur同时新增相同的一个文件,无需处理。
(2)another与current不同,即other分支与cur分支增加了同名但内容不同的文件,标记为矛盾文件。
- 若不存在于LCA,也不存在于current,即another新增了一个文件且current中不存在同名文件,则标记为变更(新增)。
2. 对于LCA中的每个文件:
- 若存在于another中,则已按上述规则处理。
- 若不存在于another中,但存在于current中,且current和LCA中该文件一致,即another分支删除了文件,但current分支没有修改,则标记为变更(删除)。
- **示例**:
`Java commander.CLI jit merge dev`: 将dev分支合并到当前分支
- **失败情况:**
指定分支不存在:提醒用户不存在指定分支
文件变更存在冲突情况:提醒用户存在文件冲突,并输出冲突文件列表
### 测试
采用Junit单元测试对前两次作业要求中的Repository,Blob,Tree,Index,JitAdd,JitRM做了测试。测试类单独放在src/test文件夹下。
### 当前存在问题
- cat-file中blob文件的反序列化中文出现乱码,但是单元测试中没有出现这个问题,原因不明(排除编码问题),英文测试无误。
- 程序中很多比较的部分调用了HashMap,并以文件的hash值作为key,对文件名不同但**文件内容相同**(hash相同)的比较可能出现错误。
- 因为需要支持在out\production\SimpleGit^1.0\下创建版本库(即在idea下,主要是调试比较方便),所以测试文件中包含以下文件夹名的[branch|commander|core|fileoperation|gitobject|repository|sha1|stage|zlib|utils|**test**],在调用jit status时可能出错。