summaryrefslogtreecommitdiff
path: root/p1
diff options
context:
space:
mode:
authorpants <niklasjo2004@gmail.com>2026-09-21 14:16:34 -0700
committerpants <niklasjo2004@gmail.com>2026-09-21 14:16:34 -0700
commit81ba9eb66f8284d8760012973f84cdbe617ceb94 (patch)
tree466432644e9e3fd66ee183ec53a1f10abb1b7faa /p1
Diffstat (limited to 'p1')
-rw-r--r--p1/Client.java91
-rw-r--r--p1/ExampleTesting.java148
-rw-r--r--p1/Repository.java241
-rw-r--r--p1/Testing.java99
4 files changed, 579 insertions, 0 deletions
diff --git a/p1/Client.java b/p1/Client.java
new file mode 100644
index 0000000..510ed82
--- /dev/null
+++ b/p1/Client.java
@@ -0,0 +1,91 @@
+import java.util.*;
+
+// A program to work with Mini-Git. Manages the state of repositories and allows for all
+// operations defined in Mini-Git.
+public class Client {
+ private static List<String> ops = new ArrayList<>();
+
+ public static void main(String[] args) {
+ Collections.addAll(ops, "create", "head", "history", "commit", "drop",
+ "synchronize", "quit");
+ Scanner console = new Scanner(System.in);
+ Map<String, Repository> repos = new HashMap<>();
+ String op = "";
+ String name = "";
+
+ intro();
+
+ while (!op.equalsIgnoreCase("quit")) {
+ System.out.println("Available repositories: ");
+ for (Repository repo : repos.values()) {
+ System.out.println("\t" + repo);
+ }
+ System.out.println("Operations: " + ops);
+ System.out.print("Enter operation and repository: ");
+ String[] input = console.nextLine().split("\\s+");
+ op = input[0];
+ name = input.length > 1 ? input[1] : "";
+ while (!ops.contains(op) || (!op.equalsIgnoreCase("create") &&
+ !op.equalsIgnoreCase("quit") &&
+ !repos.containsKey(name))) {
+ System.out.println(" **ERROR**: Operation or repository not recognized.");
+ System.out.print("Enter operation and repository: ");
+ input = console.nextLine().split("\\s+");
+ op = input[0];
+ name = input.length > 1 ? input[1] : "";
+ }
+
+ Repository currRepo = repos.get(name);
+ op = op.toLowerCase();
+ if (op.equalsIgnoreCase("create")) {
+ if (currRepo != null) {
+ System.out.println(" **ERROR**: Repository with that name already exists.");
+ } else {
+ Repository newRepo = new Repository(name);
+ repos.put(name, newRepo);
+ System.out.println(" New repository created: " + newRepo);
+ }
+ } else if (op.equalsIgnoreCase("head")) {
+ System.out.println(currRepo.getRepoHead());
+ } else if (op.equalsIgnoreCase("history")) {
+ System.out.print("How many commits back? ");
+ int nHist = console.nextInt();
+ console.nextLine();
+ System.out.println(currRepo.getHistory(nHist));
+ } else if (op.equalsIgnoreCase("commit")) {
+ System.out.print("Enter commit message: ");
+ String message = console.nextLine();
+ System.out.println(" New commit: " + currRepo.commit(message));
+ } else if (op.equalsIgnoreCase("drop")) {
+ System.out.print("Enter ID to drop: ");
+ String idDrop = console.nextLine();
+ if (currRepo.drop(idDrop)) {
+ System.out.println(" Successfully dropped " + idDrop);
+ } else {
+ System.out.println(" No commit dropped!");
+ }
+ } else if (op.equalsIgnoreCase("synchronize")) {
+ System.out.print("Which repository would you like to " +
+ "synchronize into the given one? ");
+ String repo = console.nextLine();
+ if (repo.equals(name)) {
+ System.out.println("Cannot synchronize the same repositories!");
+ } else if (!repos.containsKey(repo)) {
+ System.out.println("Repository does not exist!");
+ } else {
+ currRepo.synchronize(repos.get(repo));
+ }
+ }
+ System.out.println();
+ }
+ }
+
+ // Prints out an introduction to the Mini-Git test client.
+ public static void intro() {
+ System.out.println("Welcome to the Mini-Git test client!");
+ System.out.println("Use this program to test your Mini-Git repository implemenation.");
+ System.out.println("Make sure to test all operations in all cases --");
+ System.out.println("some cases are particularly tricky.");
+ System.out.println();
+ }
+}
diff --git a/p1/ExampleTesting.java b/p1/ExampleTesting.java
new file mode 100644
index 0000000..2c00e3f
--- /dev/null
+++ b/p1/ExampleTesting.java
@@ -0,0 +1,148 @@
+import org.junit.jupiter.api.*;
+import static org.junit.jupiter.api.Assertions.*;
+import java.util.*;
+
+public class Testing {
+ private Repository repo1;
+ private Repository repo2;
+
+ // Occurs before each of the individual test cases
+ // (creates new repos and resets commit ids)
+ @BeforeEach
+ public void setUp() {
+ repo1 = new Repository("repo1");
+ repo2 = new Repository("repo2");
+ Repository.Commit.resetIds();
+ }
+
+ @Test
+ @DisplayName("EXAMPLE TEST - getHistory()")
+ public void getHistory() throws InterruptedException {
+ // Initialize commit messages
+ String[] commitMessages = new String[]{"Initial commit.",
+ "Updated method documentation.",
+ "Removed unnecessary object creation."};
+ commitAll(repo1, commitMessages);
+ testHistory(repo1, 1, commitMessages);
+ }
+
+ @Test
+ @DisplayName("EXAMPLE TEST - drop() (empty case)")
+ public void testDropEmpty() {
+ assertFalse(repo1.drop("123"));
+ }
+
+ @Test
+ @DisplayName("EXAMPLE TEST - drop() (front case)")
+ public void testDropFront() throws InterruptedException {
+ // Initialize commit messages
+ commitAll(repo1, new String[]{"First commit"}); // ID "0"
+ commitAll(repo2, new String[]{"Added unit tests."}); // ID "1"
+
+ // Assert that repo1 successfully dropped "0"
+ assertTrue(repo1.drop("0"));
+ assertEquals(repo1.getRepoSize(), 0);
+
+ // Assert that repo2 does not drop "0" but drops "1"
+ // (Note that the commit ID increments regardless of the repository!)
+ assertFalse(repo2.drop("0"));
+ assertTrue(repo2.drop("1"));
+ assertEquals(repo2.getRepoSize(), 0);
+ }
+
+ @Test
+ @DisplayName("EXAMPLE TEST - synchronize() (one: [1, 2], two: [3, 4])")
+ public void testSynchronizeOne() throws InterruptedException {
+ // Initialize commit messages
+ commitAll(repo1, new String[]{"One", "Two"});
+ commitAll(repo2, new String[]{"Three", "Four"});
+
+ // Make sure both repos got exactly 2 commits each
+ assertEquals(2, repo1.getRepoSize());
+ assertEquals(2, repo2.getRepoSize());
+
+ // Synchronize repo2 into repo1
+ repo1.synchronize(repo2);
+ assertEquals(4, repo1.getRepoSize());
+ assertEquals(0, repo2.getRepoSize());
+
+ // Make sure the history of repo1 is correctly synchronized
+ testHistory(repo1, 4, new String[]{"One", "Two", "Three", "Four"});
+ }
+
+ // Commits all of the provided messages into the provided repo, making sure timestamps
+ // are correctly sequential (no ties). If used, make sure to include
+ // 'throws InterruptedException'
+ // much like we do with 'throws FileNotFoundException'. Example useage:
+ //
+ // repo1:
+ // head -> null
+ // To commit the messages "one", "two", "three", "four"
+ // commitAll(repo1, new String[]{"one", "two", "three", "four"})
+ // This results in the following after picture
+ // repo1:
+ // head -> "four" -> "three" -> "two" -> "one" -> null
+ //
+ // YOU DO NOT NEED TO UNDERSTAND HOW THIS METHOD WORKS TO USE IT!
+ // (this is why documentation is important!)
+ public void commitAll(Repository repo, String[] messages) throws InterruptedException {
+ // Commit all of the provided messages
+ for (String message : messages) {
+ int size = repo.getRepoSize();
+ repo.commit(message);
+
+ // Make sure exactly one commit was added to the repo
+ assertEquals(size + 1, repo.getRepoSize(),
+ String.format("Size not correctly updated after commiting message [%s]",
+ message));
+
+ // Sleep to guarantee that all commits have different time stamps
+ Thread.sleep(2);
+ }
+ }
+
+ // Makes sure the given repositories history is correct up to 'n' commits, checking against
+ // all commits made in order. Example useage:
+ //
+ // repo1:
+ // head -> "four" -> "three" -> "two" -> "one" -> null
+ // (Commits made in the order ["one", "two", "three", "four"])
+ // To test the getHistory() method up to n=3 commits this can be done with:
+ // testHistory(repo1, 3, new String[]{"one", "two", "three", "four"})
+ // Similarly, to test getHistory() up to n=4 commits you'd use:
+ // testHistory(repo1, 4, new String[]{"one", "two", "three", "four"})
+ //
+ // YOU DO NOT NEED TO UNDERSTAND HOW THIS METHOD WORKS TO USE IT!
+ // (this is why documentation is important!)
+ public void testHistory(Repository repo, int n, String[] allCommits) {
+ int totalCommits = repo.getRepoSize();
+ assertTrue(n <= totalCommits,
+ String.format("Provided n [%d] too big. Only [%d] commits",
+ n, totalCommits));
+
+ String[] nCommits = repo.getHistory(n).split("\n");
+
+ assertTrue(nCommits.length <= n,
+ String.format("getHistory(n) returned more than n [%d] commits", n));
+ assertTrue(nCommits.length <= allCommits.length,
+ String.format("Not enough expected commits to check against. " +
+ "Expected at least [%d]. Actual [%d]",
+ n, allCommits.length));
+
+ for (int i = 0; i < n; i++) {
+ String commit = nCommits[i];
+
+ // Old commit messages/ids are on the left and the more recent commit messages/ids are
+ // on the right so need to traverse from right to left
+ int backwardsIndex = totalCommits - 1 - i;
+ String commitMessage = allCommits[backwardsIndex];
+
+ assertTrue(commit.contains(commitMessage),
+ String.format("Commit [%s] doesn't contain expected message [%s]",
+ commit, commitMessage));
+ assertTrue(commit.contains("" + backwardsIndex),
+ String.format("Commit [%s] doesn't contain expected id [%d]",
+ commit, backwardsIndex));
+ }
+ }
+}
diff --git a/p1/Repository.java b/p1/Repository.java
new file mode 100644
index 0000000..dff76e7
--- /dev/null
+++ b/p1/Repository.java
@@ -0,0 +1,241 @@
+import java.util.*;
+import java.text.SimpleDateFormat;
+
+public class Repository {
+
+ private String name;
+ private Commit head;
+ private int size;
+
+ public Repository(String name) {
+ if (name == null || name.isEmpty()) {
+ throw new IllegalArgumentException("Repository needs a name!");
+ }
+
+ this.name = name;
+ }
+
+ public String getRepoHead() {
+ if (head == null) {
+ return null;
+ }
+ return head.id;
+ }
+
+ public int getRepoSize() {
+ Commit temp = head;
+ int size = 0;
+ while (temp != null) {
+ size++;
+ temp = temp.past;
+ }
+ this.size = size;
+ return size;
+ }
+
+ public String toString() {
+ if (head == null) {
+ return name + " - No commits";
+ }
+ return name + " - Current head: " + head.toString();
+ }
+
+ public boolean contains(String targetId) {
+ Commit temp = head;
+
+ while (temp != null) {
+ if (temp.id.equals(targetId)) {
+ return true;
+ }
+ temp = temp.past;
+ }
+
+ return false;
+ }
+
+ public String getHistory(int n) {
+ if (n <= 0) {
+ throw new IllegalArgumentException("do you want the history or not?");
+ }
+
+ Commit temp = head;
+ String history = "";
+ if (n > size) {
+ while (temp != null) {
+ history += temp.toString() + "\n";
+ temp = temp.past;
+ }
+ }
+
+ for (int i = 0; i < n && temp != null; i++) {
+ history += temp.toString() + "\n";
+ temp = temp.past;
+ }
+
+ return history;
+ }
+
+ public String commit(String message) {
+ if (head == null) {
+ head = new Commit(message);
+ return head.id;
+ }
+ Commit old = head;
+ head = new Commit(message, old);
+ size++;
+ return head.id;
+ }
+
+ public boolean drop(String targetId) {
+ // list is empty
+ if (head == null) {
+ return false;
+ }
+
+ size--;
+ Commit temp = head;
+
+ // front
+ if (temp.id.equals(targetId)) {
+ head = temp.past;
+ return true;
+ }
+
+ // middle
+ while (temp.past != null && temp.past.past != null) {
+ if (temp.past.id == targetId) {
+ temp.past = temp.past.past;
+ return true;
+ }
+ temp = temp.past;
+ }
+
+ // end
+ if (temp.past != null && temp.past.id.equals(targetId)) {
+ temp.past = null;
+ return true;
+ }
+
+ // if we get here, targetId was not found, return size to initial state
+ size++;
+ return false;
+ }
+
+ public void synchronize(Repository other) {
+ if (head == null && other.head == null) {
+ head = null;
+ } else if (head == null) {
+ head = other.head;
+ other.head = null;
+ } else if (other.head == null) {
+ other.head = null;
+ } else {
+ List<Commit> commitList = new ArrayList<>();
+ Comparator commitComparator = new Comparator<Commit>() {
+ public int compare(Commit one, Commit two) {
+ return Long.compare(one.timeStamp, two.timeStamp);
+ }
+ };
+ Commit temp = head;
+ while (temp != null) {
+ commitList.add(temp);
+ temp = temp.past;
+ }
+ temp = other.head;
+ while (temp != null) {
+ commitList.add(temp);
+ temp = temp.past;
+ }
+ commitList.sort(commitComparator);
+ Commit prev = null;
+ for (Commit commit : commitList) {
+ commit.past = prev;
+ prev = commit;
+ }
+ head = commitList.get(commitList.size() - 1);
+ other.head = null;
+ }
+ }
+ /**
+ * DO NOT MODIFY
+ * A class that represents a single commit in the repository.
+ * Commits are characterized by an identifier, a commit message,
+ * and the time that the commit was made. A commit also stores
+ * a reference to the immediately previous commit if it exists.
+ *
+ * Staff Note: You may notice that the comments in this
+ * class openly mention the fields of the class. This is fine
+ * because the fields of the Commit class are public. In general,
+ * be careful about revealing implementation details!
+ */
+ public class Commit {
+
+ private static int currentCommitID;
+
+ /**
+ * The time, in milliseconds, at which this commit was created.
+ */
+ public final long timeStamp;
+
+ /**
+ * A unique identifier for this commit.
+ */
+ public final String id;
+
+ /**
+ * A message describing the changes made in this commit.
+ */
+ public final String message;
+
+ /**
+ * A reference to the previous commit, if it exists. Otherwise, null.
+ */
+ public Commit past;
+
+ /**
+ * Constructs a commit object. The unique identifier and timestamp
+ * are automatically generated.
+ * @param message A message describing the changes made in this commit.
+ * @param past A reference to the commit made immediately before this
+ * commit.
+ */
+ public Commit(String message, Commit past) {
+ this.id = "" + currentCommitID++;
+ this.message = message;
+ this.timeStamp = System.currentTimeMillis();
+ this.past = past;
+ }
+
+ /**
+ * Constructs a commit object with no previous commit. The unique
+ * identifier and timestamp are automatically generated.
+ * @param message A message describing the changes made in this commit.
+ */
+ public Commit(String message) {
+ this(message, null);
+ }
+
+ /**
+ * Returns a string representation of this commit. The string
+ * representation consists of this commit's unique identifier,
+ * timestamp, and message, in the following form:
+ * "[identifier] at [timestamp]: [message]"
+ * @return The string representation of this collection.
+ */
+ @Override
+ public String toString() {
+ SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd 'at' HH:mm:ss z");
+ Date date = new Date(timeStamp);
+
+ return id + " at " + formatter.format(date) + ": " + message;
+ }
+
+ /**
+ * Resets the IDs of the commit nodes such that they reset to 0.
+ * Primarily for testing purposes.
+ */
+ public static void resetIds() {
+ Commit.currentCommitID = 0;
+ }
+ }
+}
diff --git a/p1/Testing.java b/p1/Testing.java
new file mode 100644
index 0000000..2f7ae88
--- /dev/null
+++ b/p1/Testing.java
@@ -0,0 +1,99 @@
+import org.junit.jupiter.api.*;
+import static org.junit.jupiter.api.Assertions.*;
+import java.util.*;
+
+public class Testing {
+ private Repository repo1;
+ private Repository repo2;
+
+ // Occurs before each of the individual test cases
+ // (creates new repos and resets commit ids)
+ @BeforeEach
+ public void setUp() {
+ repo1 = new Repository("repo1");
+ repo2 = new Repository("repo2");
+ Repository.Commit.resetIds();
+ }
+
+ // TODO: Write your tests here!
+
+ /////////////////////////////////////////////////////////////////////////////////
+ // PROVIDED HELPER METHODS (You don't have to use these if you don't want to!) //
+ /////////////////////////////////////////////////////////////////////////////////
+
+ // Commits all of the provided messages into the provided repo, making sure timestamps
+ // are correctly sequential (no ties). If used, make sure to include
+ // 'throws InterruptedException'
+ // much like we do with 'throws FileNotFoundException'. Example useage:
+ //
+ // repo1:
+ // head -> null
+ // To commit the messages "one", "two", "three", "four"
+ // commitAll(repo1, new String[]{"one", "two", "three", "four"})
+ // This results in the following after picture
+ // repo1:
+ // head -> "four" -> "three" -> "two" -> "one" -> null
+ //
+ // YOU DO NOT NEED TO UNDERSTAND HOW THIS METHOD WORKS TO USE IT! (this is why documentation
+ // is important!)
+ public void commitAll(Repository repo, String[] messages) throws InterruptedException {
+ // Commit all of the provided messages
+ for (String message : messages) {
+ int size = repo.getRepoSize();
+ repo.commit(message);
+
+ // Make sure exactly one commit was added to the repo
+ assertEquals(size + 1, repo.getRepoSize(),
+ String.format("Size not correctly updated after commiting message [%s]",
+ message));
+
+ // Sleep to guarantee that all commits have different time stamps
+ Thread.sleep(2);
+ }
+ }
+
+ // Makes sure the given repositories history is correct up to 'n' commits, checking against
+ // all commits made in order. Example useage:
+ //
+ // repo1:
+ // head -> "four" -> "three" -> "two" -> "one" -> null
+ // (Commits made in the order ["one", "two", "three", "four"])
+ // To test the getHistory() method up to n=3 commits this can be done with:
+ // testHistory(repo1, 3, new String[]{"one", "two", "three", "four"})
+ // Similarly, to test getHistory() up to n=4 commits you'd use:
+ // testHistory(repo1, 4, new String[]{"one", "two", "three", "four"})
+ //
+ // YOU DO NOT NEED TO UNDERSTAND HOW THIS METHOD WORKS TO USE IT! (this is why documentation
+ // is important!)
+ public void testHistory(Repository repo, int n, String[] allCommits) {
+ int totalCommits = repo.getRepoSize();
+ assertTrue(n <= totalCommits,
+ String.format("Provided n [%d] too big. Only [%d] commits",
+ n, totalCommits));
+
+ String[] nCommits = repo.getHistory(n).split("\n");
+
+ assertTrue(nCommits.length <= n,
+ String.format("getHistory(n) returned more than n [%d] commits", n));
+ assertTrue(nCommits.length <= allCommits.length,
+ String.format("Not enough expected commits to check against. " +
+ "Expected at least [%d]. Actual [%d]",
+ n, allCommits.length));
+
+ for (int i = 0; i < n; i++) {
+ String commit = nCommits[i];
+
+ // Old commit messages/ids are on the left and the more recent commit messages/ids are
+ // on the right so need to traverse from right to left
+ int backwardsIndex = totalCommits - 1 - i;
+ String commitMessage = allCommits[backwardsIndex];
+
+ assertTrue(commit.contains(commitMessage),
+ String.format("Commit [%s] doesn't contain expected message [%s]",
+ commit, commitMessage));
+ assertTrue(commit.contains("" + backwardsIndex),
+ String.format("Commit [%s] doesn't contain expected id [%d]",
+ commit, backwardsIndex));
+ }
+ }
+}