diff --git a/GitTester.java b/GitTester.java new file mode 100644 index 0000000..4812ec3 --- /dev/null +++ b/GitTester.java @@ -0,0 +1,27 @@ +import java.io.IOException; + +public class GitTester { + + public static void main(String args[]) throws IOException { + + /* Your tester code goes here */ + GitWrapper gw = new GitWrapper(); + gw.init(); + + gw.add("testFolder/another/here.txt"); + + String hashOfOldCommit = gw.commit("miles", "1 commit"); + + gw.add("testFolder/one_more.txt"); + gw.add("testFolder/another.txt"); + gw.add("testFolder/shouldbegone.txt"); + + String hashOfNewCommit = gw.commit("miles", "2 commit"); + + gw.checkout(hashOfOldCommit); + gw.checkout(hashOfNewCommit); + + // in the end, nothing should change + // we checked out an older commit and then came back to the current one + } +} \ No newline at end of file diff --git a/GitWrapper.java b/GitWrapper.java new file mode 100644 index 0000000..64aa193 --- /dev/null +++ b/GitWrapper.java @@ -0,0 +1,100 @@ +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +public class GitWrapper { + + private gitRepository repo; + + /** + * Initializes a new Git repository. + * This method creates the necessary directory structure + * and initial files (index, HEAD) required for a Git repository. + * + * @throws IOException + */ + public void init() throws IOException { + repo = new gitRepository(false); + }; + + /** + * Stages a file for the next commit. + * This method adds a file to the index file. + * If the file does not exist, it throws an IOException. + * If the file is a directory, it throws an IOException. + * If the file is already in the index, it does nothing. + * If the file is successfully staged, it creates a blob for the file. + * + * @param filePath The path to the file to be staged. + * @throws IOException + */ + public void add(String filePath) throws IOException { + File fileToAdd = new File(filePath); + + if (!fileToAdd.exists()) { + throw new IOException("add: file doesn't exist"); + } + + if (fileToAdd.isDirectory()) { + throw new IOException("add: file is a directory"); + } + + repo.addFile(fileToAdd.getPath()); + }; + + /** + * Creates a commit with the given author and message. + * It should capture the current state of the repository by building trees based + * on the index file, + * writing the tree to the objects directory, + * writing the commit to the objects directory, + * updating the HEAD file, + * and returning the commit hash. + * + * The commit should be formatted as follows: + * tree: + * parent: + * author: + * date: + * summary: + * + * @param author The name of the author making the commit. + * @param message The commit message describing the changes. + * @return The SHA1 hash of the new commit. + * @throws IOException + */ + public String commit(String author, String message) throws IOException { + return repo.commit(author, message); + }; + + /** + * EXTRA CREDIT: + * Checks out a specific commit given its hash. + * This method should read the HEAD file to determine the "checked out" commit. + * Then it should update the working directory to match the + * state of the repository at that commit by tracing through the root tree and + * all its children. + * + * @param commitHash The SHA1 hash of the commit to check out. + * @throws IOException + */ + public void checkout(String commitHash) throws IOException { + + if (!repo.doesCommitHashExist(commitHash)) { + throw new IllegalArgumentException("commitHash doesn't exist"); + } + + // deletes tracked files + repo.deleteTrackedFilesFromCurrentCommit(); + + // generates old files + repo.regenerateTrackedFilesFromCommit(commitHash); + + // rewrites head + File head = new File("git/HEAD"); + head.delete(); + head.createNewFile(); + Files.write(head.toPath(), commitHash.getBytes()); + + }; +} \ No newline at end of file diff --git a/README.md b/README.md index b26171d..0606789 100644 --- a/README.md +++ b/README.md @@ -61,4 +61,21 @@ __GP-3.1.0__ | addTreeRecursive() 4. Replaces all grouped entries in the working list with a single reference to the newly created tree object (using its hash and directory name). 5. Continues this process recursively, ascending directory levels, until the working list is collapsed to one or more entries at the root level. 6. If multiple root entries remain, combines and writes a final root tree object, then prints and returns its hash as the reference to the entire directory tree. -7. The writeTreeObj method takes a list of entries, extracts relevant parts, formats the tree content, hashes it, writes it to the "git/objects/" folder, and returns the hash. \ No newline at end of file +7. The writeTreeObj method takes a list of entries, extracts relevant parts, formats the tree content, hashes it, writes it to the "git/objects/" folder, and returns the hash. + +__GP-4.2__ | commit() +1. Writes a commit file to the objects folder by asking the user for their name and commit message, generating the date, generating the commit hash, and pulls the previous commit hash from the HEAD, if it exists +2. Writes the current commit hash to the HEAD + +__GP-4.3__ | wrapped commit(), add(), and init() +1. init() makes a repository +2. add() wraps index and BLOB code and now correctly handles adding a file already in the index +3. wrapped commit() just delegates to GP-4.2 commit() + +__GP-4.4__ | extra credit -- checkout() +1. getRootHashFromCommitHash() retrieves the root tree hash from a commitHash +2. deleteTrackedFilesFromCurrentCommit() wraps deleteRootRecursive() which deletes all files tracked from the root +3. regenerateTrackedFilesFromCommit() wraps regenRootRecursive() which regenerates all files tracked from the root +4. doesCommitHashExist() checks to see if a commitHash exists +5. getParentCommit() grabs the parent commit; null if none exists +-> Fixed an edge case where if a singular nested blob was being committed, the enclosing folders wouldn't be committed \ No newline at end of file diff --git a/gitRepository.java b/gitRepository.java index 960c473..0817ab8 100644 --- a/gitRepository.java +++ b/gitRepository.java @@ -1,9 +1,12 @@ import java.io.*; import java.math.BigInteger; +import java.nio.file.Files; +import java.nio.file.Paths; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.zip.GZIPOutputStream; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; public class gitRepository { @@ -12,11 +15,21 @@ public class gitRepository { private File OBJECTS = new File("git/objects"); private File INDEX = new File("git/index"); private File HEAD = new File("git/HEAD"); + private String rootHash; private boolean compress; - public gitRepository(boolean compress) { + public gitRepository(boolean compress) throws IOException { System.out.println(attemptCreatingGitRepository()); this.compress = compress; + if (!Files.readString(HEAD.toPath()).isEmpty()) { + this.rootHash = getRootHashFromCommitHash(Files.readString(HEAD.toPath())); + } + } + + public void addFile(String filename) throws IOException { + if (index(filename)) { + BLOB(filename); + } } public String attemptCreatingGitRepository() { @@ -52,8 +65,7 @@ public String createSha1Hash(String inputData) { while (hashtext.length() < 40) hashtext = "0" + hashtext; return hashtext; - } - catch (NoSuchAlgorithmException e) { + } catch (NoSuchAlgorithmException e) { throw new RuntimeException(); } } @@ -73,7 +85,7 @@ public String getFileContents(String fileName) { return compressContents(data.toString()); } if (!data.isEmpty()) - return data.substring(0, data.length() - 1); + return data.substring(0, data.length() - 1); return data.toString(); } @@ -87,25 +99,35 @@ public void BLOB(String fileName) { } } - public void index(String fileName) { + public boolean index(String fileName) throws IOException { + + File file = new File(fileName); + String fileContents = getFileContents(fileName); + String fileHash = createSha1Hash(fileContents); + String indexContents = Files.readString(INDEX.toPath()); + + // if the file has already been included in the index in that state + if (indexContents.contains(fileName) && indexContents.contains(fileHash)) { + return false; + } + StringBuilder fileIndex = new StringBuilder(); if (INDEX.length() > 0) fileIndex.append("\n"); String fileType; - File file = new File(fileName); if (file.isDirectory()) { fileType = "tree"; } else { fileType = "blob"; } - String fileContents = getFileContents(fileName); - String fileHash = createSha1Hash(fileContents); fileIndex.append(fileType + " " + fileHash + " " + fileName); try (BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(INDEX, true))) { bufferedWriter.write(fileIndex.toString()); } catch (IOException e) { e.printStackTrace(); } + + return true; } public String seeLastIndexEntry() { @@ -121,18 +143,18 @@ public String seeLastIndexEntry() { return lastLine; } - public static String compressContents(String contents) { + public static String compressContents(String contents) { if (contents != null && contents.length() != 0) { ByteArrayOutputStream out = new ByteArrayOutputStream(); - GZIPOutputStream gzip; - try { - gzip = new GZIPOutputStream(out); - gzip.write(contents.getBytes()); - gzip.close(); - return out.toString("ISO-8859-1"); - } catch (IOException e) { - e.printStackTrace(); - } + GZIPOutputStream gzip; + try { + gzip = new GZIPOutputStream(out); + gzip.write(contents.getBytes()); + gzip.close(); + return out.toString("ISO-8859-1"); + } catch (IOException e) { + e.printStackTrace(); + } } return contents; } @@ -229,13 +251,54 @@ public ArrayList addTreeRecursive() { } workingList = newWorkingList; } + + // to fix the edge case of the nested blob + if (workingList.size() == 1 && workingList.get(0).contains("blob ")) { + String entry = workingList.get(0); + + // thank u for the godsend getDepth method whoever wrote it + while (getDepth(entry) > 1) { + + String dir = getDirName(entry); + ArrayList temp = new ArrayList<>(); + temp.add(entry); + String treeHash = writeTreeObj(temp); + entry = "tree " + treeHash + " " + dir; + + } + + workingList.clear(); + workingList.add(entry); + } + if (workingList.size() > 1) { + // this code will never execute... String treeHash = writeTreeObj(workingList); workingList.clear(); workingList.add("tree " + treeHash); System.out.println("Root tree entry: tree " + treeHash); + + rootHash = treeHash; + } else if (!workingList.isEmpty()) { - System.out.println("Root tree entry: " + workingList.get(0)); + // in other words, if WL.size() == 1? + // bug: if one file is committed, then a blob will be here + + if (workingList.get(0).split(" ")[0].equals("blob")) { + // i think this is what hannah was trying to do + String treeHash = writeTreeObj(workingList); + workingList.clear(); + workingList.add("tree " + treeHash); + System.out.println("Root tree entry: tree " + treeHash); + } else { + String treeHash = writeTreeObj(workingList); + workingList.clear(); + workingList.add("tree " + treeHash); + System.out.println("Root tree entry: " + workingList.get(0)); + } + + rootHash = workingList.get(0).split(" ")[1]; + System.out.println(rootHash); } return workingList; } @@ -249,18 +312,168 @@ public String writeTreeObj(ArrayList entries) { String path = parts[2]; String name = path.substring(path.lastIndexOf("/") + 1); treeContent.append(type).append(" ").append(hash).append(" ").append(name); - if (i < entries.size() - 1) treeContent.append("\n"); + if (i < entries.size() - 1) + treeContent.append("\n"); } String treeString = treeContent.toString(); String treeHash = createSha1Hash(treeString); try (BufferedWriter bw = new BufferedWriter(new FileWriter("git/objects/" + treeHash))) { - bw.write(treeString); + bw.write(treeString); } catch (IOException e) { - e.printStackTrace(); + e.printStackTrace(); + } + return treeHash; + + } + + public String commit(String inputAuthor, String message) throws IOException { + addTreeRecursive(); + + String treeField = "tree: " + rootHash + "\n"; + String author = "author: " + inputAuthor + "\n"; + String summary = "summary: " + message; + String date = "date: " + java.time.LocalDateTime.now().toString() + "\n"; + + String parent; + // Efficient check to see if first commit or not + if (HEAD.length() == 0) { + parent = ""; + } else { + parent = "parent: " + Files.readAllLines(HEAD.toPath()).get(0) + "\n"; + } + + File tempCommitFile = new File("tempCommitFile"); + BufferedWriter bw = new BufferedWriter(new FileWriter(tempCommitFile, true)); + bw.write(treeField); + bw.write(parent); + bw.write(author); + bw.write(date); + bw.write(summary); + bw.close(); + + BLOB(tempCommitFile.getPath()); + String hash = createSha1Hash(Files.readString(tempCommitFile.toPath())); + tempCommitFile.delete(); + + BufferedWriter bw2 = new BufferedWriter(new FileWriter(HEAD.getPath())); + bw2.write(hash); + bw2.close(); + + return hash; + } + + private String getRootHashFromCommitHash(String commitHash) throws IOException { + ArrayList lines = new ArrayList(Files + .readAllLines(new File("git" + File.separator + "objects" + File.separator + commitHash).toPath())); + + return lines.get(0).split(": ")[1]; + } + + public void deleteTrackedFilesFromCurrentCommit() throws IOException { + deleteRootRecursive(rootHash, ""); + } + + /** + * Going fancy with this message because its an important method. + * This method recursively deletes everything from the top. + * + * @param treeHash The SHA1 hash of the tree to start deleting from + * @param path The path that the method is currently deleting from + */ + private void deleteRootRecursive(String treeHash, String path) throws IOException { + File tree = new File("git" + File.separator + "objects" + File.separator + treeHash); + ArrayList lines = new ArrayList(Files.readAllLines(tree.toPath())); + + for (String line : lines) { + String[] parsedLine = line.split(" "); + + if (parsedLine[0].equals("blob")) { + new File(path + parsedLine[2]).delete(); + } + + else { + // Must delete everything inside the directory first + // totally okay if not everything is deleted + deleteRootRecursive(parsedLine[1], path + parsedLine[2] + File.separator); + new File(path + parsedLine[2]).delete(); + } + } + } + + public void regenerateTrackedFilesFromCommit(String commitHash) throws IOException { + regenRootRecursive(getRootHashFromCommitHash(commitHash), ""); + } + + /** + * Going fancy with this message because its an important method. + * It's basically the inverse of deleting + * This method recursively deletes everything from the top. + * + * @param treeHash The SHA1 hash of the tree to start deleting from + * @param path The path that the method is currently deleting from + */ + private void regenRootRecursive(String treeHash, String path) throws IOException { + File tree = new File("git" + File.separator + "objects" + File.separator + treeHash); + ArrayList lines = new ArrayList(Files.readAllLines(tree.toPath())); + + for (String line : lines) { + String[] parsedLine = line.split(" "); + + if (parsedLine[0].equals("blob")) { + File output = new File(path + parsedLine[2]); + String hashedPathToContents = "git" + File.separator + "objects" + File.separator + parsedLine[1]; + output.createNewFile(); + + Files.copy(Paths.get(hashedPathToContents), new FileOutputStream(output)); + } + + else { + // Must delete everything inside the directory first + // totally okay if not everything is deleted + new File(path + parsedLine[2]).mkdir(); + regenRootRecursive(parsedLine[1], path + parsedLine[2] + File.separator); + } + } + } + + public boolean doesCommitHashExist(String commitHash) throws IOException { + String currentCommitHash = Files.readString(Paths.get("git" + File.separator + "HEAD")); + + if (commitHash.equals(currentCommitHash)) { + return true; } - return treeHash; + String parentCommit = getParentCommit(currentCommitHash); + while (parentCommit != null) { + if (parentCommit.equals(commitHash)) { + return true; + } + parentCommit = getParentCommit(parentCommit); + } + + /* + * At this point, we can check to see if its in the future? + */ + + ArrayList objectsList = new ArrayList(Arrays.asList(OBJECTS.listFiles())); + for (File file : objectsList) { + if (file.getName().equals(commitHash)) { + return true; + } + } + + return false; + } + + // returns null if no parent commit exists + private String getParentCommit(String commitHash) throws IOException { + File commit = new File("git" + File.separator + "objects" + File.separator + commitHash); + ArrayList lines = new ArrayList(Files.readAllLines(commit.toPath())); + if (lines.get(1).contains("parent")) { + return lines.get(1).split(": ")[1]; + } else { + return null; + } } - } \ No newline at end of file diff --git a/projectTester.java b/projectTester.java index 7c94e56..b159f21 100644 --- a/projectTester.java +++ b/projectTester.java @@ -1,7 +1,5 @@ import java.io.*; -import java.nio.file.*; import java.util.ArrayList; -import java.nio.charset.StandardCharsets; public class projectTester { @@ -11,7 +9,7 @@ public class projectTester { private static File HEAD = new File("git/HEAD"); private static String[] TEST_FILE_NAMES = {"test1.txt", "wow_another_test.txt", "testFolder/one_more.txt"}; - public static void main(String[] args) { + public static void main(String[] args) throws IOException { // testGenerateGitDirectory(); // testBLOB(); // testBLOBAndIndex(); @@ -36,7 +34,7 @@ public static void generateTestFiles() { } } - public static void testBLOBAndIndex() { + public static void testBLOBAndIndex() throws IOException { System.out.println("\nTESTING INDEXING OF BLOBS GENERATED FROM FILES"); generateTestFiles(); @@ -58,7 +56,7 @@ public static void testBLOBAndIndex() { } } - public static void testBLOB() { + public static void testBLOB() throws IOException { System.out.println("\nTESTING BLOB GENERATION FROM FILES"); gitRepository testRepo = resetRepo(false); @@ -85,7 +83,7 @@ public static void testBLOB() { } } - public static void testGenerateGitDirectory() { + public static void testGenerateGitDirectory() throws IOException { System.out.println("\nTESTING GIT DIRECTORY (AND OTHER ACCOMPANYING FILES) GENERATION"); @@ -103,7 +101,7 @@ public static void testGenerateGitDirectory() { } } - public static gitRepository resetRepo(boolean compression) { + public static gitRepository resetRepo(boolean compression) throws IOException { if (gitDIR.exists()) deleteDirectoryRecursive(gitDIR); gitRepository newRepo = new gitRepository(compression); @@ -120,7 +118,7 @@ public static void deleteDirectoryRecursive(File file) } } - public static void testTreeGeneration() { + public static void testTreeGeneration() throws IOException { System.out.println("testing tree generation"); resetRepo(false); generateTestFiles(); diff --git a/test1.txt b/test1.txt new file mode 100644 index 0000000..e3a2049 --- /dev/null +++ b/test1.txt @@ -0,0 +1,2 @@ +wow what a great test +#7534313._. \ No newline at end of file diff --git a/testFolder/another.txt b/testFolder/another.txt new file mode 100644 index 0000000..de5a353 --- /dev/null +++ b/testFolder/another.txt @@ -0,0 +1 @@ +another \ No newline at end of file diff --git a/testFolder/another/here.txt b/testFolder/another/here.txt new file mode 100644 index 0000000..ee9808d --- /dev/null +++ b/testFolder/another/here.txt @@ -0,0 +1 @@ +aaaaaa \ No newline at end of file diff --git a/testFolder/one_more.txt b/testFolder/one_more.txt new file mode 100644 index 0000000..a412727 --- /dev/null +++ b/testFolder/one_more.txt @@ -0,0 +1,2 @@ +wow what a great test +#2739023._. \ No newline at end of file diff --git a/testFolder/shouldbegone.txt b/testFolder/shouldbegone.txt new file mode 100644 index 0000000..853308c --- /dev/null +++ b/testFolder/shouldbegone.txt @@ -0,0 +1 @@ +whyyyyyy me \ No newline at end of file diff --git a/wow_another_test.txt b/wow_another_test.txt new file mode 100644 index 0000000..c2ed4e2 --- /dev/null +++ b/wow_another_test.txt @@ -0,0 +1,2 @@ +wow what a great test +#4071378._. \ No newline at end of file