gitlet的技术文档可能是很多小白第一次接触的长技术文档即使目前有很多市面上的翻译软件阅读起来也并非非常流畅,因此在这里做一个简单的翻译
前置信息
lab6 中提到的重要方法
文件操作
1 | File f = new File("file.txt"); |
目录
1 | File d = new File("dummy") |
Java 中目录同样用 File
表示
序列化
我们想要储存一个对象而不单纯是一个字符串的话,可以使用序列化,将Model
转换为 String
,存入,需要读取时,再进行反序列化解析回 Model
1 | import java.io.Serializable; |
序列化
1 | Model m; |
反序列化
1 | Model m; |
gitlet介绍
Gitlet实际上就是一个mini版git,将文件不同版本历史记录呈现在一个提交树中
这就是gitlet的文件形态
注意
游离 HEAD 指针
你不会处于一个游离HEAD的状态,也就是这样的
在实际的git中,你可以使用 checkout
或者 reset
来实现这个状态,比如
1 | # 将指定file的修改放弃,恢复到当前分支最后一个commit时的状态 |
提交树
提交树是不可变的,一旦commit节点创建,就无法销毁
几个概念
- blob: 已经保存的文件内容,实际上就是一个文件的多个版本,一个文件会对应一个或多个blob
- trees: 一个将文件名对应到相应 blob 的目录结构
- commit: 一个提交
相对于 git 的简化
- 不处理子目录
- 父级最多只有两个,也就是最多只可以由两个父级 merge 而来
- 元数据仅包含时间戳和日志消息
- 日志信息
- 时间戳
- 文件名到blob引用的映射
- 两个父级引用(另一个用于merge)
blob的独特性
使用 SHA-1 哈希函数来生成160位的哈希值,所以基本上不可能出现完全一样的哈希值
我们用这个 SHA-1 哈希值来标识每个 blob
我们不需要进行计算,但是需要正确标记我们所有的对象
- 哈希处理时包括所有的元数据
- 正确区分 blob 和 commit 的哈希值
- 在
.gitlet
中设计一个目录结构 - 为每个对象都添加一个额外的哈希,让这个对象的blob和commit分别可以对应一个哈希
- 在
建议与规范
类的实现
有两个建议类Commit
和Repository
,我们要实现的是gitlet.Main
类,Main
应该主要调用Repository
中的辅助方法,而不是在Main
本身中进行实现
runtime和memory要求
有些命令有runtime或者memory的要求,需要根据要求完成
失败情况
需要根据失败情况打印错误消息,并且不得更改其他内容
立即退出程序:System.exit(0)
一些与命令无关的的失败情况
- 没有输入参数:打印
Please enter a command.
然后退出 - 输入无效命令:打印消息
No command with that name exists.
并退出。 - 输入操作数量或格式错误:印消息
Incorrect operands.
并退出 - 输入命令要求位于已初始化的Gitlet工作目录(包含
.gitlet
的目录)但是路径错误:打印消息Not in an initialized Gitlet directory.
参数响应
init
创建一个初始commit,该commit不包含任何文件,message为init commit
,属于一个初始分支master
(也是当前分支),时间戳为 00:00:00 UTC, Thursday, 1 January 1970,所有仓库的这个初始 commit 都是一样的(同样的UID)
用法
java gitlet.Main init
失败情况
已存在gitlet的话,打印:A Gitlet version-control system already exists in the current directory.
add
将当前文件的副本添加到暂存区,用新内容覆盖暂存区中的旧内容(版本相同则不进行添加,若被更改后被添加,再次更改回原始版本,则直接从暂存区移除)
如果执行add
时文件处于rm
状态,则文件不再处于rm
状态
用法
java gitlet.Main add [file name]
runtime
$ lgN $
失败情况
文件不存在,打印File does not exist.
注意
一次只能 add
一个文件(与git不同)
commit
保存一个当前commit 和 暂存区中文件的快照,commit只更新已经跟踪且已经被add的文件,当然这个commit中已经跟踪的文件也可能在新commit中被取消跟踪(rm
指令删除)
提交后是这样的:
用法
java gitlet.Main commit [message]
注意
- commit之后,暂存区清空
- 不会对文件做任何改动,除了
rm
会在commit之后取消对文件的跟踪 - 只会commit暂存区的内容
- 添加到 commit tree中
- 移动HEAD指针指向我们的新commit
- 包含日期和时间
- 有一个关联的 log message 日志消息
- 每个commit 由 SHA-1 进行标识,包含blob引用,夫引用,log message 日志消息和提交时间
失败情况
没有文件被暂存,中止,打印No changes added to the commit.
如果没有 log message,打印Please enter a commit message.
rm
如果文件被暂存,还没有被添加,取消暂存,如果已经被追踪了,将其暂存(便于稍后删除),下次commit的时候这个暂存区中文件就消失了。已经被暂存或者追踪的文件都会直接在工作区中删除(物理上)
用法
java gitlet.Main rm [file name]
失败情况
文件没有被暂存或者追踪的话,打印No reason to remove the file.
log
从当前commit开始,沿着 commit tree 向后追溯,忽略 merge commit 中的第二个父提交
注意
- 每个commit前面有个
===
,后面有一个空行1
2
3
4
5
6
7
8
9
10
11
12
13
14===
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 - 每个 commit 条目显示提交对象的唯一 SHA-1 ID
java.util.Date
和java.util.Formatter
对获取和格式化时间非常有用- 对于 merge commit:在 SHA-1 下方添加一行类似
Merge: 4975af1 2c1ead1
,两个数字由第一个和第二个父提交 id 的前 7 位组成1
2
3
4
5===
commit 3e8bf1d794ca2e9ef8a4007275acf3751c7170ff
Merge: 4975af1 2c1ead1
Date: Sat Nov 11 12:30:00 2017 -0800
Merged development into master.
runtime
与历史节点呈线性关系
global-log
类似log,显示所有信息,顺序无关紧要
用法
java gitlet.Main global-log
注意
gitlet.Utils
中有一个使用方法,帮助迭代目录中的文件
runtime
与历史提交文件次数呈线性关系
find
找到包含指定提交信息的 id,每行一个
多字信息需要放在引号中
runtime
与commit次数呈现线性关系
失败情况
不存在这样的 commit 的话,打印:Found no commit with that message.
status
显示当前存在的分支,用 *
标记当前分支,显示已暂存和添加的文件
1 | === Branches === |
用法
java gitlet.Main status
注意
这几种情况会被视为 “modified but not staged”:
- 已被跟踪,被更改,但未 add
- 已add,用于添加,但是内容与工作区不同
- 已add,用于添加,但是被删掉了
- 没有被暂存用于删除,但是已经被跟踪且已经删掉了
runtime
取决于工作目录中的数据量加上要添加或删除的文件数加上分支数
checkout
三种用法
java gitlet.Main checkout -- [file name]
将当前commit的文件版本覆盖工作区中的文件,且不会被暂存java gitlet.Main checkout [commit id] -- [file name]
获取指定的 commit id 的文件版本,覆盖当前工作区的文件java gitlet.Main checkout [branch name]
切换分支,将新分支的所有文件覆盖当前工作区,清空暂存区,所有旧分支的被跟踪的文件在新分支中不存在的话,会被清除
runtime
与被checkout的文件呈线性关系
与commit 快照中的文件呈线性关系
失败情况
- 文件在之前commit中不存在,打印
File does not exist in that commit.
- 不存在指定的 commit id,打印
No commit with that id exists.
- 不存在指定名字的branch, 打印
No such branch exists.
,如果checkout到当前分支,打印No need to checkout the current branch.
,如果当前分支有未跟踪的文件,并且该文件被checkout后的文件覆盖,打印There is an untracked file in the way; delete it, or add and commit it first.
并退出
branch
创建一个新分支,这个新分支只是指向当前commit的指针而已(SHA-1标识)
用法
java gitlet.Main branch [branch name]
注意
- 不会立刻切换到新分支
- 调用
branch
之前,应该在名为master的默认分支上运行
失败情况
给定名称已经存在,打印A branch with that name already exists.
rm-branch rm
删除指定名称的分支指针
用法
java gitlet.Main rm-branch [branch name]
注意
只是删除与该分支关联的指针,而并没有删除 commit tree 中的commit
runtime
常数时间
失败情况
指定分支名称不存在,打印A branch with that name does not exist.
删除当前分支,打印annot remove the current branch.
并 中止
reset
将 HEAD 移动到指定的 commit id,然后删除所有已被跟踪但是在指定commit中不存在的文件,暂存区清空,这里的[commit id]
可以直接缩写为checkout
用法
java gitlet.Main reset [commit id]
失败情况
不存在指定的 id,打印No commit with that id exists.
,存在未跟踪的文件,而且即将被覆盖,打印There is an untracked file in the way; delete it, or add and commit it first.
注意
很接近git中的git reset --hard [commit hash]
merge
- 引入一个概念叫分割点,就是开始出现两个分支的commit
- 如果给定的分支与分割点是同一个提交,啥都不做,打印
Given branch is an ancestor of the current branch.
- 如果分割点是当前分支,打印
Current branch fast-forwarded.
- 如果给定的分支与分割点是同一个提交,啥都不做,打印
- 自分割点来,当前branch没修改过,在给定 branch 中修改的,直接覆盖,然后自动暂存
- 当前branch修改,给定branch为修改,不动
- 相同方式修改,不动(都删除了的话,也不动)
- 分割点不存在,存在当前分支的文件,不动
- 分割点不存在,存在给定branch的文件,check out 然后暂存
- 存在于分割点,当前branch未修改,给定branch不存在的,被删除
- 存在于分割点,给定branch未修改,当前branch不存在,也被删除
- 不同方式修改,将冲突文件内容替换为:将 “contents of …” 替换为指定文件的内容
1
2
3
4
5<<<<<<< HEAD
contents of file in current branch
=======
contents of file in given branch
>>>>>>>
已删除的文件视为空文件,直接连接,如果末尾没有换行符,可能类似于:1
2
3<<<<<<< HEAD
contents of file in current branch=======
contents of file in given branch>>>>>>>
用法
java gitlet.Main merge [branch name]
注意
- merge之后会自动commit并记录一个log message
Merged [given branch name] into [current branch name].
- 如果merge遇到冲入,在终端打印
Encountered a merge conflict.
runtime
$O(NlgN + D)$,其中 $N$ 是两个分支的祖先提交的总数,$D$ 是这些提交下的所有文件的总数据量
失败情况
- 存在已暂存的添加或删除操作,打印
You have uncommitted changes.
并退出 - 不存在指定名称的分支,打印
A branch with that name does not exist.
- 将分支与其自身合并,打印
Cannot merge a branch with itself.
- 合并操作由于 commit 中没有更改而导致错误,则直接让正常的提交错误消息通过
- 如果覆盖或删除当前提交中未跟踪的文件,打印
There is an untracked file in the way; delete it, or add and commit it first.
并退出