这个项目将会带着我们一同手写实现一个Git版本管理工具,事件性和趣味性非常强,想象一下自己可以使用自己写的代码管理工具Gitlet,想想都爽。
将代码还有文件内容保存到一个一个blobs里面,不同版本的文件只需要通过指针指向该版本对应条件下存在的文件就可以实现版本回退和更换,CS61B的助教课中的Assistant画了一幅非常生动的图:
这幅图就非常直观表示出了不同分支内容和blobs之间的关系,图片中红色的就是blobs,对于项目中的每一个文件,都有一个blob保存这个文件。
因为要支持多版本的文件更新,所以使用树形结构,如果仅仅使用链表只能支持单个分支的形式。
将元数据、日志信息(正常commit -m后面加的东西)、还有相关指针信息
这张图我感觉很好地展示了commit和blobs之间的关系,可以看到blobs是真正存储数据的部分,commit通过存储对于数据存储块的引用(在C语言中就是指针)来实现版本追踪,同时很大程度上节省了空间,不用每次commit都将所有的数据单独保存成一个快照。
这里.Gitlet的设计可以参考.git文件夹的设计,在VSCode中打开隐藏的.git文件夹查看结构:
借用阿里的.git文件架构介绍:
如果想要一个纯净的.git文件参考一下用来写init函数,可以在powershell中找一个文件夹执行git init,于是乎得到以下文件:
保存暂存区的内容,可以简单理解成一个本地的temp文件,在推送至远端或者进行快照之前可供修改的部分
这个是实现的重点,包含三个内容:
这个部分在GItlet中使用了Java的面向对象编程中常用的对象序列化和对象反序列化进行
COMMIT_EDITMSG:commit修改的最新的相关信息
这里输出的commit内容刚好跟我最近一次提交的信息对应:
config:设置远程仓库地址以及远程仓库分支、本地仓库分支
tips:
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();
}
}
//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 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();
}
}
//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();
}
//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
}
}
//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();
}
}
//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;
}
}
}
//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 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);
}
//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);
}
//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 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 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));
}
}
}