Gitlet / Command.java
Command.java
Raw
package gitlet;

import java.io.File;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.List;
import java.util.LinkedList;

/** The command class for Gitlet. Works with the inputs from
 *  Main and calls the appropriate methods.
 *  @author Ethan Ikegami
 */
public class Command {
    /** Current Working Directory. */
    static final File CWD = new File(".");

    /** Gitlet folder. */
    static final File GITLET_FOLDER = Utils.join(CWD, ".gitlet");

    /** Commits folder. */
    static final File COMMIT_DIR = Utils.join(GITLET_FOLDER, "commits");

    /** Blobs folder. */
    static final File BLOBS_DIR = Utils.join(GITLET_FOLDER, "blobs");

    /** Staging file. */
    static final File STAGING = Utils.join(GITLET_FOLDER, "staging");

    /** Branches file. */
    static final File BRANCHES = Utils.join(GITLET_FOLDER, "branches");

    /** Method that is called for the Gitlet command: init. */
    public static void init() {
        GITLET_FOLDER.mkdir();
        COMMIT_DIR.mkdir();
        BLOBS_DIR.mkdir();

        Commit initial = new Commit("initial commit", null, null);
        String initialSha = Utils.sha1(Utils.serialize(initial));
        Branch master = new Branch();
        master.newBranch("master", initialSha);
        master.setCurrBranch("master");

        Utils.writeObject(Utils.join(COMMIT_DIR, initialSha), initial);
        Utils.writeObject(BRANCHES, master);
        Utils.writeObject(STAGING, new Staging(null));
    }

    /** Method that is called for the Gitlet command: add [FILENAME]. */
    public static void add(String fileName) {
        File toAdd = Utils.join(CWD, fileName);
        Staging current = new Staging(STAGING);
        if (!toAdd.exists()) {
            System.err.printf("File does not exist.");
            System.exit(0);
        } else {
            Blob fileToBlob = new Blob(toAdd);
            String contents = Utils.sha1(Utils.serialize(fileToBlob));
            if (current.getRemovalStage().contains(fileName)) {
                current.removeFromRemoval(fileName);
            } else if (!Utils.join(BLOBS_DIR, contents).exists()) {
                current.add(fileToBlob);
            }
        }
        Utils.writeObject(STAGING, current);
    }

    /** Method that is called for the Gitlet command: commit [MESSAGE].
     *  Also takes in a COMMITSTR and SECONDPARENT if the commit is
     *  a merge. */
    public static void commit(String message, String commitStr,
                              Commit secondParent) {
        Branch curr = Utils.readObject(BRANCHES, Branch.class);
        String prevSha = curr.getCurrCommitID();
        Commit prev = Utils.readObject(Utils.join(COMMIT_DIR,
                prevSha), Commit.class);
        Staging stage = Utils.readObject(STAGING,
                Staging.class);

        if (stage.getCurrentStage().size() == 0
                && stage.getRemovalStage().size() == 0) {
            System.err.printf("No changes added to the commit.");
            System.exit(0);
        }
        for (Map.Entry<String, byte[]> entry
                : stage.getCurrentStage().entrySet()) {
            File blobName = Utils.join(BLOBS_DIR,
                    Utils.sha1(entry.getValue()));
            Utils.writeContents(blobName, entry.getValue());
        }

        Commit newComm = new Commit(message, prev,
                prev.getFiles());
        if (commitStr != null) {
            newComm.setMergeText(commitStr);
            newComm.setCommit2(secondParent);
            newComm.setFiles2(secondParent);
        }
        newComm.updateComm(stage);

        String newSha = Utils.sha1(Utils.serialize(newComm));
        curr.updateBranch(newSha);
        Utils.writeObject(Utils.join(COMMIT_DIR, newSha),
                newComm);
        Utils.writeObject(BRANCHES, curr);
        STAGING.delete();
    }

    /** Method that is called for the Gitlet command: log. */
    public static void log() {
        Branch curr = Utils.readObject(BRANCHES, Branch.class);
        String prevSha = curr.getCurrCommitID();
        Commit prev = Utils.readObject(Utils.join(COMMIT_DIR,
                prevSha), Commit.class);

        logHelper(prevSha, prev);
        for (String i : prev.getCommits()) {
            Commit id = Utils.readObject(Utils.join(COMMIT_DIR,
                    i), Commit.class);
            logHelper(i, id);
        }
    }

    /** Method that is called for the Gitlet command: checkout
     *  [COMMITID] -- [FILENAME]. COMMITID can be null which means
     *  the commit HEAD is pointing to will be used. */
    public static void checkoutFile(String commitID, String fileName) {
        if (commitID == null) {
            Branch curr = Utils.readObject(BRANCHES, Branch.class);
            commitID = curr.getCurrCommitID();
        }

        if (commitID.length() < 10) {
            commitID = commitIDFinder(commitID);
        }

        File commitFile = Utils.join(COMMIT_DIR, commitID);
        if (!commitFile.exists()) {
            System.err.printf("No commit with that id exists.");
            System.exit(0);
        }
        Commit comm = Utils.readObject(commitFile, Commit.class);
        overwriteFile(comm, fileName);
    }

    /** Method that is called for the Gitlet command: remove [FILENAME]. */
    public static void remove(String fileName) {
        Branch curr = Utils.readObject(BRANCHES, Branch.class);
        String prevSha = curr.getCurrCommitID();
        Commit prev = Utils.readObject(Utils.join(COMMIT_DIR,
                prevSha), Commit.class);
        Staging current = new Staging(STAGING);

        if (!prev.getFiles().containsKey(fileName)
                && !current.getCurrentStage().containsKey(fileName)) {
            System.err.printf("No reason to remove the file.");
            System.exit(0);
        } else {
            current.removeFrom(fileName);
            if (prev.getFiles().containsKey(fileName)) {
                current.addRemove(fileName);
                Utils.restrictedDelete(Utils.join(CWD, fileName));
            }
        }

        Utils.writeObject(STAGING, current);
    }

    /** Method that is called for the Gitlet command: global-log. */
    public static void globalLog() {
        for (String commitID : Utils.plainFilenamesIn(COMMIT_DIR)) {
            Commit comm = Utils.readObject(Utils.join(COMMIT_DIR,
                    commitID), Commit.class);
            logHelper(commitID, comm);
        }
    }

    /** Method that is called for the Gitlet command: find [MESSAGE]. */
    public static void find(String message) {
        boolean notFound = true;
        for (String commitID : Utils.plainFilenamesIn(COMMIT_DIR)) {
            Commit comm = Utils.readObject(Utils.join(COMMIT_DIR,
                    commitID), Commit.class);
            if (comm.getMessage().equals(message)) {
                System.out.println(commitID);
                notFound = false;
            }
        }
        if (notFound) {
            System.err.printf("Found no commit with that message.");
            System.exit(0);
        }
    }

    /** Method that is called for the Gitlet command: checkout [BRANCHNAME]. */
    public static void checkoutBranch(String branchName) {
        Branch curr = Utils.readObject(BRANCHES, Branch.class);
        if (curr.getCurrBranch().equals(branchName)) {
            System.err.printf("No need to checkout "
                    + "the current branch.");
            System.exit(0);
        }
        LinkedList<String> untracked = untrackedFiles();

        LinkedHashMap<String, String> commPrev =
                Utils.readObject(Utils.join(COMMIT_DIR,
                curr.getCurrCommitID()), Commit.class).getFiles();
        curr.setCurrBranch(branchName);
        Commit commCurr = Utils.readObject(Utils.join(COMMIT_DIR,
                curr.getCurrCommitID()), Commit.class);
        untrackCheck(commCurr, untracked);

        for (Map.Entry<String, String> entry
                : commCurr.getFiles().entrySet()) {
            overwriteFile(commCurr, entry.getKey());
            commPrev.remove(entry.getKey());
        }
        for (Map.Entry<String, String> entry : commPrev.entrySet()) {
            Utils.restrictedDelete(Utils.join(CWD, entry.getKey()));
        }

        STAGING.delete();
        Utils.writeObject(BRANCHES, curr);
    }

    /** Method that is called for the Gitlet command: reset [COMMITID]. */
    public static void reset(String commitID) {
        if (commitID.length() < 10) {
            commitID = commitIDFinder(commitID);
        }
        File commitFile = Utils.join(COMMIT_DIR, commitID);
        if (!commitFile.exists()) {
            System.err.printf("No commit with that id exists.");
            System.exit(0);
        }
        Branch curr = Utils.readObject(BRANCHES, Branch.class);
        LinkedHashMap<String, String> commPrev =
                Utils.readObject(Utils.join(COMMIT_DIR,
                curr.getCurrCommitID()), Commit.class).getFiles();
        Commit commCurr = Utils.readObject(commitFile, Commit.class);
        untrackCheck(commCurr, null);
        for (Map.Entry<String, String> entry : commCurr.getFiles().entrySet()) {
            overwriteFile(commCurr, entry.getKey());
            commPrev.remove(entry.getKey());
        }
        for (Map.Entry<String, String> entry : commPrev.entrySet()) {
            Utils.restrictedDelete(Utils.join(CWD, entry.getKey()));
        }

        curr.updateBranch(commitID);
        STAGING.delete();
        Utils.writeObject(BRANCHES, curr);
    }

    /** Method that is called for the Gitlet command: branch [BRANCHNAME]. */
    public static void branch(String branchName) {
        Branch curr = Utils.readObject(BRANCHES, Branch.class);
        if (curr.getBranches().containsKey(branchName)) {
            System.err.printf("A branch with that name already exists.");
            System.exit(0);
        }
        curr.newBranch(branchName, curr.getCurrCommitID());
        Utils.writeObject(BRANCHES, curr);
    }

    /** Method that is called for the Gitlet command: rm-branch [BRANCHNAME]. */
    public static void rmBranch(String branchName) {
        Branch curr = Utils.readObject(BRANCHES, Branch.class);
        if (!curr.getBranches().containsKey(branchName)) {
            System.err.printf("A branch with that name does not exist.");
            System.exit(0);
        } else if (curr.getCurrBranch().equals(branchName)) {
            System.err.printf("Cannot remove the current branch.");
            System.exit(0);
        } else {
            curr.remove(branchName);
        }
        Utils.writeObject(BRANCHES, curr);
    }

    /** Method that is called for the Gitlet command: status. */
    public static void status() {
        System.out.println("=== Branches ===");
        Branch curr = Utils.readObject(BRANCHES, Branch.class);
        for (String i : curr.getBranches().keySet()) {
            if (i.equals(curr.getCurrBranch())) {
                System.out.println("*" + i);
            } else {
                System.out.println(i);
            }
        }
        System.out.println("\n=== Staged Files ===");
        Staging current = new Staging(STAGING);
        for (String i : current.getCurrentStage().keySet()) {
            System.out.println(i);
        }
        System.out.println("\n=== Removed Files ===");
        for (String i : current.getRemovalStage()) {
            System.out.println(i);
        }
        System.out.println("\n=== Modifications Not Staged For Commit ===");
        System.out.println("\n=== Untracked Files ===");
        for (String i : untrackedFiles()) {
            System.out.println(i);
        }
        System.out.println();
    }

    /** Method that is called for the Gitlet command: merge [BRANCHNAME]. */
    public static void merge(String branchName) {
        Branch curr = Utils.readObject(BRANCHES, Branch.class);
        String commitID1 = curr.getBranches().get(curr.getCurrBranch());
        String commitID2 = curr.getBranches().get(branchName);
        if (curr.getCurrBranch().equals(branchName)) {
            System.err.printf("Cannot merge a branch with itself.");
            System.exit(0);
        }
        if (commitID2 == null) {
            System.err.printf("A branch with that name does not exist.");
            System.exit(0);
        }
        Commit comm1 = Utils.readObject(Utils.join(COMMIT_DIR, commitID1),
                Commit.class);
        if (untrackedFiles().size() > 0) {
            System.err.printf("There is an untracked file in the "
                    + "way; delete it, or add and commit it first.");
            System.exit(0);
        }
        Commit comm2 = Utils.readObject(Utils.join(COMMIT_DIR, commitID2),
                Commit.class);
        Commit split = splitPoint(comm1, comm2, branchName);
        Staging current = new Staging(STAGING);
        if (current.getCurrentStage().size() > 0
                || current.getRemovalStage().size() > 0) {
            System.err.printf("You have uncommitted changes.");
            System.exit(0);
        }
        HashSet<String> fileNames = commitFiles(comm1, comm2, split);
        for (String i : fileNames) {
            String head = comm1.getFiles().get(i);
            String other = comm2.getFiles().get(i);
            String splitContent = split.getFiles().get(i);
            if (head == null) {
                head = "";
            }
            if (other == null) {
                other = "";
            }
            if (splitContent == null) {
                splitContent = "";
            }
            if (splitContent.equals(head) && !splitContent.equals(other)) {
                if (other.length() == 0) {
                    remove(i);
                } else {
                    Blob contents = Utils.readObject(Utils.join(BLOBS_DIR,
                            other), Blob.class);
                    Utils.writeContents(Utils.join(CWD, i), contents.getBlob());
                    add(i);
                }
            } else if (!splitContent.equals(head)
                    && !splitContent.equals(other)) {
                mergeFiles(head, other, i);
            }
        }
        commit("Merged " + branchName + " into " + curr.getCurrBranch()
                        + ".", commitID1.substring(0, 7) + " "
                + commitID2.substring(0, 7), comm2);
    }

    /** Returns whether the .gitlet folder exists or
     *  has been initialized. */
    public static boolean exists() {
        return GITLET_FOLDER.exists();
    }

    /** Returns the long commitID given a shortened SHORTID. */
    public static String commitIDFinder(String shortID) {
        for (String commitID : Utils.plainFilenamesIn(COMMIT_DIR)) {
            if (commitID.startsWith(shortID)) {
                return commitID;
            }
        }
        System.err.printf("No commit with that id exists.");
        System.exit(0);
        return "";
    }

    /** Returns a lists the untracked files in the CWD. */
    public static LinkedList<String> untrackedFiles() {
        List<String> currentFiles = Utils.plainFilenamesIn(CWD);
        Branch curr = Utils.readObject(BRANCHES, Branch.class);
        String prevSha = curr.getCurrCommitID();
        Commit prev = Utils.readObject(Utils.join(COMMIT_DIR, prevSha),
                Commit.class);
        Staging current = new Staging(STAGING);

        LinkedList<String> unstaged = new LinkedList<>();
        for (String i : currentFiles) {
            if ((!current.getCurrentStage().containsKey(i)
                    && !prev.getFiles().containsKey(i))
                    || current.getRemovalStage().contains(i)) {
                unstaged.add(i);
            }
        }
        return unstaged;
    }

    /** Given a COMM instance and a FILENAME, overwrites the current
     *  file in the CWD with the file from the commit instance. */
    public static void overwriteFile(Commit comm, String fileName) {
        String blobFile = comm.getFiles().get(fileName);
        if (blobFile == null) {
            System.err.printf("File does not exist in that commit.");
            System.exit(0);
        }
        Blob file = Utils.readObject(Utils.join(BLOBS_DIR, blobFile),
                Blob.class);
        Utils.writeContents(Utils.join(CWD, fileName), file.getBlob());
    }

    /** Returns the latest commit between two different
     *  branches, COMM1 and COMM2. Also given the other
     *  branch, GIVENBRANCH, in order to fast-forward
     *  if necessary. */
    public static Commit splitPoint(Commit comm1, Commit comm2,
                                    String givenBranch) {
        Commit split = null;
        String commitid = null;
        LinkedList<String> commits1 = comm1.getCommits();
        LinkedList<String> commits2 = comm2.getCommits();
        commits1.addFirst(Utils.sha1(Utils.serialize(comm1)));
        commits2.addFirst(Utils.sha1(Utils.serialize(comm2)));
        for (String i : commits1) {
            if (commits2.contains(i)) {
                split = Utils.readObject(Utils.join(COMMIT_DIR, i),
                        Commit.class);
                commitid = i;
                break;
            }
        }
        if (split == null || commits2.get(0).equals(commitid)) {
            System.err.printf("Given branch is an ancestor of "
                    + "the current branch.");
            System.exit(0);
        }
        if (commits1.get(0).equals(commitid)) {
            checkoutBranch(givenBranch);
            System.err.printf("Current branch fast-forwarded.");
            System.exit(0);
        }
        return split;
    }

    /** Returns a unique HashSet of files from an arbitrary
     *  amount of COMMITS. */
    public static HashSet<String> commitFiles(Commit... commits) {
        HashSet<String> files = new HashSet<>();
        for (Commit i : commits) {
            files.addAll(i.getFiles().keySet());
        }
        return files;
    }

    /** A helper function that takes in a COMMITID of a COMM
     *  and prints the correct log of that commit. Handles
     *  both regular commits and merge commits. */
    public static void logHelper(String commitID, Commit comm) {
        if (comm.getMergeText() == null) {
            System.out.println("===" + "\n" + "commit " + commitID
                    + "\n" + "Date: " + comm.getTimestamp()
                    + "\n" + comm.getMessage() + "\n");
        } else {
            System.out.println("===" + "\n" + "commit " + commitID
                    + "\n" + "Merge: " + comm.getMergeText()
                    + "\n" + "Date: " + comm.getTimestamp()
                    + "\n" + comm.getMessage() + "\n");
        }
    }

    /** Takes in a NEWCOMM and an optional list of UNTRACKED
     *  to determine if a file in the given CWD is untracked. */
    public static void untrackCheck(Commit newComm,
                                    LinkedList<String> untracked) {
        if (untracked == null) {
            untracked = untrackedFiles();
        }
        if (untracked.size() > 0) {
            for (String i : untracked) {
                if (newComm.getFiles().get(i) != null) {
                    System.err.printf("There is an untracked file in the "
                            + "way; delete it, or add and commit it first.");
                    System.exit(0);
                }
            }
        }
    }

    /** Takes in a HEAD, OTHER, and FILENAME in order to
     *  merge two different files from two different
     *  branches with the same file name. */
    public static void mergeFiles(String head, String other, String fileName) {
        String newFile = "<<<<<<< HEAD\n";
        if (head.length() != 0) {
            newFile += Utils.readObject(Utils.join(BLOBS_DIR, head),
                    Blob.class).getBlob();
        }
        newFile += "=======\n";
        if (other.length() != 0) {
            newFile += Utils.readObject(Utils.join(BLOBS_DIR, other),
                    Blob.class).getBlob();
        }
        newFile += ">>>>>>>\n";
        Utils.writeContents(Utils.join(CWD, fileName), newFile);
        add(fileName);
        System.out.println("Encountered a merge conflict.");
    }
}