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 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 untracked = untrackedFiles(); LinkedHashMap 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 entry : commCurr.getFiles().entrySet()) { overwriteFile(commCurr, entry.getKey()); commPrev.remove(entry.getKey()); } for (Map.Entry 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 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 entry : commCurr.getFiles().entrySet()) { overwriteFile(commCurr, entry.getKey()); commPrev.remove(entry.getKey()); } for (Map.Entry 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 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 untrackedFiles() { List 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 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 commits1 = comm1.getCommits(); LinkedList 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 commitFiles(Commit... commits) { HashSet 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 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."); } }