Instructions
Objective
Write a program to build gitlet program using Java programming language.
Requirements and Specifications
Create a gitlet program.
Source Code
package gitlet;
import java.io.File;
import java.io.IOException;
import java.nio.file.*;
import java.util.*;
import java.util.stream.Collectors;
public class Gitlet implements Saveable, Dumpable {
private static final Set<String> temp = new HashSet<>();
static {
temp.add("gitlet.iml");
temp.add("src");
temp.add("samples");
temp.add("out");
temp.add("lib");
temp.add(".idea");
}
public static final String GITLET_HIDDEN_FOLDER = ".gitlet";
public static final String STAGING_FOLDER = "staging";
public static final String STAGING_RM_FOLDER = "staging-rm";
private static final String MAIN_FILENAME = "gitlet-main";
private transient Collection<Branch> branches;
private Collection<String> branchHashes;
private transient Branch currentBranch;
private String currentBranchHash;
private transient Commit currentCommit;
private String currentCommitHash;
private Gitlet(Collection<Branch> branches, Branch currentBranch, Commit currentCommit) {
this.branches = branches;
this.currentBranch = currentBranch;
this.currentCommit = currentCommit;
}
public void add(String fileName) {
Path path = Paths.get(fileName);
if (!Files.exists(path)) {
throw new GitletException("File does not exist");
}
Blob blob = currentCommit.getBlob(fileName);
try {
unstageFileForRemove(fileName);
if (blob != null) {
if (areEquals(fileName, blob)) {
unstageFile(fileName);
return;
}
}
stageFile(fileName);
} catch (IOException e) {
e.printStackTrace();
}
}
public void commit(String commitMessage) {
commit(commitMessage, null);
}
private void commit(String commitMessage, Commit commit) {
if (commitMessage.isEmpty()) {
throw new GitletException("Please, enter a commit message");
}
Map<String, Blob> fileTree = new HashMap<>(currentCommit.getFileTree());
try {
if (!hasStagingChanges()) {
throw new GitletException("No changes added to the commit");
}
applyStagingChangesToFileTree(fileTree);
clearStagingArea();
Collection<Commit> parentCommits = new ArrayList<>();
parentCommits.add(currentCommit);
if (commit != null) {
parentCommits.add(commit);
}
currentCommit = new Commit(commitMessage, System.currentTimeMillis(), fileTree, parentCommits);
for (Branch branch : branches) {
if (branch.getName().equals(currentBranch.getName())) {
branch.setHead(currentCommit);
break;
}
}
save(Paths.get(GITLET_HIDDEN_FOLDER, MAIN_FILENAME).toFile());
} catch (IOException e) {
e.printStackTrace();
}
}
public void rm(String fileName) {
boolean removed = false;
try {
removed = unstageFile(fileName);
Blob blob = currentCommit.getBlob(fileName);
if (blob != null) {
Files.createFile(Paths.get(GITLET_HIDDEN_FOLDER, STAGING_RM_FOLDER, fileName));
Files.deleteIfExists(Paths.get(fileName));
removed = true;
}
} catch (IOException e) {
e.printStackTrace();
}
if (!removed) {
throw new GitletException("No reason to remove the file");
}
}
public void log() {
Commit commit = currentCommit;
while (commit != null) {
System.out.println(commit.log());
Collection<Commit> parent = commit.getParentCommits();
if (parent.isEmpty()) {
commit = null;
} else {
commit = parent.iterator().next();
}
}
}
public void globalLog() {
for (Commit commit : collectAllCommits()) {
System.out.println(commit.log());
}
}
public void find(String message) {
boolean found = false;
for (Commit commit : collectAllCommits()) {
if (commit.getCommitMessage().contains(message)) {
found = true;
System.out.println(commit.getCommitId());
}
}
if (!found) {
throw new GitletException("Found no commit with that message");
}
}
public void status() {
System.out.println("=== Branches ===");
List<Branch> sortedBranches = new ArrayList<>(branches);
sortedBranches.sort(Comparator.comparing(Branch::getName));
for (Branch branch : sortedBranches) {
if (branch.getName().equals(currentBranch.getName())) {
System.out.println("*" + branch.getName());
} else {
System.out.println(branch.getName());
}
}
System.out.println();
System.out.println("=== Staged Files ===");
List<String> sortedStaged = null;
try {
sortedStaged = getStagedFiles().stream().sorted(Comparator.naturalOrder())
.collect(Collectors.toList());
for (String staged : sortedStaged) {
System.out.println(staged);
}
} catch (IOException e) {
e.printStackTrace();
}
System.out.println();
System.out.println("=== Removed Files ===");
List<String> sortedRemoved = getTrackedFiles()
.stream().filter(p -> !Files.exists(Paths.get(p)))
.sorted(Comparator.naturalOrder()).collect(Collectors.toList());
for (String removed : sortedRemoved) {
System.out.println(removed);
}
System.out.println();
System.out.println("=== Modifications Not Staged For Commit ===");
Map<String, Blob> fileTree = currentCommit.getFileTree();
Map<String, String> modificationMap = new HashMap<>();
for (Map.Entry<String, Blob> pair : fileTree.entrySet()) {
Path path = Paths.get(pair.getKey());
if( Files.exists(Paths.get(GITLET_HIDDEN_FOLDER, STAGING_RM_FOLDER, pair.getKey()))) {
continue;
}
if (!Files.exists(path)) {
modificationMap.put(pair.getKey(), "deleted");
} else {
try {
byte[] bytes = Files.readAllBytes(path);
if (!Arrays.equals(bytes, pair.getValue().getBytes())) {
modificationMap.put(pair.getKey(), "modified");
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
List<Map.Entry<String, String>> sortedModified = new ArrayList<>(modificationMap.entrySet());
sortedModified.sort(Map.Entry.comparingByKey());
for (Map.Entry<String, String> pair : sortedModified) {
System.out.println(pair.getKey() + " (" + pair.getValue() + ")");
}
System.out.println();
try {
System.out.println("=== Untracked Files ===");
Collection<String> tracked = new ArrayList<>(fileTree.keySet());
tracked.addAll(sortedStaged);
List<String> untracked = Files.list(Paths.get(""))
.map(p -> p.toFile().getName())
.filter(p -> !p.equals(GITLET_HIDDEN_FOLDER))
.filter(p -> !tracked.contains(p)).sorted(String::compareTo)
.collect(Collectors.toList());
for (String untrackedFile : untracked) {
System.out.println(untrackedFile);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public void checkout(Path fileName) {
Blob blob = currentCommit.getBlob(fileName.toFile().getName());
if (blob == null) {
throw new GitletException("File does not exist in that commit");
}
try {
Files.deleteIfExists(fileName);
Files.write(fileName, blob.getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}
public void checkout(String commitId, Path fileName) {
Set<Commit> allCommits = collectAllCommits();
Commit foundCommit = null;
for (Commit commit : allCommits) {
if (commit.getCommitId().startsWith(commitId)) {
foundCommit = commit;
break;
}
}
if (foundCommit == null) {
throw new GitletException("No commit with that id exists");
}
Blob blob = foundCommit.getBlob(fileName.toFile().getName());
if (blob == null) {
throw new GitletException("File does not exist in that commit");
}
try {
Files.deleteIfExists(fileName);
Files.write(fileName, blob.getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}
public void checkout(String branchName) {
if (branchName.equals(currentBranch.getName())) {
throw new GitletException("No need to checkout the current branch");
}
Branch foundBranch = null;
for (Branch branch : branches) {
if (branchName.equals(branch.getName())) {
foundBranch = branch;
break;
}
}
if (foundBranch == null) {
throw new GitletException("No such branch exists.");
}
Commit commit = foundBranch.getHead();
try {
for (Path filePath : Files.list(Paths.get("")).collect(Collectors.toList())) {
if (temp.contains(filePath.toString()))
continue;
if (filePath.toString().equals(GITLET_HIDDEN_FOLDER))
continue;
Blob blob = commit.getBlob(filePath.toString());
if (blob != null) {
if (currentCommit.getBlob(filePath.toString()) == null) {
throw new GitletException("There is an untracked file in the way; delete it, or add and commit it first.");
}
}
}
for (Path filePath : Files.list(Paths.get("")).collect(Collectors.toList())) {
if (temp.contains(filePath.toString()))
continue;
if (filePath.toString().equals(GITLET_HIDDEN_FOLDER))
continue;
Files.deleteIfExists(filePath);
}
for (Map.Entry<String, Blob> pair : commit.getFileTree().entrySet()) {
Blob blob = pair.getValue();
Files.write(Paths.get(pair.getKey()), blob.getBytes());
}
currentBranch = foundBranch;
currentCommit = commit;
save(Paths.get(GITLET_HIDDEN_FOLDER, MAIN_FILENAME).toFile());
} catch (IOException e) {
}
}
public void branch(String branchName) {
for (Branch branch : branches) {
if (branchName.equals(branch.getName())) {
throw new GitletException("A branch with that name already exists.");
}
}
Branch newBranch = new Branch(branchName, currentCommit);
branches.add(newBranch);
save(Paths.get(GITLET_HIDDEN_FOLDER, MAIN_FILENAME).toFile());
}
public void rmBranch(String branchName) {
if (branchName.equals(currentBranch.getName())) {
throw new GitletException("Cannot remove the current branch.");
}
Branch foundBranch = null;
for (Branch branch : branches) {
if (branchName.equals(branch.getName())) {
foundBranch = branch;
break;
}
}
if (foundBranch == null) {
throw new GitletException("A branch with that name does not exist.");
}
branches.remove(foundBranch);
save(Paths.get(GITLET_HIDDEN_FOLDER, MAIN_FILENAME).toFile());
}
public void reset(String commitId) {
Commit foundCommit = getCommitById(commitId);
try {
checkUntrackedOverwritten(foundCommit);
clearWorkingDirectory();
clearStagingArea();
for (Map.Entry<String, Blob> pair : foundCommit.getFileTree().entrySet()) {
Blob blob = pair.getValue();
Files.write(Paths.get(pair.getKey()), blob.getBytes());
}
currentCommit = foundCommit;
save(Paths.get(GITLET_HIDDEN_FOLDER, MAIN_FILENAME).toFile());
} catch (IOException e) {
}
}
public void merge(String branchName) {
if (branchName.equals(currentBranch.getName())) {
throw new GitletException("Cannot merge a branch with itself.");
}
Branch foundBranch = null;
for (Branch branch : branches) {
if (branchName.equals(branch.getName())) {
foundBranch = branch;
break;
}
}
if (foundBranch == null) {
throw new GitletException("A branch with that name does not exist.");
}
try {
if (Files.list(Paths.get(GITLET_HIDDEN_FOLDER, STAGING_FOLDER)).count() > 0 ||
Files.list(Paths.get(GITLET_HIDDEN_FOLDER, STAGING_RM_FOLDER)).count() > 0) {
throw new GitletException("You have uncommitted changes.");
}
}
catch (IOException e) {
}
Commit splitPoint = getSplitPoint(foundBranch);
if (splitPoint.equals(currentCommit)) {
throw new GitletException("Current branch fast-forwarded.");
}
if (splitPoint.equals(foundBranch.getHead())) {
throw new GitletException("Given branch is an ancestor of the current branch.");
}
Collection<String> currentModified = getModified(splitPoint, currentCommit);
Collection<String> currentAdded = getAdded(splitPoint, currentCommit);
Collection<String> currentRemoved = getAdded(currentCommit, splitPoint);
Collection<String> givenModified = getModified(splitPoint, foundBranch.getHead());
Collection<String> givenAdded = getAdded(splitPoint, foundBranch.getHead());
Collection<String> givenRemoved = getAdded(foundBranch.getHead(), splitPoint);
try {
if (hasStagingChanges()) {
throw new GitletException("You have uncommitted changes.");
}
Collection<String> changedFiles = new HashSet<>();
changedFiles.addAll(givenModified);
changedFiles.addAll(givenRemoved);
changedFiles.addAll(givenAdded);
for (String changedFile : changedFiles) {
if (getUntrackedFiles().contains(changedFile)) {
throw new GitletException("There is an untracked file in the way; delete it, or add and commit it first.");
}
}
boolean conflict = false;
for (String givenModifiedFile : givenModified) {
if (!currentModified.contains(givenModifiedFile) && !currentRemoved.contains(givenModifiedFile)) {
checkout(foundBranch.getHead().getCommitId(), Paths.get(givenModifiedFile));
stageFile(givenModifiedFile);
}
Blob givenBlob = foundBranch.getHead().getBlob(givenModifiedFile);
if (currentModified.contains(givenModifiedFile)) {
Blob currentBlob = currentCommit.getBlob(givenModifiedFile);
if (!areEquals(currentBlob, givenBlob)) {
conflict = true;
mergeConfictFiles(givenModifiedFile, currentBlob, givenBlob);
stageFile(givenModifiedFile);
}
}
if (currentRemoved.contains(givenModifiedFile)) {
conflict = true;
mergeConfictFiles(givenModifiedFile, null, givenBlob);
stageFile(givenModifiedFile);
}
}
for (String givenAddedFile : givenAdded) {
if (!currentAdded.contains(givenAddedFile)) {
checkout(foundBranch.getHead().getCommitId(), Paths.get(givenAddedFile));
stageFile(givenAddedFile);
}
if (currentAdded.contains(givenAddedFile)) {
Blob givenBlob = foundBranch.getHead().getBlob(givenAddedFile);
Blob currentBlob = currentCommit.getBlob(givenAddedFile);
if (!areEquals(currentBlob, givenBlob)) {
conflict = true;
mergeConfictFiles(givenAddedFile, currentBlob, givenBlob);
stageFile(givenAddedFile);
}
}
}
for (String givenRemovedFile : givenRemoved) {
if (!currentModified.contains(givenRemovedFile) && !currentRemoved.contains(givenRemovedFile)) {
rm(givenRemovedFile);
}
if (currentModified.contains(givenRemovedFile)) {
Blob currentBlob = currentCommit.getBlob(givenRemovedFile);
conflict = true;
mergeConfictFiles(givenRemovedFile, currentBlob, null);
stageFile(givenRemovedFile);
}
}
commit("Merged " + branchName + " into " + currentBranch.getName() + ".", foundBranch.getHead());
if (conflict) {
throw new GitletException("Encountered a merge conflict.");
}
} catch (IOException e) {
}
}
@Override
public void save(File file) {
Saveable.super.save(file);
for (Branch branch : branches) {
String branchHash = Utils.sha1(branch.getName());
File branchFile = Paths.get(GITLET_HIDDEN_FOLDER, branchHash).toFile();
branch.save(branchFile);
}
}
@Override
public void calculateHashes() {
branchHashes = new ArrayList<>();
for (Branch branch : branches) {
branchHashes.add(Utils.sha1(branch.getName()));
}
currentBranchHash = Utils.sha1(currentBranch.getName());
currentCommitHash = currentCommit.getCommitId();
}
@Override
public void dump() {
String result = "Number of branches: " + branchHashes.size() + System.lineSeparator() +
"Current branch: " + currentBranchHash + System.lineSeparator() +
"Current commit: " + currentCommitHash + System.lineSeparator();
System.out.println(result);
}
public static void init() {
if (Files.exists(Paths.get(GITLET_HIDDEN_FOLDER))) {
throw new GitletException("A Gitlet version-control system already exists in the current directory.");
}
try {
Files.createDirectory(Paths.get(GITLET_HIDDEN_FOLDER));
Files.createDirectory(Paths.get(GITLET_HIDDEN_FOLDER, STAGING_FOLDER));
Files.createDirectory(Paths.get(GITLET_HIDDEN_FOLDER, STAGING_RM_FOLDER));
Commit commit = new Commit("initial commit", 0);
Branch branch = new Branch("master", commit);
Gitlet gitlet = new Gitlet(Collections.singletonList(branch), branch, commit);
gitlet.save(Paths.get(GITLET_HIDDEN_FOLDER, MAIN_FILENAME).toFile());
} catch (IOException e) {
e.printStackTrace();
}
}
public static Gitlet run() {
if (!Files.exists(Paths.get(GITLET_HIDDEN_FOLDER))) {
throw new GitletException("Not in an initialized Gitlet directory.");
}
assert Files.exists(Paths.get(GITLET_HIDDEN_FOLDER, STAGING_FOLDER));
return load(Paths.get(GITLET_HIDDEN_FOLDER, MAIN_FILENAME).toFile());
}
public static Gitlet load(File file) {
Gitlet gitlet = Utils.readObject(file, Gitlet.class);
gitlet.currentCommit = Commit.load(Paths.get(GITLET_HIDDEN_FOLDER, gitlet.currentCommitHash).toFile());
gitlet.currentBranch = Branch.load(Paths.get(GITLET_HIDDEN_FOLDER, gitlet.currentBranchHash).toFile());
gitlet.branches = new ArrayList<>();
for (String branchHash : gitlet.branchHashes) {
gitlet.branches.add(Branch.load(Paths.get(GITLET_HIDDEN_FOLDER, branchHash).toFile()));
}
return gitlet;
}
private Set<Commit> collectAllCommits() {
Set<Commit> allCommits = new HashSet<>();
for (Branch branch : branches) {
collectAllCommitsStep(branch.getHead(), allCommits);
}
return allCommits;
}
private void collectAllCommitsStep(Commit commit, Set<Commit> allCommits) {
allCommits.add(commit);
for (Commit parent : commit.getParentCommits()) {
collectAllCommitsStep(parent, allCommits);
}
}
private void clearStagingArea() throws IOException {
clearArea(Paths.get(GITLET_HIDDEN_FOLDER, STAGING_FOLDER));
clearArea(Paths.get(GITLET_HIDDEN_FOLDER, STAGING_RM_FOLDER));
}
private void clearWorkingDirectory() throws IOException {
clearArea(Paths.get(""));
}
private void clearArea(Path path) throws IOException {
Files.list(path)
.forEach(p -> {
try {
if ((!p.toString().equals(GITLET_HIDDEN_FOLDER))
&& (!temp.contains(p.toString())))
Files.deleteIfExists(p);
} catch (IOException e) {
e.printStackTrace();
}
});
}
private void checkUntrackedOverwritten(Commit newCommit) throws IOException {
for (Path filePath : Files.list(Paths.get("")).collect(Collectors.toList())) {
if (temp.contains(filePath.toString()))
continue;
if (filePath.toString().equals(GITLET_HIDDEN_FOLDER))
continue;
Blob blob = newCommit.getBlob(filePath.toString());
if (blob != null) {
if (currentCommit.getBlob(filePath.toString()) == null) {
throw new GitletException("There is an untracked file in the way; delete it, or add and commit it first.");
}
}
}
}
private Commit getSplitPoint(Branch branch) {
Map<String, Integer> prevCommits = new HashMap<>();
getPrevCommitsStep(prevCommits, 0, currentCommit);
Set<Commit> givenBranchCommits = new HashSet<>();
collectAllCommitsStep(branch.getHead(), givenBranchCommits);
String splitPoint = null;
int distance = Integer.MAX_VALUE;
for (Commit commit : givenBranchCommits) {
if (prevCommits.containsKey(commit.getCommitId())) {
int currDistance = prevCommits.get(commit.getCommitId());
if (currDistance < distance) {
splitPoint = commit.getCommitId();
distance = currDistance;
}
}
}
return getCommitById(splitPoint);
}
private void getPrevCommitsStep(Map<String, Integer> prevCommits, int distance, Commit commit) {
prevCommits.put(commit.getCommitId(), distance);
if (commit.getParentCommits() != null) {
for (Commit parentCommit : commit.getParentCommits()) {
getPrevCommitsStep(prevCommits, distance + 1, parentCommit);
}
}
}
private Commit getCommitById(String commitId) {
Collection<Commit> allCommits = collectAllCommits();
Commit foundCommit = null;
for (Commit commit : allCommits) {
if (commit.getCommitId().startsWith(commitId)) {
foundCommit = commit;
break;
}
}
if (foundCommit == null) {
throw new GitletException("No commit with that id exists.");
}
return foundCommit;
}
private Collection<String> getAdded(Commit oldCommit, Commit newCommit) {
Collection<String> result = new ArrayList<>();
Map<String, Blob> oldTree = oldCommit.getFileTree();
Map<String, Blob> newTree = newCommit.getFileTree();
for (Map.Entry<String, Blob> pair : newTree.entrySet()) {
String file = pair.getKey();
if (!oldTree.containsKey(file)) {
result.add(file);
}
}
return result;
}
private Collection<String> getModified(Commit oldCommit, Commit newCommit) {
Collection<String> result = new ArrayList<>();
Map<String, Blob> oldTree = oldCommit.getFileTree();
Map<String, Blob> newTree = newCommit.getFileTree();
for (Map.Entry<String, Blob> pair : oldTree.entrySet()) {
String file = pair.getKey();
if (newTree.containsKey(file)) {
byte[] oldBytes = pair.getValue().getBytes();
byte[] newBytes = newTree.get(file).getBytes();
if (!Arrays.equals(oldBytes, newBytes)) {
result.add(file);
}
}
}
return result;
}
private void stageFile(String fileName) throws IOException{
Files.copy(Paths.get(fileName), Paths.get(GITLET_HIDDEN_FOLDER, STAGING_FOLDER, fileName));
}
private boolean unstageFile(String fileName) throws IOException {
Path path = Paths.get(GITLET_HIDDEN_FOLDER, STAGING_FOLDER, fileName);
boolean removed = Files.exists(path);
if (removed) {
Files.delete(path);
return true;
}
return false;
}
private boolean unstageFileForRemove(String fileName) throws IOException {
Path path = Paths.get(GITLET_HIDDEN_FOLDER, STAGING_RM_FOLDER, fileName);
boolean removed = Files.exists(path);
if (removed) {
Files.delete(path);
return true;
}
return false;
}
private boolean areEquals(String fileName, Blob blob) throws IOException {
byte[] bytes = Files.readAllBytes(Paths.get(fileName));
return Arrays.equals(bytes, blob.getBytes());
}
private boolean areEquals(Blob blob1, Blob blob2) throws IOException {
return Arrays.equals(blob1.getBytes(), blob2.getBytes());
}
private boolean hasStagingChanges() throws IOException{
return (Files.list(Paths.get(GITLET_HIDDEN_FOLDER, STAGING_FOLDER)).count() != 0)
|| (Files.list(Paths.get(GITLET_HIDDEN_FOLDER, STAGING_RM_FOLDER)).count() != 0);
}
private void applyStagingChangesToFileTree(Map<String, Blob> fileTree) throws IOException {
Files.list(Paths.get(GITLET_HIDDEN_FOLDER, STAGING_FOLDER)).forEach(path -> {
try {
byte[] bytes = Files.readAllBytes(path);
File file = path.toFile();
Blob blob = new Blob(file.getName(), file.lastModified(), bytes);
fileTree.put(file.getName(), blob);
} catch (IOException e) {
e.printStackTrace();
}
}
);
Files.list(Paths.get(GITLET_HIDDEN_FOLDER, STAGING_RM_FOLDER)).forEach(path -> {
File file = path.toFile();
fileTree.remove(file.getName());
}
);
}
private Set<String> getStagedFiles() throws IOException {
return Files.list(Paths.get(GITLET_HIDDEN_FOLDER, STAGING_FOLDER))
.map(p -> p.toFile().getName()).collect(Collectors.toSet());
}
private Set<String> getTrackedFiles() {
return getTrackedFiles(currentCommit);
}
private Set<String> getTrackedFiles(Commit commit) {
return commit.getFileTree().keySet();
}
private Set<String> getUntrackedFiles() throws IOException{
return getUntrackedFiles(currentCommit);
}
private Set<String> getUntrackedFiles(Commit commit) throws IOException {
return Files.list(Paths.get("")).map(p -> p.toFile().getName())
.filter(f -> !getTrackedFiles(commit).contains(f))
.filter(f -> !temp.contains(f)).collect(Collectors.toSet());
}
private void mergeConfictFiles(String fileName, Blob currentBlob, Blob givenBlob) throws IOException {
String content = "<<<<<< HEAD" + System.lineSeparator()
+ (currentBlob == null ? "" : new String(currentBlob.getBytes())) + "======" + System.lineSeparator()
+ (givenBlob == null ? "" : new String(givenBlob.getBytes())) + ">>>>>>";
Files.write(Paths.get(fileName), content.getBytes());
}
}