package gitlet; import java.io.File; import java.io.IOException; import java.util.*; import static gitlet.Utils.*; /** Represents a gitlet repository. * gitlet is a version control system that saves versions of files and allows the user to * retrieve older versions of files. * @author Bassem Halim */ public class Repository { private static Commit head; // stores the name and ID (Sha1) of branches(commit node) private static HashMap branches; /** 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"); public static final File COMMIT_DIR = join(GITLET_DIR, "commit"); public static final File STAGING_DIR_ADD = join(GITLET_DIR, "staging_add"); public static final File STAGING_DIR_RM = join(GITLET_DIR, "stageing_rm"); public static final File HEAD_FILE = join(GITLET_DIR, "HEAD.txt"); public static final File BLOBS_DIR = join(COMMIT_DIR, "BLOBS"); public static final File BRANCHES = join(GITLET_DIR, "Branches.txt"); /** * returns true if a repo is initialized in CWD */ protected static void isInit() { if (!GITLET_DIR.exists()) { System.out.println("Not in an initialized Gitlet directory."); System.exit(0); } } /** * creates directory and creates the master branch with the first commit * with -m "initial commit and time stamp 00:00:00 UTC, Thursday, 1 January 1970 */ public static void initialize() { // if a repo is already initialized, abort and print error msg if (GITLET_DIR.mkdir()) { // mkdir() returns False if GITLET_DIR already exists try { COMMIT_DIR.mkdir(); BLOBS_DIR.mkdir(); STAGING_DIR_ADD.mkdir(); STAGING_DIR_RM.mkdir(); BRANCHES.createNewFile(); // Date(0) => 00:00:00 UTC 1 January 1970 head = new Commit("initial commit", (long) 0, null); branches = new HashMap<>(); branches.put("master", head.getID()); branches.put("current_branch", "master"); //the current branch is master //currBranchName = "master"; File initCmtDIR = join(COMMIT_DIR, getAbv(head.getID())); initCmtDIR.mkdir(); //naming the commit file with the sha1 of the commit File initCommit = join(initCmtDIR, head.getID() + ".txt"); initCommit.createNewFile(); writeObject(initCommit, head); HEAD_FILE.createNewFile(); writeObject(HEAD_FILE, head); writeObject(BRANCHES, branches); } catch (IOException excp) { System.out.println(excp.getMessage()); } } else { System.out.println("A Gitlet version-control system already" + " exists in the current directory."); System.exit(0); } } @SuppressWarnings("unchecked") private static void read() { head = readObject(HEAD_FILE, Commit.class); branches = readObject(BRANCHES, HashMap.class); } /** * saves the current head and branches */ private static void save() { writeObject(HEAD_FILE, head); writeObject(BRANCHES, branches); } /** *adds .txt to the staging area (if it already exits there, overwrite it) * if .txt had the same content as the last commit, do not stage it */ /** *returns abbreviated cmt ID */ private static String getAbv(String cmtID) { return cmtID.substring(0, 4); } /** * stages filename for addition by saving it in STAGING_DIR */ public static void add(String filename) { read(); File fileToAdd = join(CWD, filename); if (fileToAdd.exists()) { String content = readContentsAsString(fileToAdd); String sha1content = sha1(content); //head = getHead(); File removed = join(STAGING_DIR_RM, filename); removed.delete(); //if file staged for removal, remove it from staging for removal DIR //if file already in head and has the same content if (head.contains(filename) && (sha1content.equals(head.getsha1(filename)))) { File staged = join(STAGING_DIR_ADD, filename); if (staged.exists()) { staged.delete(); } return; } else { File toStage = join(STAGING_DIR_ADD, filename); try { toStage.createNewFile(); } catch (IOException excp) { System.out.println(excp.getMessage()); } writeContents(toStage, content); } } else { System.out.println("File does not exist."); } } //returns the name of the current branch private static String currBrnach() { return branches.get("current_branch"); } /** * Saves a snapshot of tracked files in the current commit and staging area so * they can be restored at a later time * the parents argument it used for the merge command */ public static void commit(String msg, String[] parents) { File[] added = STAGING_DIR_ADD.listFiles(); File[] deleted = STAGING_DIR_RM.listFiles(); if (added.length != 0 || deleted.length != 0) { read(); Commit oldHead = head; if (parents == null) { parents = new String[]{oldHead.getID()}; } head = new Commit(msg, parents); head.addStaged(added); head.addUnchanged(plainFilenamesIn(STAGING_DIR_RM)); String currentBranch = currBrnach(); branches.put(currentBranch, head.getID()); // update the pointer of the current branch String abvID = getAbv(head.getID()); File newCommitDIR = join(COMMIT_DIR, abvID); File newCommit = join(newCommitDIR, head.getID() + ".txt"); cleanDir(STAGING_DIR_ADD); cleanDir(STAGING_DIR_RM); try { writeObject(BRANCHES, branches); writeObject(HEAD_FILE, head); newCommitDIR.mkdir(); newCommit.createNewFile(); writeObject(newCommit, head); } catch (IOException excp) { throw new IllegalArgumentException(excp.getMessage()); } } else { System.out.println("No changes added to the commit."); } } /** * logs all commit data starting from head location to the initial commit */ public static void log() { read(); Commit curr = head; while (curr != null) { curr.print(); Commit[] cmts = curr.getPrev(); //ignore second parent if (cmts == null) { curr = null; } else { curr = cmts[0]; } } } /** *returns the commit with the given ID if it exists, else null */ private static Commit getCommit(String cmtID) { if (cmtID == null) { return head; } else if (cmtID.length() == 40) { String abv = getAbv(cmtID); File dir = join(COMMIT_DIR, abv); if (dir.exists()) { File cmtFile = join(dir, cmtID + ".txt"); return readObject(cmtFile, Commit.class); } } else { //abbreviated id File[] dirs = COMMIT_DIR.listFiles(); for (File d :dirs) { String name = d.getName(); //check if the first 4 chars of the dir match the cmtID if (cmtID.startsWith(name)) { File[] files = d.listFiles(); Commit cmt = readObject(files[0], Commit.class); return cmt; } } } System.out.println("No commit with that id exists."); System.exit(0); return null; } /** * checkout filename from the given commit to the CWD */ public static void checkout(String filename, String commitID) { read(); Commit cmt = getCommit(commitID); if (!cmt.contains(filename)) { System.out.println("File does not exist in that commit."); return; } File stored = cmt.getFile(filename); String content = readContentsAsString(stored); File newFile = join(CWD, filename); if (newFile.exists()) { //overwrite writeContents(newFile, content); } else { //create new File //save content in the new file try { newFile.createNewFile(); writeContents(newFile, content); } catch (IOException excp) { throw new IllegalArgumentException(excp.getMessage()); } } } /** *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. * * it is also used for the reset command */ public static void checkoutBranch(String branchName, boolean reset, String id) { read(); Commit other; if (!reset) { if (!branches.containsKey(branchName)) { System.out.println("No such branch exists."); return; } else if (currBrnach().equals(branchName)) { System.out.println("No need to checkout the current branch."); return; } String branchID = branches.get(branchName); other = getCommit(branchID); } else { other = getCommit(id); } List filesInCWD = plainFilenamesIn(CWD); List headFiles = head.getFiles(); List otherFiles = other.getFiles(); for (String file : filesInCWD) { //tracked in other but not in Head if (!headFiles.contains(file) && otherFiles.contains(file)) { System.out.println("There is an untracked file in the way; delete it," + "or add and commit it first."); return; } } //copy files from given branch to CWD for (String file :otherFiles) { File inBranch = other.getFile(file); File inCWD = join(CWD, file); if (!inCWD.exists()) { createFile(inCWD); } String contents = readContentsAsString(inBranch); writeContents(inCWD, contents); } //delete files that are in curr head and not in given branch for (String file: headFiles) { if (!otherFiles.contains(file)) { File inCWD = join(CWD, file); inCWD.delete(); } } head = other; if (reset) { //move current branch pointer to the given commit branches.put(branchName, head.getID()); } branches.put("current_branch", branchName); save(); //clear staging area cleanDir(STAGING_DIR_ADD); cleanDir((STAGING_DIR_RM)); } /** * removes file from CWD and from add staging area **/ public static void remove(String filename) { File staged = join(STAGING_DIR_ADD, filename); read(); // file not staged for addition or tracked by head commit if (!staged.exists() && !head.contains(filename)) { System.out.println("No reason to remove the file."); return; } if (staged.exists()) { staged.delete(); } if (head.contains(filename)) { File toRemove = join(STAGING_DIR_RM, filename); createFile(toRemove); File tracked = head.getFile(filename); String content = readContentsAsString(tracked); writeContents(toRemove, content); File toDelete = join(CWD, filename); toDelete.delete(); //delete the file if it is still in CWD } } /** * prints all commits */ public static void globalLog() { File[] directoris = COMMIT_DIR.listFiles(); for (File dir: directoris) { String name = dir.getName(); if (name.equals("BLOBS")) { continue; } Commit cmt = getCommit(name); cmt.print(); } } /** finds the commit with the given msg and prints its ID */ public static void find(String msg) { int count = 0; File[] dirs = COMMIT_DIR.listFiles(); for (File d: dirs) { String id = d.getName(); if (id.equals("BLOBS")) { continue; } Commit cmt = getCommit(id); if (cmt.find(msg)) { count++; System.out.println(cmt.getID()); } } if (count == 0) { System.out.println("Found no commit with that message."); } } /** prints the status of the repository */ public static void status() { read(); //Branches System.out.println("=== Branches ==="); System.out.printf("*%s\n", currBrnach()); for (String key : branches.keySet()) { if (key.equals("current_branch") || key.equals(currBrnach())) { continue; } else { System.out.println(key); } } System.out.println(); //Staged Files System.out.println("=== Staged Files ==="); List stagedFiles = Utils.plainFilenamesIn(STAGING_DIR_ADD); for (String file: stagedFiles) { System.out.println(file); } System.out.println(); //Removed Files System.out.println("=== Removed Files ==="); List removedFiles = Utils.plainFilenamesIn(STAGING_DIR_RM); for (String file: removedFiles) { System.out.println(file); } System.out.println(); //=== Modifications Not Staged For Commit === System.out.println("=== Modifications Not Staged For Commit ==="); printModified(); System.out.println(); //=== Untracked Files === System.out.println("=== Untracked Files ==="); printUntracked(); } private static void printUntracked() { List inCWD = plainFilenamesIn(CWD); List added = plainFilenamesIn(STAGING_DIR_ADD); for (String f : inCWD) { //file in cwd and is untracked in head nor staged for addition if (!head.trackedFiles.containsKey(f) && !added.contains(f)) { System.out.println(f); } } } private static void printModified() { read(); List inCWD = plainFilenamesIn(CWD); List added = plainFilenamesIn(STAGING_DIR_ADD); List del = plainFilenamesIn(STAGING_DIR_RM); for (String f : head.trackedFiles.keySet()) { if (!added.contains(f) && inCWD.contains(f)) { File file = join(CWD, f); String content = readContentsAsString(file); String sha1 = sha1(content); if (!sha1.equals(head.trackedFiles.get(f))) { System.out.println(f + " (modified)"); } } } for (String f : added) { if (inCWD.contains(f)) { File file = join(CWD, f); String content = readContentsAsString(file); File add = join(STAGING_DIR_ADD, f); String contentAdd = readContentsAsString(add); if (!contentAdd.equals(content)) { System.out.println(f + " (modified)"); } } else { System.out.println(f + " (deleted)"); } } } /** * creates a new pointer to head */ public static void branch(String branchName) { read(); if (branches.containsKey(branchName)) { System.out.println("A branch with that name already exists."); return; } String newBranchID = head.getID(); branches.put(branchName, newBranchID); save(); } /** * removes the pointer with name [branch name] without deleting commits */ public static void rmBranch(String branchName) { read(); if (!branches.containsKey(branchName)) { System.out.println("A branch with that name does not exist."); return; } if (branchName.equals(currBrnach())) { System.out.println("Cannot remove the current branch."); return; } branches.remove(branchName); save(); } /** * Checks out all the files tracked by the given commit */ public static void reset(String cmtID) { read(); String currentBranch = currBrnach(); checkoutBranch(currentBranch, true, cmtID); } /** * merges the files from the given branch to the current branch * and commits */ public static void merge(String branch) { read(); List added = plainFilenamesIn(STAGING_DIR_ADD); List removed = plainFilenamesIn(STAGING_DIR_RM); mergeExcep(branch, added, removed); String branchId = branches.get(branch); Commit other = getCommit(branchId); List inCWD = plainFilenamesIn(CWD); List inOther = other.getFiles(); List inHead = head.getFiles(); for (String file : inCWD) { //tracked in other but not in Head if (!inHead.contains(file) && inOther.contains(file)) { System.out.println("There is an untracked file in the way; delete it," + "or add and commit it first."); return; } } Commit splitPoint = getSplitPoint(head, other); if (splitPoint.equal(other)) { System.out.println("Given branch is an ancestor of the current branch."); return; } else if (splitPoint.equal(head)) { checkoutBranch(branch, false, null); System.out.println("Current branch fast-forwarded."); return; } Boolean conflict = false; List spFiles = splitPoint.getFiles(); for (String f: spFiles) { //modified in other and curr (in different ways) => merge conflict if (modified(f, splitPoint, other) && modified(f, splitPoint, head)) { File blobOther = other.getFile(f); String contentOther = ""; if (blobOther != null) { contentOther = readContentsAsString(blobOther); } File blobHead = head.getFile(f); String contentHead = ""; if (blobHead != null) { contentHead = readContentsAsString(blobHead); } if (!contentHead.equals(contentOther)) { mergeConflict(f, contentHead, contentOther); conflict = true; } //modified in head but not in other } else if (modified(f, splitPoint, other) && !modified(f, splitPoint, head)) { //unmodified in head but not present in other if (!other.trackedFiles.containsKey(f)) { remove(f); } else { checkout(f, other.getID()); add(f); } } } //not in split nor head but in other for (String f: inOther) { if (!head.trackedFiles.containsKey(f) && !splitPoint.trackedFiles.containsKey(f)) { checkout(f, other.getID()); add(f); } } String msg = "Merged " + branch + " into " + currBrnach() + "."; String[] parents = {head.getID(), other.getID()}; commit(msg, parents); if (conflict) { System.out.println("Encountered a merge conflict."); } } /** * handles some merge exceptions */ private static void mergeExcep(String branch, List added, List removed) { if (!added.isEmpty() || !removed.isEmpty()) { System.out.println("You have uncommitted changes."); System.exit(0); } if (branch.equals(currBrnach())) { System.out.println("Cannot merge a branch with itself."); System.exit(0); } if (!branches.containsKey(branch)) { System.out.println("A branch with that name does not exist."); System.exit(0); } } /** * generates merge conflict file and stages it for addition */ private static void mergeConflict(String filename, String headContent, String branchContent) { String newContent = "<<<<<<< HEAD\n" + headContent + "=======\n" + branchContent + ">>>>>>>\n"; File newFile = join(CWD, filename); createFile(newFile); writeContents(newFile, newContent); add(filename); } /** * returns the parent node of both branches( the latest common ancestor) */ private static Commit getSplitPoint(Commit curr, Commit given) { Set seen = new HashSet<>(); Deque queue = new ArrayDeque<>(); queue.add(curr); //BFS search on curr while (!queue.isEmpty()) { Commit cmt = queue.remove(); if (cmt.parents != null) { for (String c : cmt.parents) { if (!seen.contains(c)) { Commit temp = getCommit(c); queue.add(temp); } } } seen.add(cmt.getID()); } queue.add(given); while (!queue.isEmpty()) { Commit cmt = queue.remove(); if (seen.contains(cmt.getID())) { return cmt; } if (cmt.parents != null) { for (String c : cmt.parents) { Commit temp = getCommit(c); queue.add(temp); } } } return null; } /** * returns true if the file is modified from its version in splitPoint * deleted files are considered modified */ private static boolean modified(String filename, Commit splitPoint, Commit other) { if (!other.trackedFiles.containsKey(filename)) { return true; } String contentSP = splitPoint.trackedFiles.get(filename); String contentOther = other.trackedFiles.get(filename); if (contentOther.equals(contentSP)) { return false; } return true; } }