Gitlet / src / gitlet / Repository.java
Repository.java
Raw
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<String, String> 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 <filename>.txt to the staging area (if it already exits there, overwrite it)
     * if <filename>.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<String> filesInCWD = plainFilenamesIn(CWD);
        List<String> headFiles = head.getFiles();
        List<String> 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<String> 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<String> 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<String> inCWD = plainFilenamesIn(CWD);
        List<String> 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<String> inCWD = plainFilenamesIn(CWD);
        List<String> added = plainFilenamesIn(STAGING_DIR_ADD);
        List<String> 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<String> added = plainFilenamesIn(STAGING_DIR_ADD);
        List<String> removed = plainFilenamesIn(STAGING_DIR_RM);
        mergeExcep(branch, added, removed);

        String branchId = branches.get(branch);
        Commit other = getCommit(branchId);
        List<String> inCWD = plainFilenamesIn(CWD);
        List<String> inOther = other.getFiles();
        List<String> 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<String> 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<String> added, List<String> 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<String> seen = new HashSet<>();
        Deque<Commit> 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;
    }

}