Gitlet——简化版Git

[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 |

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值