diff --git a/Git.java b/Git.java index effb278..82d7e82 100644 --- a/Git.java +++ b/Git.java @@ -6,47 +6,290 @@ import static java.nio.file.StandardCopyOption.*; import java.nio.file.Path; import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; -import java.nio.file.StandardOpenOption; import java.security.DigestException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.zip.*; +import java.util.Calendar; + +public class Git implements GitInterface { + private String currentDate; + private String headHash; + private String treeHash; + private String hashOfCommit; + + public void stage(String filePath) { + try { + if (filePath.contains("/")) { + int charIndex = filePath.indexOf("/"); + filePath = filePath.substring(0, charIndex); + } + Git.createBlob(Paths.get(filePath), false); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public String commit(String author, String message) { + currentDate = getDate(); + findHeadHash(); + if (!headHash.equals("")) { + // add extra files from head into index + writeToIndex(); + } + getTreeContents(); + StringBuffer toCommit = new StringBuffer(""); + toCommit.append("tree: " + treeHash); + toCommit.append("\nparent: " + headHash); + toCommit.append("\nauthor: " + author); + toCommit.append("\ndate: " + currentDate); + toCommit.append("\nmessage: " + message); + String commitContent = toCommit.toString(); + try { + // create a file to put the commit in + // make the commit file + // get that hash and put it in head + + File tempCommitFile = File.createTempFile("contentToHash", null); + FileWriter writer = new FileWriter(tempCommitFile); + writer.write(commitContent); + writer.close(); + hashOfCommit = Git.sha1(tempCommitFile.toPath()); + writer = new FileWriter(new File("./git/HEAD")); + writer.write(hashOfCommit); + writer.close(); + writer = new FileWriter(new File("./git/objects/" + hashOfCommit)); + writer.write(commitContent); + writer.close(); + // creates tree snapshot of the tree and puts in objects + BufferedReader reader = new BufferedReader(new FileReader("./git/index")); + StringBuffer sb = new StringBuffer(); + while (reader.ready()) { + sb.append((char) reader.read()); + } + reader.close(); + writer = new FileWriter(new File("./git/objects/" + treeHash)); + writer.write(sb.toString()); + writer.close(); + // overwrites index once commit is created + writer = new FileWriter(new File("./git/index")); + writer.write(""); + writer.close(); + } catch (Exception e) { + e.printStackTrace(); + } + return hashOfCommit; + } + + public void checkout(String commitHash) { + hashOfCommit = commitHash; + try { + // go through the current tree and delete everything + BufferedReader reader = new BufferedReader(new FileReader("./git/objects/" + treeHash)); + while (reader.ready()) { + Files.deleteIfExists(Paths.get(reader.readLine().substring(46))); + } + reader.close(); + // add everything from the previous tree back in + reader = new BufferedReader(new FileReader("./git/objects/" + commitHash)); + treeHash = reader.readLine().substring(6); + reader.close(); + reader = new BufferedReader(new FileReader("./git/objects/" + treeHash)); + while (reader.ready()) { + String line = reader.readLine(); + String fileName = line.substring(46); + File file = new File(fileName); + if (file.isDirectory()) { + file.mkdirs(); + } else { + if (fileName.contains("/")) { + int charIndex = fileName.lastIndexOf("/"); + File newDir = new File(fileName.substring(0, charIndex)); + newDir.mkdirs(); + } + file.createNewFile(); + BufferedWriter writer = new BufferedWriter(new FileWriter(file)); + String fileHash = line.substring(5,46); + writer.write(getHashFileContent(fileHash)); + writer.close(); + } + } + reader.close(); + + // put the correct hash in head + BufferedWriter writer = new BufferedWriter(new FileWriter ("./git/HEAD")); + writer.write(commitHash); + writer.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public String getHashFileContent(String hash) { + StringBuffer sb = new StringBuffer(); + try { + BufferedReader reader2 = new BufferedReader(new FileReader("./git/objects/" + hash)); + while (reader2.ready()) { + sb.append((char) reader2.read()); + } + reader2.close(); + } catch (Exception e) { + e.printStackTrace(); + } + return sb.toString(); + } + + public void findHeadHash() { + StringBuffer headHashReader = new StringBuffer(""); + try { + BufferedReader reader = new BufferedReader(new FileReader(new File("./git/HEAD"))); + while (reader.ready()) { + headHashReader.append(((char) reader.read())); + } + reader.close(); + headHash = headHashReader.toString(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void writeToIndex() { + // part 1 finds the hash of the previous tree + // currently we have the hash of the commit + String oldTreeHash = ""; + try { + // this finds the commit + BufferedReader reader = new BufferedReader(new FileReader("./git/objects/" + headHash)); + // read just after the first 6 characters of the tree: + // ("tree: " + hash of tree) + // we just want the hash of the tree + oldTreeHash = reader.readLine(); + oldTreeHash = oldTreeHash.substring(6); + reader.close(); + + if (oldTreeHash.equals("")) { + return; + } + + // part 2 actually writes to the file + reader = new BufferedReader(new FileReader("./git/objects/" + oldTreeHash)); + BufferedWriter writer = new BufferedWriter(new FileWriter("./git/index", true)); + while (reader.ready()) { // read index list of previous tree + String blobLine = reader.readLine(); + if (blobLine.equals("")) { + break; + } + String nextBlob = blobLine.substring(46); + if (!fileInIndex(nextBlob)) { + writer.write(blobLine + "\n"); + } + // if (!nextBlob.contains("/")) { // really jank technical way of doing this but + // it works! + // Git.createBlob(Paths.get(nextBlob), false); + // } + // add all files not already listed in index to index + } + reader.close(); + writer.close(); + } catch (Exception e) { + e.printStackTrace(); + } + + } + + public boolean fileInIndex(String fileName) { + int counter = 0; + try { + BufferedReader reader = new BufferedReader(new FileReader("./git/index")); + while (reader.ready()) { + if (reader.readLine().substring(46).equals(fileName)) { + counter++; + } + } + reader.close(); + } catch (Exception e) { + e.printStackTrace(); + } + if (counter > 0) { + return true; + } else { + return false; + } + } + + public void getTreeContents() { + // StringBuffer content = new StringBuffer(""); + // try { + // BufferedReader reader = new BufferedReader(new FileReader(new + // File("./git/index"))); + // while (reader.ready()) { + // content.append((char) reader.read()); + // } + // reader.close(); + // } catch (Exception e) { + // e.printStackTrace(); + // } + try { + treeHash = Git.sha1(Paths.get("./git/index")); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public String getDate() { + Calendar date = Calendar.getInstance(); + int day = date.get(Calendar.DAY_OF_MONTH); + int month = date.get(Calendar.MONTH); + int year = date.get(Calendar.YEAR); + return ((month + 1) + "/" + day + "/" + year); + } -public class Git { public static void main(String[] args) throws DigestException, NoSuchAlgorithmException, IOException { // Test creating repository when it doesn't exist (should print "Initialized // repository and deleted files") // System.out.println(initRepoTester()); // Test creating respository when it already exists (should print "Git // Repository already exists") - // initRepo(); + initRepo(); // System.out.println(initRepoTester()); - blobTester(Paths.get("/Users/oliviakong/Desktop/everything basically/forkedcodetest/newFolder"), false); - // Path path = Paths.get("/Users/oliviakong/Desktop/everything basically/forkedcodetest/newFolder"); + blobTester(Paths.get( + "/Users/RiyanKadribegovic/Desktop/School/12th/Honors Topics in Computer Science/git-project-olivia"), + false); + // Path path = Paths.get("/Users/oliviakong/Desktop/everything + // basically/forkedcodetest/newFolder"); // createBlob(path, false); } - - //Tests initRepo() for when directory already exists or doesn't exist yet - public static String initRepoTester() throws IOException{ - //Creates all three directories/files - git, objects and index within git + + public Git() { + try { + initRepo(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + // Tests initRepo() for when directory already exists or doesn't exist yet + public static String initRepoTester() throws IOException { + // Creates all three directories/files - git, objects and index within git Path file1 = Paths.get("./git/objects"); File file2 = new File("./git/index"); Path file3 = Paths.get("./git"); - //Tests if repository already exists, which should print "Git repository already exists" - if (file1.toFile().exists() && file2.exists()){ + // Tests if repository already exists, which should print "Git repository + // already exists" + if (file1.toFile().exists() && file2.exists()) { initRepo(); return ""; } - //Initializes repo + // Initializes repo initRepo(); - //Checks if files were created + // Checks if files were created boolean bool1 = file1.toFile().exists(); boolean bool2 = file2.exists(); - //Deletes all the files (have to delete objects and index first as files.delete() only deletes empty directories) + // Deletes all the files (have to delete objects and index first as + // files.delete() only deletes empty directories) boolean delete = file1.toFile().delete() && file2.delete() && file3.toFile().delete(); - //Checks if files were created and then deleted - if (bool1&&bool2&&delete){ + // Checks if files were created and then deleted + if (bool1 && bool2 && delete) { return "Initialized repository and deleted files"; } return "Did not initialize repository"; @@ -58,31 +301,38 @@ public static void initRepo() throws IOException { // along the way Path file1 = Paths.get("./git/objects"); File file2 = new File("./git/index"); + File file3 = new File("./git/HEAD"); // Makes directories - will be false if they already exist boolean bool1 = file1.toFile().mkdirs(); boolean bool2 = file2.createNewFile(); + boolean bool3 = file3.createNewFile(); // Returns "Git repository already exists" if both directories already exist - if (!(bool1) && !(bool2)) { + if (!(bool1) && !(bool2) && !(bool3)) { System.out.println("Git Repository already exists"); } } - public static void blobTester(Path path, boolean compress) throws DigestException, NoSuchAlgorithmException, IOException { + public static void blobTester(Path path, boolean compress) + throws DigestException, NoSuchAlgorithmException, IOException { // path = createBlob(path, compress); // Path path2 = Paths.get("./git/objects/" + sha1(path)); - // System.out.println("Copied file exists within objects directory: " + path2.toFile().exists()); - // // System.out.println("Contents of copied and original are the same: " + (Files.mismatch(path, path2) == -1)); + // System.out.println("Copied file exists within objects directory: " + + // path2.toFile().exists()); + // // System.out.println("Contents of copied and original are the same: " + + // (Files.mismatch(path, path2) == -1)); // boolean bool1 = path2.toFile().delete(); - // System.out.println("Path deleted correctly in objects directory in order to reset: " + (bool1)); + // System.out.println("Path deleted correctly in objects directory in order to + // reset: " + (bool1)); // checkIndex(path); // if (compress) { - // boolean bool2 = path.toFile().delete(); - // System.out.println("Unzipped path deleted correctly in order to reset: " + (bool2)); + // boolean bool2 = path.toFile().delete(); + // System.out.println("Unzipped path deleted correctly in order to reset: " + + // (bool2)); // } // Create the blob (or tree) from the file or directory Path resultingPath = createBlob(path, compress); - + String pathHash = sha1(resultingPath); Path objectsPath = Paths.get("./git/objects/" + pathHash); @@ -106,7 +356,8 @@ public static void blobTester(Path path, boolean compress) throws DigestExceptio if (allObjects != null) { for (File object : allObjects) { boolean objectDeletionStatus = object.delete(); - System.out.println("Deleted object in objects folder: " + object.getName() + " : " + objectDeletionStatus); + System.out.println( + "Deleted object in objects folder: " + object.getName() + " : " + objectDeletionStatus); } } System.out.println("All files in objects directory cleared."); @@ -114,17 +365,17 @@ public static void blobTester(Path path, boolean compress) throws DigestExceptio public static String sha1(Path path) throws DigestException, IOException, NoSuchAlgorithmException { MessageDigest md = MessageDigest.getInstance("SHA-1"); - + // if it's a directory if (path.toFile().isDirectory()) { File[] allFiles = path.toFile().listFiles(); StringBuilder indexContent = new StringBuilder(); - + if (allFiles != null && allFiles.length > 0) { for (File file : allFiles) { Path filePath = file.toPath(); String fileSha1 = sha1(filePath); - + if (file.isDirectory()) { // if directory indexContent.append("tree "); } else { // if file @@ -139,18 +390,45 @@ public static String sha1(Path path) throws DigestException, IOException, NoSuch } else { md.update(Files.readAllBytes(path)); } - + byte[] digest = md.digest(); BigInteger fileInt = new BigInteger(1, digest); - return fileInt.toString(16); + String hashText = fileInt.toString(16); + while (hashText.length() < 40) { + hashText = "0" + hashText; + } + return hashText; } - //Creates blob using fileToSave, compress - zip-compression true or false, returns the path of the unzipped file - public static Path createBlob(Path fileToSave, boolean compress) throws DigestException, NoSuchAlgorithmException, IOException { + // Creates blob using fileToSave, compress - zip-compression true or false, + // returns the path of the unzipped file + public static Path createBlob(Path fileToSave, boolean compress) + throws DigestException, NoSuchAlgorithmException, IOException { return createBlob(fileToSave, compress, ""); } - private static Path createBlob(Path fileToSave, boolean compress, String parent) throws DigestException, NoSuchAlgorithmException, IOException { + private static boolean inIndex(String line) { + int counter = 0; + try { + BufferedReader reader = new BufferedReader(new FileReader("./git/index")); + while (reader.ready()) { + if (reader.readLine().equals(line)) { + counter++; + } + } + reader.close(); + } catch (Exception e) { + e.printStackTrace(); + } + if (counter > 0) { + return true; + } else { + return false; + } + } + + private static Path createBlob(Path fileToSave, boolean compress, String parent) + throws DigestException, NoSuchAlgorithmException, IOException { StringBuilder sb = new StringBuilder(); // if file is a directory if (fileToSave.toFile().isDirectory()) { @@ -169,9 +447,19 @@ private static Path createBlob(Path fileToSave, boolean compress, String parent) // index file line if (parent.equals("")) { - sb.append("tree " + hash + " " + fileToSave.toFile().getName() + "\n"); + String toWrite = "tree " + hash + " " + fileToSave.toFile().getName(); + if (inIndex(toWrite)) { + System.out.println("already exists in index"); + } else { + sb.append(toWrite).append("\n"); + } } else { - sb.append("tree " + hash + " " + parent + "/" + fileToSave.toFile().getName() + "\n"); + String toWrite = "tree " + hash + " " + parent + "/" + fileToSave.toFile().getName(); + if (inIndex(toWrite)) { + System.out.println("already exists in index"); + } else { + sb.append(toWrite).append("\n"); + } } } else { // if directory is not empty // compresses if true, unzips in order to copy data @@ -180,7 +468,8 @@ private static Path createBlob(Path fileToSave, boolean compress, String parent) fileToSave = unzip(str1, fileToSave.getFileName().toString()); } - String currentParent = parent.isEmpty() ? fileToSave.toFile().getName() : parent + "/" + fileToSave.toFile().getName(); + String currentParent = parent.isEmpty() ? fileToSave.toFile().getName() + : parent + "/" + fileToSave.toFile().getName(); sb.append("tree " + sha1(fileToSave) + " " + currentParent + "\n"); for (File file : filesInside) { @@ -202,13 +491,14 @@ private static Path createBlob(Path fileToSave, boolean compress, String parent) // copies data Files.copy(fileToSave, hash, REPLACE_EXISTING); - String str = "blob " + sha1(fileToSave) + " " + (parent.isEmpty() ? "" : parent + "/") + fileToSave.getFileName().toString() + "\n"; + String str = "blob " + sha1(fileToSave) + " " + (parent.isEmpty() ? "" : parent + "/") + + fileToSave.getFileName().toString() + "\n"; sb.append(str); } try (BufferedWriter bw = new BufferedWriter(new FileWriter("./git/index", true))) { bw.write(sb.toString()); } - return(fileToSave); + return (fileToSave); } // zip-compression method @@ -295,4 +585,17 @@ public static void deleteIndex(String line) throws IOException { System.out.println("Entry deleted in index: " + successful); } + public static void deleteEverything(Path path) { + File file = new File(path.toString()); + if (file.listFiles() != null) { + for (File childFile : file.listFiles()) { + if (childFile.isDirectory()) { + deleteEverything(childFile.toPath()); + } + childFile.delete(); + } + file.delete(); + } + } + } \ No newline at end of file diff --git a/GitInterface.java b/GitInterface.java new file mode 100644 index 0000000..5e9aa27 --- /dev/null +++ b/GitInterface.java @@ -0,0 +1,29 @@ +public interface GitInterface { + + /** + * Stages a file for the next commit. + * + * @param filePath The path to the file to be staged. + */ + void stage(String filePath); + + /** + * Creates a commit with the given author and message. + * It should capture the current state of the repository, + * update the HEAD, and return the commit hash. + * + * @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. + */ + String commit(String author, String message); + + /** + * EXTRA CREDIT: Checks out a specific commit given its hash. + * This should update the working directory to match the + * state of the repository at that commit. + * + * @param commitHash The SHA1 hash of the commit to check out. + */ + void checkout(String commitHash); +} diff --git a/GitTester.java b/GitTester.java new file mode 100644 index 0000000..7f44338 --- /dev/null +++ b/GitTester.java @@ -0,0 +1,54 @@ +import java.nio.file.Paths; +import java.io.*; + +public class GitTester { + @SuppressWarnings("unused") + public static void main(String[] args) { + try { + // part 1: create initial commit + Git.deleteEverything(Paths.get("git")); + // Testing git creation and repo initialization: + File testFile = new File("testFile.txt"); + File testDir = new File("testDir"); + File testFile2 = new File("testDir/testFile2.txt"); + testFile.createNewFile(); + BufferedWriter writer = new BufferedWriter(new FileWriter("testFile.txt")); + writer.write("hello!!!!"); + writer.close(); + testDir.mkdir(); + testFile2.createNewFile(); + writer = new BufferedWriter(new FileWriter("testDir/testFile2.txt")); + writer.write("yoooooo"); + writer.close(); + // do the commit + Git dir = new Git(); + dir.stage("testFile.txt"); + dir.stage("testDir"); + System.out.println(dir.commit("Riyan", "part 1")); + + // part 2: create second commit + // this tests editing a file and creating a new file but only staging the new file + File newTestFile = new File("newTestFile.txt"); + newTestFile.createNewFile(); + writer = new BufferedWriter(new FileWriter("newTestFile.txt")); + writer.write("insert test content here"); + writer.close(); + writer = new BufferedWriter(new FileWriter ("testDir/testFile2.txt", true)); + writer.write ("\nand more"); + writer.close(); + // do the commit + dir.stage("newTestFile.txt"); + System.out.println(dir.commit("Riyan", "part 2")); + + // part 3: create third commit + // this stages the last file + dir.stage("testDir/testFile2.txt"); + System.out.println(dir.commit("Riyan", "part 3")); + + // part 4: check out second commit + dir.checkout("9a79fdcee8a5f88d0e54adbce45c54b335ce7dce"); + } catch (Exception e) { + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/README.md b/README.md index 3739beb..9af32e0 100644 --- a/README.md +++ b/README.md @@ -1 +1,39 @@ # git-project-olivia + +1. I coded stage(). It works +To stage a file, make sure the file exists and then call workingRepository.stage(fileName);. This will add it to the index. + +2. I coded commit(String author, String message). It works +Calling workingRepository.commit(authorName, messageContent) will update the HEAD file as well as create an instance where every staged file is saved. This also clears the index, as every unchanged file is now saved. + +3. I coded checkout(String commitHash). Everything works, except it throws a FileNotFoundException when I try to read the information in the documents of the previous commit. +Calling workingReposity.checkout(previousCommitHash) will update the HEAD to the previousCommitHash and update the current instance of work to the given commit. + +4. I fixed a lot of bugs. One bug I have not fixed is that when you stage a file, if it's inside a directory, all of the files inside that directory will be staged instead of just the staged one. Another bug is that in the checkout() method, the computer calls a FileNotFoundException where it shouldn't. + +Example of a situation where the first bug would arise: + +Initial working repository: +blob ds3r0 dir1/file1.txt +blob qrwio dir1/file2.txt +tree 3r9q8 dir1 +blob 4309r file3.txt + +After changing file1 and file2's contents but only staging file2, the commited tree would look like: +blob 08rwu dir1/file1.txt +blob 04uwr dir1/file2.txt +tree 9mim9 dir1 +blob 4309r file3.txt + +Instead of: +blob ds3r0 dir1/file1.txt +blob 04uwr dir1/file2.txt +tree 09ujm dir1 +blob 4309r file3.txt + +//all of these hashes are completely made up of keyboard smash + +Further explanation for the second bug: +When I try to read the hash files from the objects folder to fill in the information back to whatever the given commit is, it throws a FileNotFoundException even though the file exists. A message like this will show up: +java.io.FileNotFoundException: ./git/objects/6d6ffe15e7b120ab0bbb90453c30040699f793f0 (No such file or directory) +However, ./git/objects/6d6ffe15e7b120ab0bbb90453c30040699f793f0 does exist and is a valid file in the objects folder. \ No newline at end of file diff --git a/newTestFile.txt b/newTestFile.txt new file mode 100644 index 0000000..e69de29 diff --git a/testDir/testFile2.txt b/testDir/testFile2.txt new file mode 100644 index 0000000..e69de29 diff --git a/testFile.txt b/testFile.txt new file mode 100644 index 0000000..e69de29