package gitlet; import java.io.File; import java.io.IOException; import java.io.Serial; import java.io.Serializable; import java.util.*; import static gitlet.Utils.*; /** * Represents a gitlet repository. * * @author Abraham Brionse */ public class Repository implements Serializable { /** * Serial ID so Java can serialize and deserialize. */ @Serial private static final long serialVersionUID = 1234567L; /** * The current working directory. */ public static final File CWD = new File(System.getProperty("user.dir")); /** * The .gitlet directory. */ public static final File GITLET_DIR = join(CWD, ".gitlet"); /** * The object directory in which blobs and commimts will be saved. */ public static final File OBJECT_DIR = join(GITLET_DIR, "objects"); /** * The file in whicih this repository will be saved. */ public static final File REPO_FILE = join(GITLET_DIR, "repo"); /** * The file that will keep track of which branch the repo currently points to. */ public final File HEAD_FILE = join(GITLET_DIR, "HEAD"); /** * The file that will keep track of all branches in the commit. */ public final File BRANCHES_FILE = join(GITLET_DIR, "branches"); /** * The stage area of the repo where files for addition/removal go. */ private Stage REPO_STAGE; /** * The SHA-1 String representation of the HEAD pointer. * This should follow the commit that is being checkoutBranch. By default, * it tracks the MASTER pointer. */ private String HEAD; /** * The latest commit that has been made on the active branch. * It denotes the HEAD commit on the active branch. */ private Commit HEAD_COMMIT; /** * All the commits that belong to this repository and branch. */ private LinkedHashMap<String, Commit> repoCommits; /** * The branches of this repo. It should allow movement between two branches. * The branches are in the form of branchName(String):Branch(branchName, commitSHA) * A branch is simply a reference to a commit. */ private LinkedHashMap<String, Branch> commitTree; /** * Does the required filesystem operataions to allow for persistance. * Created any necessary files or folders. * .gitlet/ - top level folder that indicated that a VCS exists in the directory * - objects/ folder containing all blob and commit objects */ public Repository() { this.repoCommits = new LinkedHashMap<>(); this.commitTree = new LinkedHashMap<>(); } public void setUpPersistence() throws IOException { if (!GITLET_DIR.exists()) { GITLET_DIR.mkdir(); } if (!OBJECT_DIR.exists()) { OBJECT_DIR.mkdir(); } if (!Stage.STAGE_FILE.exists()) { Stage.STAGE_FILE.createNewFile(); } if (!HEAD_FILE.exists()) { HEAD_FILE.createNewFile(); } if (!BRANCHES_FILE.exists()) { BRANCHES_FILE.createNewFile(); } REPO_STAGE = new Stage(); makeInitialCommit(); } /** * Make the initial commit that is done when initializing a reposiory. */ public void makeInitialCommit() { Commit initCommit = new Commit(); String commitSHA = initCommit.getCommitID(); this.repoCommits.put(commitSHA, initCommit); this.HEAD_COMMIT = new Commit(initCommit); initCommit.saveCommit(); System.out.println("THIS IS THE INITAL COMMIT:\n" + HEAD_COMMIT); Branch masterBranch = new Branch("master", commitSHA); this.commitTree.put("master", masterBranch); this.HEAD = "master"; Utils.writeObject(HEAD_FILE, this.HEAD); Utils.writeObject(BRANCHES_FILE, this.commitTree); } /** * Update the commit reference that the HEAD points to. Also updates the branch. * * @param newCommitSHA SHA-1 string of the newest commit that HEAD should point to */ private void updateHEADreference(String newCommitSHA) { commitTree.get(HEAD).updateCommitSHA(newCommitSHA); this.updateBranches(); } /** * Update the branch that the HEAD pointer points to. */ private void updateHEAD() { Utils.writeObject(HEAD_FILE, this.HEAD); } /** * Update the branches. */ private void updateBranches() { Utils.writeObject(BRANCHES_FILE, this.commitTree); } private Stage getRepoStage() { return Stage.getStage(); } /** * Adds a file to the staging area if it is different than the version that existed in the last commit. * If the file does not exist in the current working directory, it will exit with an error. * * @param fileName the name of the file in the current workind directory */ public void addFile(String fileName) { File fileToAdd = join(CWD, fileName); if (!fileToAdd.exists()) { GitletException error = new GitletException("File does not exist."); System.out.println(error.getMessage()); System.exit(0); } /* Make a temporary blob to record the file and compare with exists blobs in the latest commit. If the blob's share the same SHA, that indicates they are recording the same file and file contents and the file will not get added to the staging area. */ Blob tempBlob = new Blob(fileName); TreeMap<String, Blob> latestCommitFiles = this.HEAD_COMMIT.getCommitFiles(); // int empty = latestCommitFiles == null ? 0 : latestCommitFiles.size(); // System.out.println("IS THE LATEST COMMIT EMPTY OR DOES IT HAVE HOW MANY ITEMS " + empty); boolean sameFileContents = false; if (latestCommitFiles != null && latestCommitFiles.containsKey(fileName)) { Blob comparisonBlob = latestCommitFiles.get(fileName); sameFileContents = comparisonBlob.compareBlobs(tempBlob); if (sameFileContents) { System.out.println("No changes deteced in file."); } } if (!sameFileContents) { REPO_STAGE = getRepoStage(); REPO_STAGE.addFile(fileName); } } /** * Stage a file for removal in the next commit so that it is no longer trakced by the VCS. * * @param fileName the file to stop tracking */ public void removeFile(String fileName) { this.REPO_STAGE = getRepoStage(); TreeMap<String, Blob> latestCommitFiles = HEAD_COMMIT.getCommitFiles(); boolean latestCommitHasFile = latestCommitFiles.containsKey(fileName); if (!latestCommitHasFile) { System.out.println("No reason to remove the file."); System.exit(0); } this.REPO_STAGE.removeFile(fileName); } public TreeMap<String, Blob> getHeadCommitFiles() { return this.HEAD_COMMIT.getCommitFiles(); } /** * Created and persistently saved a commit object using a message * in the command line. * * @param message message to store in the commit object */ public void makeCommit(String message) { this.REPO_STAGE = getRepoStage(); TreeMap<String, Blob> newFilesToCommit = REPO_STAGE.getAddedFiles(); System.out.println("THE STAGING AREA JUST LOOKED LIKE THIS"); this.printRepoStatus(); System.out.print("CURRENT THE HEAD COMMIT LOOKS LIKE THIS\n"); System.out.println(this.HEAD_COMMIT); /* Combine files in the staging area and the previous commit for total file recollection in the new commit. Also removes files from the commit they are staged for removal. It makes sure not to overwrite the existing files that are in the staging area as the addFile method already checks for duplicate file content. */ if (this.HEAD_COMMIT.getCommitFiles() != null) { for (Blob savedBlob : this.getHeadCommitFiles().values()) { String savedBlobFileName = savedBlob.getTrackingFile(); if (newFilesToCommit.containsKey(savedBlobFileName)) { continue; } newFilesToCommit.put(savedBlobFileName, savedBlob); } if (this.REPO_STAGE.getRemovalFiles().size() > 0) { for (String fileName : this.REPO_STAGE.getRemovalFiles()) { newFilesToCommit.remove(fileName); } } } String parentID = this.HEAD_COMMIT.getCommitID(); Commit newCommit = new Commit(message, newFilesToCommit, parentID, this.HEAD); this.HEAD_COMMIT = new Commit(newCommit); System.out.println("THIS IS THE LATEST NEW COMMIT ON THE REPO:\n" + newCommit); System.out.println("THIS IS THE HEAD COMMIT ON THE REPO:\n" + this.HEAD_COMMIT); String newCommitSHA = newCommit.getCommitID(); this.repoCommits.put(newCommitSHA, newCommit); newCommit.saveCommit(); this.updateHEADreference(newCommitSHA); this.REPO_STAGE = new Stage(); } public void printRepoStatus() { REPO_STAGE = getRepoStage(); System.out.println("=== Branches ==="); System.out.println("*" + this.HEAD); for (Branch b : commitTree.values()) { System.out.println(b.toString()); } REPO_STAGE.printStagingArea(); } public void printAllCommits() { for (Commit c : this.repoCommits.values()) { System.out.println(c.toString()); } } private String getFirstKey() { String[] aKeys = repoCommits.keySet().toArray(new String[repoCommits.size()]); return aKeys[0]; } public void saveRepo() { Utils.writeObject(REPO_FILE, this); } public LinkedHashMap<String, Commit> getRepoCommits() { return this.repoCommits; } /** * Get the SHA string of the latest commit. * It is useful for assigning parentSHA strings. * * @return parentSHA string */ private String getParnetCommitSHA() { String[] aKeys = repoCommits.keySet().toArray(new String[repoCommits.size()]); return aKeys[aKeys.length - 1]; } /** * Print the commit of the current working branch starting at the head pointer to the inital commit. */ public void printRepoLog() { Commit headCommit = this.HEAD_COMMIT; String commitID; String dateTime; String message; String branchName; String pID; String fileContents; while (headCommit.getParentCommit() != null) { commitID = headCommit.getCommitID(); dateTime = headCommit.getDateTime(); message = headCommit.getMessage(); branchName = headCommit.getBranch(); pID = headCommit.getParentCommit(); fileContents = headCommit.getCommitFiles().values().toString(); System.out.println("==="); System.out.printf("commit %s\nParent: %s\nDate: %s\nFiles Tracking:%s\n%s\n%s\n", commitID, pID, dateTime, fileContents, message, branchName); System.out.println(); System.out.println("==="); headCommit = repoCommits.get(headCommit.getParentCommit()); } Commit initCommit = repoCommits.get(getFirstKey()); commitID = initCommit.getCommitID(); dateTime = initCommit.getDateTime(); message = initCommit.getMessage(); branchName = initCommit.getBranch(); pID = initCommit.getParentCommit(); System.out.println("==="); System.out.printf("commit %s\nParent: %s\nDate: %s\n%s\n%s\n", commitID, pID, dateTime, message, branchName); System.out.println(); System.out.println("==="); } /** * Prints the all the commits ever made in this branch by descending date order. */ public void printRepoGlobalLog() { String commitID; String dateTime; String message; String branchName; String pID; Commit tempCommit; // Set<Map.Entry<String, Commit>> commits = this.repoCommits.entrySet(); // Iterator<Map.Entry<String, Commit>> i = commits.iterator(); List<String> commitKeys = new ArrayList<>(this.repoCommits.keySet()); Collections.reverse(commitKeys); for (String key : commitKeys) { commitID = key; tempCommit = this.repoCommits.get(key); dateTime = tempCommit.getDateTime(); message = tempCommit.getMessage(); branchName = tempCommit.getBranch(); pID = tempCommit.getParentCommit(); System.out.println("==="); System.out.printf("commit %s\nParent: %s\nDate: %s\n%s\n%s\n", commitID, pID, dateTime, message, branchName); System.out.println(); System.out.println("==="); } } /** * Make a new branch in the repo. If the branch already exists, exit. * * @param branchName new branch name */ public void branch(String branchName) { if (commitTree.containsKey(branchName)) { System.out.println("A branch with that name already exists"); System.exit(0); } Branch newBranch = new Branch(branchName, this.HEAD_COMMIT.getCommitID()); commitTree.put(branchName, newBranch); this.updateBranches(); } /** * Takes all files in the commit at the head of the given branch, and puts them in * the working directory, overwriting the versions of the files that are already there * \if they exist. Also, at the end of this command, the given branch will now be * considered the current branch (HEAD). Any files that are tracked in the current branch * but are not present in the checked-out branch are deleted. The staging area is cleared, * unless the checked-out branch is the current branch. * * @param branchName name of branch */ public void checkout(String branchName) { if (!commitTree.containsKey(branchName)) { System.out.println("No such branch exists"); System.exit(0); } else if (Objects.equals(this.HEAD, branchName)) { System.out.println("No need to checkout the current branch."); System.exit(0); } /* Compare the files in the user directory to the files that are being tracked in the current working branch to make sure the user has commited, add, or deleted them before overwriting files. */ List<String> userFiles = plainFilenamesIn(CWD); assert userFiles != null; for (String fileName : userFiles) { boolean userFilePreviouslyCommited = this.getHeadCommitFiles().containsKey(fileName); if (Objects.equals(fileName, ".DS_Store")) { continue; } if (!userFilePreviouslyCommited) { System.out.println("There is an untracked file in the way; delete it," + "or add and commit it first: " + fileName); System.exit(0); } System.out.println("exists in the current commit: " + this.HEAD + "\n" + fileName); } /* Once the user has commited, added, or deleted the file that were not being tracked, the HEAD branch is switched to branchName. The blobs that exists at this HEAD commit will override the file contents in the user directory. It also reassigns the HEAD commit. It will delete files in the CWD if the new branch did not previously commit them. */ String headCommitSHA = commitTree.get(branchName).getCommitSHA(); this.HEAD_COMMIT = repoCommits.get(headCommitSHA); this.HEAD = branchName; TreeMap<String, Blob> newHeadFiles = this.getHeadCommitFiles(); this.updateHEAD(); for (String fileName : userFiles) { boolean storedFileExistsinUserDir = newHeadFiles.containsKey(fileName); if (storedFileExistsinUserDir) { Blob tempBlob = newHeadFiles.get(fileName); File checkoutFile = join(CWD, fileName); byte[] checkoutFileContents = tempBlob.getTrackingFileContents(); writeContents(checkoutFile, checkoutFileContents); } else { restrictedDelete(fileName); } } this.REPO_STAGE = new Stage(); } /** * Takes the version of the file as it exists in the head commit and puts it in the working directory, * overwriting the version of the file that’s already there if there is one. * The new version of the file is not staged. * * @param fileName the name of the file to access */ public void checkoutFile(String fileName) { File checkoutFile = join(CWD, fileName); TreeMap<String, Blob> currentHeadFiles = this.getHeadCommitFiles(); if (!currentHeadFiles.containsKey(fileName)) { System.out.println("File does not exist in that commit."); System.exit(0); } Blob tempBlob = currentHeadFiles.get(fileName); byte[] checkoutFileContents = tempBlob.getTrackingFileContents(); writeContents(checkoutFile, checkoutFileContents); } /** * Takes the version of the file as it exists in the commit with the given id, * and puts it in the working directory, overwriting the version of the file * that’s already there if there is one. The 1`version of the file is not staged. * * @param commitID ID of an existing commit * @param fileName name of the file to retrieve */ public void checkout(String commitID, String fileName) { File checkoutFile = join(CWD, fileName); if (!repoCommits.containsKey(commitID)) { System.out.println("No commit with that id exists"); System.exit(0); } Commit tempCommit = repoCommits.get(commitID); for (Blob b : tempCommit.getCommitFiles().values()) { String storedFile = b.getTrackingFile(); if (storedFile.equals(fileName)) { byte[] newFileContents = b.getTrackingFileContents(); writeContents(checkoutFile, newFileContents); } else { System.out.println("File does not exist in that commit"); System.exit(0); } } } }