Project02 Gitlet
- 一、项目简介
- 二、Git和Gitlet
- 2.1 Git简介
- 2.2 Gitlet简介
- 三、框架设计
- 3.1 Blobs
- 3.2 Trees
- 3.3 Commits
- 四、.Gitlet文件结构设计
- 4.1 .git文件架构
- 4.1.1 重点介绍
- index(VSCode中无法查看,会乱码)
- objects(VSCode中无法查看,会乱码)
- 4.1.2 其他文件大杂烩
- 4.2 Gitlet文件架构设计
- 五、项目文件设计
- Repository类
- init函数
- add函数
- commit函数
- rm函数
- log函数
- global_log函数
- find函数
- status函数
- checkout部分综合代码
- branch函数
- rm_branch函数
- reset函数
- merge函数
一、项目简介
这个项目将会带着我们一同手写实现一个Git版本管理工具,事件性和趣味性非常强,想象一下自己可以使用自己写的代码管理工具Gitlet,想想都爽。
二、Git和Gitlet
2.1 Git简介
- 保存整个文件目录的内容。在git中,这被称为提交(git commit + git push),而保存的内容本身被称为提交。
- 还原一个或多个文件的版本或整个提交。在git中,这称为签出那些文件或提交的文件。
- 查看备份的历史记录。在git中,您可以在称为日志的东西中查看此历史(简称开发历史,同时起到还原的作用)。
- 维护相关的提交序列,称为分支(branch)。
- 将一个分支中的更改合并到另一个分支中(简单理解就是将不同版本的代码进行合并)。
- 不仅支持历史版本的修改,还能支持形式上的不同实现方案(Plan A B C D)同时推进。
2.2 Gitlet简介
- Gitlet支持多版本多分枝开发。
- Gitlet不支持删除commit,这样设计的目的是尽可能保证各个历史版本的完整性。
- Gitlet的结构比较像树形结构,但是Gitlet仅仅支持两个parents的树形结构。
- GItlet更像是一个本地Git版本管理工具,将代码推送到远程分支的功能可以自己感兴趣进行添加,在我的项目实现里面我添加了这个功能,因为是Git的忠实信徒(doge)。
三、框架设计
3.1 Blobs
将代码还有文件内容保存到一个一个blobs里面,不同版本的文件只需要通过指针指向该版本对应条件下存在的文件就可以实现版本回退和更换,CS61B的助教课中的Assistant画了一幅非常生动的图:
这幅图就非常直观表示出了不同分支内容和blobs之间的关系,图片中红色的就是blobs,对于项目中的每一个文件,都有一个blob保存这个文件。
3.2 Trees
因为要支持多版本的文件更新,所以使用树形结构,如果仅仅使用链表只能支持单个分支的形式。
3.3 Commits
将元数据、日志信息(正常commit -m后面加的东西)、还有相关指针信息
这张图我感觉很好地展示了commit和blobs之间的关系,可以看到blobs是真正存储数据的部分,commit通过存储对于数据存储块的引用(在C语言中就是指针)来实现版本追踪,同时很大程度上节省了空间,不用每次commit都将所有的数据单独保存成一个快照。
四、.Gitlet文件结构设计
4.1 .git文件架构
这里.Gitlet的设计可以参考.git文件夹的设计,在VSCode中打开隐藏的.git文件夹查看结构:
借用阿里的.git文件架构介绍:
如果想要一个纯净的.git文件参考一下用来写init函数,可以在powershell中找一个文件夹执行git init,于是乎得到以下文件:
4.1.1 重点介绍
index(VSCode中无法查看,会乱码)
保存暂存区的内容,可以简单理解成一个本地的temp文件,在推送至远端或者进行快照之前可供修改的部分
objects(VSCode中无法查看,会乱码)
这个是实现的重点,包含三个内容:
- commits:元数据,commit指针以及整体commit的维护
- trees:各个commit版本文件的存储结构(数据结构)
- blobs:数据块
这个部分在GItlet中使用了Java的面向对象编程中常用的对象序列化和对象反序列化进行
4.1.2 其他文件大杂烩
-
COMMIT_EDITMSG:commit修改的最新的相关信息
这里输出的commit内容刚好跟我最近一次提交的信息对应:
-
config:设置远程仓库地址以及远程仓库分支、本地仓库分支
- HEAD:保存了本地仓库的分支指针位置,在这个仓库中是master
- ORIG_HEAD:用哈希的方法随机生成的唯一标识码
- log :保存日志信息相关内容
- HEAD:保存所有的头部指针信息
这里的matster文件保存了所有master分支提交的信息 - refs:保存本地分支和远程分支对应关系
- HEAD:保存所有的头部指针信息
4.2 Gitlet文件架构设计
- objects 文件夹:存储commit和blob对象,数据结构是tree(使用hashcode作为文件名)
- commits文件夹:存储每一次commit信息
- blobs文件夹:存储数据块
- commit 文件:用sha1编码保存对commit的引用
- blob文件:用sha1编码·1保存对blob的引用
- remotes 存储远端分支内容,用于远程推送
- HEAD文件: 存储当前分支的名称,默认为master分支,在init的时候生成
- staging 存储缓存区内容(以Stage class的形式存储blob)
- removedStage 存储被rm的文件(以Stage class的形式存储)
五、项目文件设计
- Utils文件中是用于实现生成sha1标签值、对象序列化和反序列化、使用到的数据结构、常用的IO操作的辅助函数
- Repository中定用于初始化的时候定义Gitlet项目文件夹的架构和文件初始化。
- Commit中定义了git中比较重要的一个类commit的应用。
tips:
- 序列化的时间不能由于对象的内容大小而变化。
Repository类
init函数
public void init() {
//create a new .gitlet directory
if (GITLET_DIR.exists()) {
System.out.println("A Gitlet version-control system already exists in the current directory.");
return;
} else {
GITLET_DIR.mkdir();
// set the visibility of the .gitlet directory to hidden
DosFileAttributeView fileAttributeView = Files.getFileAttributeView(GITLET_DIR.toPath(), DosFileAttributeView.class);
try {
fileAttributeView.setHidden(true);
} catch (IOException e) {
throw new RuntimeException(e);
}
//get the current time
long time = System.currentTimeMillis();
//change the type of the time to a string
String timestamp = String.valueOf(time);
//create the initial commit
Commit initialCommit = new Commit("initial commit", null,timestamp, null);
//set the branch to master by defult
branch = "master";
//create object area for commits areas
File object = join(GITLET_DIR, "object");
//create subdirectories for commits areas and blobs
File commit = join(object, "commit");
//create subdirectories for blobs areas
File blob = join(object, "blob");
//create head area for commits areas
File head = join(GITLET_DIR, "HEAD");
//init the head to master
writeContents(head, "master");
//create staging area for commits
File staging = join(GITLET_DIR, "staging");
//create the files above
object.mkdir();
commit.mkdir();
blob.mkdir();
staging.mkdir();
}
}
- 判断当前文件夹下是否存在.gitlet仓库,如果没有就新建一个,如果有就提示已经存在。
- 创建commit文件夹和blob文件夹,同时初始化一个initial commit对象到文件并保存到commit文件夹下
- 创建一个staging区域用于暂存commit的内容。
add函数
//add a copy of the files as currently exist to the stagging area
public void add(String filename) {
//if the current working version of the file is identical to the version in the current commit do not stage!
//if the file does not exist at all,print an error message
if (!join(CWD, filename).exists()) {
System.out.println("File does not exist.");
return;
}
//if the current working version of the file is identical to the version in the current commit, do not stage it
if (join(GITLET_DIR, "object", "commit", "HEAD", filename).exists()) {
if (join(CWD, filename).equals(join(GITLET_DIR, "object", "commit", "HEAD", filename))) {
return;
}
}
//if the file is not staged, add it to the staging area
if (!join(GITLET_DIR, "staging", filename).exists()) {
writeContents(join(GITLET_DIR, "staging", filename), readContentsAsString(join(CWD, filename)));
}
//if the file is already staged, overwrite the file in the staging area with the new version
else {
writeContents(join(GITLET_DIR, "staging", filename), readContentsAsString(join(CWD, filename)));
}
}
- 将指定的文件添加到暂存区中
commit函数
//commit the files in the staging area
public void commit(String message) {
//if no files have been staged, print an error message
if (join(GITLET_DIR,"stageing").list().length == 1) {
System.out.println("No changes added to the commit.");
return;
}
//create a new commit object and set the parent commit
//TODO: figure out how the blobs work
Commit newCommit = new Commit(message, readObject(join(GITLET_DIR, "HEAD", "HEAD"), Commit.class), String.valueOf(System.currentTimeMillis()), null);
//create a new commit file
File commitFile = join(GITLET_DIR, "object", "commit", newCommit.getUID());
//serialize the commit object and write it to the commit file
writeObject(commitFile, newCommit);
//update the head to the new commit
writeContents(join(GITLET_DIR, "HEAD", "HEAD"), newCommit.getUID());
//clear the staging area
for (File file : join(GITLET_DIR, "staging").listFiles()) {
file.delete();
}
}
- 新建一个commit对象记录当前的项目状态。
rm函数
//remove the file from the staging area
public void rm(String filename) {
//if the file is not staged, print an error message
if (!join(GITLET_DIR, "staging", filename).exists()) {
System.out.println("No reason to remove the file.");
return;
}
//remove the file from the staging area
join(GITLET_DIR, "staging", filename).delete();
}
- 从暂存区中将已经添加的某个文件删除。
log函数
//print the commit history, but we only display the first parent commit links and ignore any second parent links
public void log() {
//get into the commit directory
File commit = join(GITLET_DIR, "object", "commit");
//get the current commit and deserialize it to a commit object
File currentCommit = join(commit, readContentsAsString(join(GITLET_DIR, "HEAD", "HEAD")));
Commit current = readObject(currentCommit, Commit.class);
//print the commit history
while (current != null) {
System.out.println("===");
System.out.println("commit " + current.getUID());
System.out.println("Date: " + current.getTimestamp());
System.out.println(current.getMessage());
System.out.println();
current = current.getParent();//implement the circular of the commit messages
}
}
- 从commit文件夹下获取当前分支的commit信息并打印在控制台上面。
global_log函数
//print the commit history of all commits
public void global_log() {
//get the global log of all commits
File commit = join(GITLET_DIR, "object", "commit");
for (File file : commit.listFiles()) {
Commit current = readObject(file, Commit.class);
System.out.println("===");
System.out.println("commit " + current.getUID());
System.out.println("Date: " + current.getTimestamp());
System.out.println(current.getMessage());
System.out.println();
}
}
- 在全局范围内打印所有的commit信息。
find函数
//find the commit with the given message
public void find(String message) {
//get the global log of all commits
File commit = join(GITLET_DIR, "object", "commit");
for (File file : commit.listFiles()) {
Commit current = readObject(file, Commit.class);
if (current.getMessage().equals(message)) {
System.out.println(current.getUID());//print the commit message by line
return;
}
}
}
- 根据文件名找到相应的commit message
status函数
//print the status of the repository
public void status() {
//get the current branch
File head = join(GITLET_DIR, "HEAD");
String currentBranch = readContentsAsString(head);
//print the current branch
System.out.println("=== Branches ===");
System.out.println("*" + currentBranch);
for (File file : join(GITLET_DIR, "object", "commit").listFiles()) {
if (!file.getName().equals(currentBranch)) {
System.out.println(file.getName());
}
}
System.out.println();
//print the staged files
System.out.println("=== Staged Files ===");
for (File file : join(GITLET_DIR, "staging").listFiles()) {
System.out.println(file.getName());
}
System.out.println();
//print the removed files
System.out.println("=== Removed Files ===");
for (File file : join(GITLET_DIR, "staging").listFiles()) {
System.out.println(file.getName());
}
System.out.println();
//print the modified files
System.out.println("=== Modifications Not Staged For Commit ===");
System.out.println();
//print the untracked files
System.out.println("=== Untracked Files ===");
System.out.println();
}
- 打印当前的暂存区的状态
checkout部分综合代码
//checkout the file from the current commit
public void checkout(String filename) {
//get the current commit
File commit = join(GITLET_DIR, "object", "commit");
File currentCommit = join(commit, readContentsAsString(join(GITLET_DIR, "HEAD")));
Commit current = readObject(currentCommit, Commit.class);
//get the file from the current commit
File file = join(GITLET_DIR, "object", "blob", current.getUID(), filename);
//copy the file to the current working directory
writeContents(join(CWD, filename), readContents(file));
}
//checkout the file from the commit id
public void checkout(String commitID, String filename) {
//get the commit from the commit id
File commit = join(GITLET_DIR, "object", "commit");
File currentCommit = join(commit, commitID);
Commit current = readObject(currentCommit, Commit.class);
//get the file from the commit
File file = join(GITLET_DIR, "object", "blob", current.getUID(), filename);
//copy the file to the current working directory
writeContents(join(CWD, filename), readContents(file));
}
//checkout the branch
public void checkoutBranch(String branchName) {
//get the current branch
File head = join(GITLET_DIR, "HEAD");
String currentBranch = readContentsAsString(head);
//if the branch does not exist, print an error message
if (!join(GITLET_DIR, "object", "commit", branchName).exists()) {
System.out.println("No such branch exists.");
return;
}
//if the branch is the current branch, print an error message
if (currentBranch.equals(branchName)) {
System.out.println("Already on the target branch, no need to change the branch");
return;
}
//get the current commit
File commit = join(GITLET_DIR, "object", "commit");
File currentCommit = join(commit, readContentsAsString(join(GITLET_DIR, "HEAD")));
Commit current = readObject(currentCommit, Commit.class);
//get the branch commit
File branchCommit = join(commit, branchName);
Commit branch = readObject(branchCommit, Commit.class);
//get the files from the branch commit
for (File file : join(GITLET_DIR, "object", "blob", branch.getUID()).listFiles()) {
//copy the file to the current working directory
writeContents(join(CWD, file.getName()), readContents(file));
}
//delete the files that are not in the branch commit
for (File file : join(CWD).listFiles()) {
if (!join(GITLET_DIR, "object", "blob", branch.getUID(), file.getName()).exists()) {
file.delete();
}
}
//update the head to the branch commit
writeContents(join(GITLET_DIR, "HEAD"), branchName);
}
- 分支切换以及状态更新
branch函数
//Description: Creates a new branch with the given name, and points it at the current head commit. A branch is nothing more than a name for a reference (a SHA-1 identifier) to a commit node. This command does NOT immediately switch to the newly created branch (just as in real Git). Before you ever call branch, your code should be running with a default branch called “master”.
public void branch(String branchName) {
//get the current branch
File head = join(GITLET_DIR, "HEAD");
String currentBranch = readContentsAsString(head);
//if the branch already exists, print an error message
if (join(GITLET_DIR, "object", "commit", branchName).exists()) {
System.out.println("A branch with that name already exists.");
return;
}
//create a new branch commit
File commit = join(GITLET_DIR, "object", "commit");
File currentCommit = join(commit, readContentsAsString(join(GITLET_DIR, "HEAD")));
Commit current = readObject(currentCommit, Commit.class);
Commit newBranch = new Commit(current.getMessage(), current, String.valueOf(System.currentTimeMillis()), null);
//create a new branch commit file
File branchCommit = join(commit, branchName);
//serialize the branch commit object and write it to the branch commit file
writeObject(branchCommit, newBranch);
}
- 创建一个新的branch
rm_branch函数
//remove the branch
public void rm_branch(String branchName) {
//get the current branch
File head = join(GITLET_DIR, "HEAD");
String currentBranch = readContentsAsString(head);
//if the branch does not exist, print an error message
if (!join(GITLET_DIR, "object", "commit", branchName).exists()) {
System.out.println("A branch with that name does not exist.");
return;
}
//if the branch is the current branch, print an error message
if (currentBranch.equals(branchName)) {
System.out.println("Cannot remove the current branch.");
return;
}
//remove the branch
join(GITLET_DIR, "object", "commit", branchName).delete();
}
- 删除一个分支
reset函数
//reset the commit header to the given commit
public void reset(String commitID) {
//get the commit from the commit id
File commit = join(GITLET_DIR, "object", "commit");
File currentCommit = join(commit, commitID);
Commit current = readObject(currentCommit, Commit.class);
//get the files from the commit
for (File file : join(GITLET_DIR, "object", "blob", current.getUID()).listFiles()) {
//copy the file to the current working directory
writeContents(join(CWD, file.getName()), readContents(file));
}
//delete the files that are not in the commit
for (File file : join(CWD).listFiles()) {
if (!join(GITLET_DIR, "object", "blob", current.getUID(), file.getName()).exists()) {
file.delete();
}
}
//update the head to the commit
writeContents(join(GITLET_DIR, "HEAD"), commitID);
}
merge函数
//merge the branch with the current branch
public void merge(String branchName) {
//get the current branch
File head = join(GITLET_DIR, "HEAD");
String currentBranch = readContentsAsString(head);
//if the branch does not exist, print an error message
if (!join(GITLET_DIR, "object", "commit", branchName).exists()) {
System.out.println("A branch with that name does not exist.");
return;
}
//if the branch is the current branch, print an error message
if (currentBranch.equals(branchName)) {
System.out.println("Cannot merge a branch with itself.");
return;
}
//get the current commit
File commit = join(GITLET_DIR, "object", "commit");
File currentCommit = join(commit, readContentsAsString(join(GITLET_DIR, "HEAD")));
Commit current = readObject(currentCommit, Commit.class);
//get the branch commit
File branchCommit = join(commit, branchName);
Commit branch = readObject(branchCommit, Commit.class);
//get the split point commit
Commit splitPoint = findSplitPoint(current, branch);
//get the files from the split point commit
for (File file : join(GITLET_DIR, "object", "blob", splitPoint.getUID()).listFiles()) {
//copy the file to the current working directory
writeContents(join(CWD, file.getName()), readContents(file));
}
//get the files from the branch commit
for (File file : join(GITLET_DIR, "object", "blob", branch.getUID()).listFiles()) {
//if the file is not in the split point commit, copy the file to the current working directory
if (!join(GITLET_DIR, "object", "blob", splitPoint.getUID(), file.getName()).exists()) {
writeContents(join(CWD, file.getName()), readContents(file));
}
}
//get the files from the current commit
for (File file : join(GITLET_DIR, "object", "blob", current.getUID()).listFiles()) {
//if the file is not in the split point commit, copy the file to the current working directory
if (!join(GITLET_DIR, "object", "blob", splitPoint.getUID(), file.getName()).exists()) {
writeContents(join(CWD, file.getName()), readContents(file));
}
}
}