diff --git a/GitHash.java b/GitHash.java index cc48eec..b3ef45a 100644 --- a/GitHash.java +++ b/GitHash.java @@ -8,46 +8,60 @@ import java.io.IOException; import java.nio.file.Files; import java.security.MessageDigest; +import java.util.ArrayList; import java.security.NoSuchAlgorithmException; public class GitHash { public static void gitRepoInit() throws IOException { - GitHash.createDirectoryIfMissing("git"); - GitHash.createDirectoryIfMissing("git/objects"); - GitHash.createFileIfMissing("git/HEAD"); - GitHash.createFileIfMissing("git/INDEX"); - System.out.println("Git Repository Created"); + boolean createdSomething = false; + if (createDirectoryIfMissing("git")) { + createdSomething = true; + } + if (createDirectoryIfMissing("git/objects")) { + createdSomething = true; + } + if (createFileIfMissing("git/HEAD")) { + createdSomething = true; + } + if (createFileIfMissing("git/INDEX")) { + createdSomething = true; + } + + if (createdSomething) { + System.out.println("Git Repository Created"); + } + else { + System.out.println("Git Repository Already Exists"); + } } - public static void createDirectoryIfMissing(String path) throws IOException { + public static boolean createDirectoryIfMissing(String path) throws IOException { File directoryCreator = new File(path); if (!directoryCreator.exists()) { - directoryCreator.mkdir(); - } else { - System.out.println(path + " Already Exists"); - } + return directoryCreator.mkdir(); + } + return false; } - public static void createFileIfMissing(String path) throws IOException { + public static boolean createFileIfMissing(String path) throws IOException { File fileCreator = new File(path); if (!fileCreator.exists()) { - fileCreator.createNewFile(); - } else { - System.out.println(path + " Already Exists"); + return fileCreator.createNewFile(); } + return false; } public static void deleteGitDirectory() throws IOException { - File indexDeleter = new File("git/INDEX"); - indexDeleter.delete(); - File headDeleter = new File("git/HEAD"); - headDeleter.delete(); - File objectsDeleter = new File("git/objects"); - objectsDeleter.delete(); - File gitDeleter = new File("git"); - gitDeleter.delete(); - System.out.println("Git Repository Deleted"); + removeRecursively("git", "."); // deletes ./git and everything inside it + + File gitChecker = new File("git"); + if (!gitChecker.exists()) { + System.out.println("Git Repository Deleted"); + } + else { + System.out.println("Git Repository Not Deleted"); + } } public static String generateSHA1Hash(File file) throws IOException { @@ -68,6 +82,11 @@ public static String generateSHA1Hash(File file) throws IOException { public static void createBLOBAndAddToObjects(File file) throws IOException { String hash = generateSHA1Hash(file); File blob = new File("git/objects/" + hash); + + if (blob.exists()) { + return; + } + blob.createNewFile(); FileOutputStream blobWriter = new FileOutputStream(blob); BufferedReader fileCopier = new BufferedReader(new FileReader(file)); @@ -95,9 +114,16 @@ public static void blobExists(File file) throws IOException { } } + // newline between entries only public static void addBLOBEntryToIndex(File file) throws IOException { - BufferedWriter entryWriter = new BufferedWriter(new FileWriter("git/INDEX", true)); - entryWriter.write(generateSHA1Hash(file) + " " + file.getName() + "\n"); + File indexFile = new File("git/INDEX"); + boolean isEmpty = indexFile.length() == 0; + BufferedWriter entryWriter = new BufferedWriter(new FileWriter(indexFile, true)); + if (!isEmpty) { + entryWriter.write("\n"); + } + + entryWriter.write(generateSHA1Hash(file) + " " + file.getName()); entryWriter.close(); } @@ -125,4 +151,429 @@ public static void removeRecursively(String pathName, String parentPathName) thr return; } + + + // 3.0 + public static String addDirectory(String directoryPath) throws IOException { + File dir = new File(directoryPath); + if (!dir.exists() || !dir.isDirectory()) { + throw new IllegalArgumentException("Not a valid directory: " + directoryPath); + } + + return buildTreeAndStore(dir, directoryPath); + } + + + private static String buildTreeAndStore(File dir, String dirLabel) throws IOException { + File[] children = dir.listFiles(); + if (children == null) { + return storeTreeObjectContent(""); + } + + java.util.ArrayList treeLines = new java.util.ArrayList<>(); + + for (File child: children) { + String name = child.getName(); + + if (!name.equals("git")) { + String childPathLabel = dirLabel + "/" + name; + + if (child.isDirectory()) { + String subTreeHash = buildTreeAndStore(child, childPathLabel); + treeLines.add("tree " + subTreeHash + " " + name); + addTreeEntryToIndex(subTreeHash, childPathLabel); + } + else { + createBLOBAndAddToObjects(child); + String blobHash = generateSHA1Hash(child); + treeLines.add("blob " + blobHash + " " + name); + addBlobEntryToIndex(blobHash, childPathLabel); + } + } + } + + String treeContent = ""; + for (int i = 0; i < treeLines.size(); i++) { + treeContent += treeLines.get(i); + if (i < treeLines.size() - 1) { + treeContent += "\n"; + } + } + + return storeTreeObjectContent(treeContent); + } + + + private static String storeTreeObjectContent(String treeContent) throws IOException { + File tempTreeFile = new File("git/TEMP_TREE.txt"); + if (!tempTreeFile.exists()) { + tempTreeFile.createNewFile(); + } + + FileWriter tempWriter = new FileWriter(tempTreeFile, false); + tempWriter.write(treeContent); + tempWriter.close(); + + String treeHash = generateSHA1Hash(tempTreeFile); + + File treeObject = new File("git/objects/" + treeHash); + if (!treeObject.exists()) { + treeObject.createNewFile(); + FileWriter objWriter = new FileWriter(treeObject, false); + objWriter.write(treeContent); + objWriter.close(); + } + + tempTreeFile.delete(); + + return treeHash; + } + + + private static void addBlobEntryToIndex(String hash, String path) throws IOException { + addTypedEntryToIndex("blob", hash, path); + } + + private static void addTreeEntryToIndex(String hash, String path) throws IOException { + addTypedEntryToIndex("tree", hash, path); + } + + private static void addTypedEntryToIndex(String type, String hash, String path) throws IOException { + File indexFile = new File("git/INDEX"); + boolean isEmpty = indexFile.length() == 0; + + BufferedWriter entryWriter = new BufferedWriter(new FileWriter(indexFile, true)); + if (!isEmpty) { + entryWriter.write("\n"); + } + entryWriter.write(type + " " + hash + " " + path); + entryWriter.close(); + } + + + //3.1 + public static void stageDirectoryToIndexBlobsOnly(String directoryPath) throws IOException { + File dir = new File(directoryPath); + if (!dir.exists() || !dir.isDirectory()) { + throw new IllegalArgumentException("Not a valid directory: " + directoryPath); + } + + stageDirectoryHelper(dir, directoryPath); + } + + private static void stageDirectoryHelper(File dir, String rootLabel) throws IOException { + File[] children = dir.listFiles(); + if (children == null) { + return; + } + + for (File child: children) { + String name = child.getName(); + + if (!name.equals("git")) { + String childPath = rootLabel + "/" + name; + + if (child.isDirectory()) { + stageDirectoryHelper(child, childPath); + } + else { + createBLOBAndAddToObjects(child); + String hash = generateSHA1Hash(child); + addBlobPathEntryToIndex(hash, childPath); + } + } + } + } + + private static void addBlobPathEntryToIndex(String hash, String relativePath) throws IOException { + File indexFile = new File("git/INDEX"); + boolean isEmpty = indexFile.length() == 0; + + BufferedWriter writer = new BufferedWriter(new FileWriter(indexFile, true)); + if (!isEmpty) { + writer.write("\n"); + } + writer.write(hash + " " + normalizeRelativePath(relativePath)); + writer.close(); + } + + private static String normalizeRelativePath(String path) { + path = path.trim(); + + while (path.startsWith("./")) { + path = path.substring(2); + } + while (path.startsWith("/")) { + path = path.substring(1); + } + return path; + } + + + //objective B + public static void writeWorkingListFromIndex(String workingListFilePath) throws IOException { + File indexFile = new File("git/INDEX"); + File workingFile = new File(workingListFilePath); + + FileWriter clear = new FileWriter(workingFile, false); + clear.write(""); + clear.close(); + + if (indexFile.exists() && indexFile.length() > 0) { + BufferedReader br = new BufferedReader(new FileReader(indexFile)); + String line; + boolean firstOutLine = true; + + while ((line = br.readLine()) != null) { + line = line.trim(); + + if (line.length() > 0) { + String[] parts = line.split(" ", 2); + + if (parts.length == 2) { + String sha1 = parts[0]; + String path = normalizeRelativePath(parts[1]); + + BufferedWriter bw = new BufferedWriter(new FileWriter(workingFile, true)); + if (!firstOutLine) { + bw.write("\n"); + } + bw.write("blob " + sha1 + " " + path); + bw.close(); + + firstOutLine = false; + } + } + } + + br.close(); + } + } + + + //objective C + public static String generateTreesFromWorkingList(String workingListFilePath, String rootDirPath) throws IOException { + rootDirPath = normalizeRelativePath(rootDirPath); + + ArrayList items = readWorkingListItems(workingListFilePath); + + ArrayList dirs = new ArrayList<>(); + addUniqueDir(dirs, rootDirPath); + + for (int i = 0; i < items.size(); i++) { + WLItem it = items.get(i); + if (it.type.equals("blob")) { + String parent = parentDir(it.path); + + while (parent.length() > 0) { + addUniqueDir(dirs, parent); + if (parent.equals(rootDirPath)) { + parent = ""; + } + else { + parent = parentDir(parent); + } + } + } + } + + sortDirsByDepthDescending(dirs); + + ArrayList dirKeys = new ArrayList<>(); + ArrayList dirVals = new ArrayList<>(); + + for (int d = 0; d < dirs.size(); d++) { + String dirPath = dirs.get(d); + + ArrayList treeLines = new ArrayList<>(); + + for (int i = 0; i < items.size(); i++) { + WLItem it = items.get(i); + if (it.type.equals("blob")) { + String p = parentDir(it.path); + if (dirPath.equals(p)) { + treeLines.add("blob " + it.hash + " " + nameOnly(it.path)); + } + } + } + + for (int i = 0; i < dirs.size(); i++) { + String maybeChild = dirs.get(i); + if (isDirectChildDir(dirPath, maybeChild)) { + String childHash = getMappedHash(dirKeys, dirVals, maybeChild); + if (childHash != null) { + treeLines.add("tree " + childHash + " " + nameOnly(maybeChild)); + } + } + } + + String treeContent = ""; + for (int i = 0; i < treeLines.size(); i++) { + treeContent += treeLines.get(i); + if (i < treeLines.size() - 1) { + treeContent += "\n"; + } + } + + String treeHash = storeTreeObjectContent(treeContent); + + dirKeys.add(dirPath); + dirVals.add(treeHash); + + appendWorkingListEntry(workingListFilePath, "tree " + treeHash + " " + dirPath); + + + items.add(new WLItem("tree", treeHash, dirPath)); + } + + return getMappedHash(dirKeys, dirVals, rootDirPath); + } + + + private static class WLItem { + String type; + String hash; + String path; + + WLItem(String type, String hash, String path) { + this.type = type; + this.hash = hash; + this.path = path; + } + } + + private static ArrayList readWorkingListItems(String workingListFilePath) throws IOException { + ArrayList items = new ArrayList<>(); + File f = new File(workingListFilePath); + + if (f.exists() && f.length() > 0) { + BufferedReader br = new BufferedReader(new FileReader(f)); + String line; + + while ((line = br.readLine()) != null) { + line = line.trim(); + + if (line.length() > 0) { + String[] parts = line.split(" ", 3); + if (parts.length == 3) { + items.add(new WLItem(parts[0], parts[1], normalizeRelativePath(parts[2]))); + } + } + } + + br.close(); + } + return items; + } + + private static void addUniqueDir(ArrayList dirs, String dirPath) { + boolean exists = false; + for (int i = 0; i < dirs.size(); i++) { + if (dirs.get(i).equals(dirPath)) { + exists = true; + } + } + if (!exists) { + dirs.add(dirPath); + } + } + + private static void sortDirsByDepthDescending(ArrayList dirs) { + for (int i = 0; i < dirs.size(); i++) { + int best = i; + for (int j = i + 1; j < dirs.size(); j++) { + if (depth(dirs.get(j)) > depth(dirs.get(best))) { + best = j; + } + } + String temp = dirs.get(i); + dirs.set(i, dirs.get(best)); + dirs.set(best, temp); + } + } + + private static void appendWorkingListEntry(String workingListFilePath, String entry) throws IOException { + File f = new File(workingListFilePath); + boolean isEmpty = (!f.exists() || f.length() == 0); + + BufferedWriter bw = new BufferedWriter(new FileWriter(f, true)); + if (!isEmpty) { + bw.write("\n"); + } + bw.write(entry); + bw.close(); + } + + private static String getMappedHash(ArrayList keys, ArrayList vals, String key) { + for (int i = 0; i < keys.size(); i++) { + if (keys.get(i).equals(key)) { + return vals.get(i); + } + } + return null; + } + + private static String parentDir(String path) { + int lastSlash = -1; + + for (int i = 0; i < path.length(); i++) { + if (path.charAt(i) == '/') { + lastSlash = i; + } + } + + if (lastSlash == -1) { + return ""; + } + return path.substring(0, lastSlash); + } + + private static String nameOnly(String path) { + int lastSlash = -1; + + for (int i = 0; i < path.length(); i++) { + if (path.charAt(i) == '/') { + lastSlash = i; + } + } + + if (lastSlash == -1) { + return path; + } + return path.substring(lastSlash + 1); + } + + private static int depth(String dirPath) { + int count = 0; + for (int i = 0; i < dirPath.length(); i++) { + if (dirPath.charAt(i) == '/') { + count++; + } + } + return count; + } + + private static boolean isDirectChildDir(String parentDir, String childDir) { + if (childDir.equals(parentDir)) { + return false; + } + + String prefix = parentDir + "/"; + if (!childDir.startsWith(prefix)) { + return false; + } + + String rest = childDir.substring(prefix.length()); + + int slashPos = -1; + for (int i = 0; i < rest.length(); i++) { + if (rest.charAt(i) == '/') { + slashPos = i; + } + } + return slashPos == -1; + } + + + } diff --git a/GitHashMilestone31Tester.java b/GitHashMilestone31Tester.java new file mode 100644 index 0000000..58b9593 --- /dev/null +++ b/GitHashMilestone31Tester.java @@ -0,0 +1,243 @@ +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; + +public class GitHashMilestone31Tester { + + public static void main(String[] args) throws IOException { + System.out.println("-------Milestone 3.1 Tester-------"); + + GitHash.deleteGitDirectory(); + GitHash.gitRepoInit(); + + // Build a folder to test: + // m31root/ + // A.txt + // sub/ + // B.txt + String root = "m31root"; + String subDir = root + "/sub"; + + deleteFolder(new File(root)); + new File(root).mkdir(); + new File(subDir).mkdir(); + + File a = new File(root + "/A.txt"); + File b = new File(subDir + "/B.txt"); + + writeFile(a, "AAA"); + writeFile(b, "BBB"); + + GitHash.cleanObjectsAndINDEX(); + + System.out.println("\n---3.1A: Stage directory to INDEX (blobs only)---"); + GitHash.stageDirectoryToIndexBlobsOnly(root); + + File indexFile = new File("git/INDEX"); + printCheck("INDEX exists", indexFile.exists()); + printCheck("INDEX not empty", indexFile.length() > 0); + printCheck("INDEX contains NO tree lines", !fileContainsLineStartingWith(indexFile, "tree ")); + + String indexText = readWholeFile(indexFile); + String aHash = GitHash.generateSHA1Hash(a); + String bHash = GitHash.generateSHA1Hash(b); + + printCheck("INDEX contains A.txt as ", containsExactLine(indexText, aHash + " " + root + "/A.txt")); + printCheck("INDEX contains B.txt as ", containsExactLine(indexText, bHash + " " + root + "/sub/B.txt")); + + System.out.println("\n---3.1B: Write working list from INDEX---"); + String workingListPath = "git/WORKING_LIST.txt"; + GitHash.writeWorkingListFromIndex(workingListPath); + + File workingFile = new File(workingListPath); + printCheck("WORKING_LIST exists", workingFile.exists()); + printCheck("WORKING_LIST not empty", workingFile.length() > 0); + + String workingBefore = readWholeFile(workingFile); + printCheck("WORKING_LIST contains blob A line", containsExactLine(workingBefore, "blob " + aHash + " " + root + "/A.txt")); + printCheck("WORKING_LIST contains blob B line", containsExactLine(workingBefore, "blob " + bHash + " " + root + "/sub/B.txt")); + + System.out.println("\n---3.1C: Generate trees from working list---"); + String rootTreeHash = GitHash.generateTreesFromWorkingList(workingListPath, root); + System.out.println("Root tree hash: " + rootTreeHash); + + printCheck("Root tree hash length is 40", rootTreeHash != null && rootTreeHash.length() == 40); + + File rootTreeObj = new File("git/objects/" + rootTreeHash); + printCheck("Root tree object exists in git/objects", rootTreeObj.exists()); + + String workingAfter = readWholeFile(workingFile); + printCheck("WORKING_LIST now contains at least one tree line", fileContainsLineStartingWith(workingFile, "tree ")); + + String subTreeHash = findTreeHashForPath(workingAfter, root + "/sub"); + printCheck("WORKING_LIST contains tree entry for " + root + "/sub", subTreeHash != null); + + if (subTreeHash != null) { + File subTreeObj = new File("git/objects/" + subTreeHash); + printCheck("Sub tree object exists in git/objects", subTreeObj.exists()); + + String subTreeText = readWholeFile(subTreeObj); + printCheck("Sub tree object contains 'blob B.txt' (name only)", containsExactLine(subTreeText, "blob " + bHash + " B.txt")); + + printCheck("Sub tree object does NOT end with newline", !fileEndsWithNewline(subTreeObj)); + } + + String rootTreeText = readWholeFile(rootTreeObj); + printCheck("Root tree object contains 'blob A.txt' (name only)", containsExactLine(rootTreeText, "blob " + aHash + " A.txt")); + + if (subTreeHash != null) { + printCheck("Root tree object contains 'tree sub' (name only)", containsExactLine(rootTreeText, "tree " + subTreeHash + " sub")); + } + + printCheck("INDEX does NOT end with newline", !fileEndsWithNewline(indexFile)); + printCheck("WORKING_LIST does NOT end with newline", !fileEndsWithNewline(workingFile)); + printCheck("Root tree object does NOT end with newline", !fileEndsWithNewline(rootTreeObj)); + + printCheck("A blob object exists", new File("git/objects/" + aHash).exists()); + printCheck("B blob object exists", new File("git/objects/" + bHash).exists()); + + deleteFolder(new File(root)); + + System.out.println("\n------Done------"); + } + + private static void writeFile(File f, String text) throws IOException { + FileWriter fw = new FileWriter(f, false); + fw.write(text); + fw.close(); + } + + private static String readWholeFile(File f) throws IOException { + BufferedReader br = new BufferedReader(new FileReader(f)); + String out = ""; + String line; + boolean first = true; + + while ((line = br.readLine()) != null) { + if (!first) out += "\n"; + out += line; + first = false; + } + br.close(); + return out; + } + + private static boolean containsExactLine(String text, String target) { + String current = ""; + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + if (c == '\n') { + if (current.equals(target)) { + return true; + } + current = ""; + } + else { + current += c; + } + } + return current.equals(target); + } + + private static boolean fileContainsLineStartingWith(File f, String prefix) throws IOException { + if (!f.exists() || f.length() == 0){ + return false; + } + BufferedReader br = new BufferedReader(new FileReader(f)); + String line; + boolean found = false; + + while ((line = br.readLine()) != null) { + if (line.startsWith(prefix)) { + found = true; + } + } + br.close(); + return found; + } + + private static String findTreeHashForPath(String workingText, String targetPath) { + String current = ""; + for (int i = 0; i < workingText.length(); i++) { + char c = workingText.charAt(i); + if (c == '\n') { + String h = treeHashFromLine(current, targetPath); + if (h != null) return h; + current = ""; + } + else { + current += c; + } + } + return treeHashFromLine(current, targetPath); + } + + private static String treeHashFromLine(String line, String targetPath) { + if (!line.startsWith("tree ")) { + return null; + } + + int firstSpace = -1; + for (int i = 5; i < line.length(); i++) { + if (line.charAt(i) == ' ') { + firstSpace = i; + i = line.length(); + } + } + + if (firstSpace == -1) { + return null; + } + + String hash = line.substring(5, firstSpace); + String path = line.substring(firstSpace + 1); + + if (path.equals(targetPath)) { + return hash; + } + return null; + } + + private static boolean fileEndsWithNewline(File f) throws IOException { + if (!f.exists() || f.length() == 0) { + return false; + } + + FileReader fr = new FileReader(f); + int last = -1; + int ch; + + while ((ch = fr.read()) != -1) { + last = ch; + } + fr.close(); + return last == '\n'; + } + + private static void deleteFolder(File f) { + if (f == null || !f.exists()) { + return; + } + + if (f.isDirectory()) { + File[] children = f.listFiles(); + if (children != null) { + for (int i = 0; i < children.length; i++) { + deleteFolder(children[i]); + } + } + } + f.delete(); + } + + private static void printCheck(String msg, boolean ok) { + if (ok) { + System.out.println("[PASS] " + msg); + } + else { + System.out.println("[FAIL] " + msg); + } + } +} \ No newline at end of file diff --git a/GitHashMilestone3Tester.java b/GitHashMilestone3Tester.java new file mode 100644 index 0000000..74b65a4 --- /dev/null +++ b/GitHashMilestone3Tester.java @@ -0,0 +1,214 @@ +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; + +public class GitHashMilestone3Tester { + + public static void main(String[] args) throws IOException { + System.out.println("-------Milestone 3 Tester-------"); + + GitHash.deleteGitDirectory(); + GitHash.gitRepoInit(); + + // Make a simple folder structure to test: + // m3testRoot/ + // README.md + // Hello.txt + // scripts/ + // Notes.txt + + String root = "m3testRoot"; + String scripts = root + "/scripts"; + + deleteFolder(new File(root)); + + new File(root).mkdir(); + new File(scripts).mkdir(); + + File readme = new File(root + "/README.md"); + File hello = new File(root + "/Hello.txt"); + File notes = new File(scripts + "/Notes.txt"); + + writeFile(readme, "This is a README.\nSecond line."); + writeFile(hello, "Hello world!"); + writeFile(notes, "This is a text file inside the scripts folder."); + + System.out.println("\nCalling addDirectory(\"" + root + "\")..."); + String rootTreeHash = GitHash.addDirectory(root); + System.out.println("Returned tree hash: " + rootTreeHash); + + File rootTreeObject = new File("git/objects/" + rootTreeHash); + printCheck("Root tree object exists in git/objects", rootTreeObject.exists()); + + String readmeHash = GitHash.generateSHA1Hash(readme); + String helloHash = GitHash.generateSHA1Hash(hello); + String notesHash = GitHash.generateSHA1Hash(notes); + + printCheck("README blob exists", new File("git/objects/" + readmeHash).exists()); + printCheck("Hello blob exists", new File("git/objects/" + helloHash).exists()); + printCheck("Notes.txt blob exists", new File("git/objects/" + notesHash).exists()); + + System.out.println("\nChecking git/INDEX"); + String indexText = readFileAsText(new File("git/INDEX")); + + printCheck("INDEX has README entry", hasLine(indexText, "blob " + readmeHash + " " + root + "/README.md")); + + printCheck("INDEX has Hello entry", hasLine(indexText, "blob " + helloHash + " " + root + "/Hello.txt")); + + printCheck("INDEX has Notes.txt entry", hasLine(indexText, "blob " + notesHash + " " + root + "/scripts/Notes.txt")); + + String scriptsTreeHash = findTreeHashInIndex(indexText, root + "/scripts"); + printCheck("INDEX has scripts tree entry", scriptsTreeHash != null); + + if (scriptsTreeHash != null) { + File scriptsTreeObject = new File("git/objects/" + scriptsTreeHash); + printCheck("Scripts tree object exists", scriptsTreeObject.exists()); + + String scriptsTreeText = readFileAsText(scriptsTreeObject); + printCheck("Scripts tree file includes Notes.txt (name only)", hasLine(scriptsTreeText, "blob " + notesHash + " Notes.txt")); + + printCheck("Scripts tree file does NOT end with a newline", !fileEndsWithNewline(scriptsTreeObject)); + } + + System.out.println("\nChecking root tree object content"); + String rootTreeText = readFileAsText(rootTreeObject); + + printCheck("Root tree has README.md line (name only)", hasLine(rootTreeText, "blob " + readmeHash + " README.md")); + + printCheck("Root tree has Hello.txt line (name only)", hasLine(rootTreeText, "blob " + helloHash + " Hello.txt")); + + if (scriptsTreeHash != null) { + printCheck("Root tree has scripts line (name only)", hasLine(rootTreeText, "tree " + scriptsTreeHash + " scripts")); + } + + printCheck("INDEX does NOT end with a newline", !fileEndsWithNewline(new File("git/INDEX"))); + + printCheck("Root tree object does NOT end with a newline", !fileEndsWithNewline(rootTreeObject)); + + deleteFolder(new File(root)); + + System.out.println("\n-------Done-------"); + } + + + private static void writeFile(File f, String text) throws IOException { + FileWriter fw = new FileWriter(f, false); + fw.write(text); + fw.close(); + } + + private static String readFileAsText(File f) throws IOException { + BufferedReader br = new BufferedReader(new FileReader(f)); + String text = ""; + String line; + boolean firstLine = true; + + while ((line = br.readLine()) != null) { + if (!firstLine) { + text += "\n"; + } + text += line; + firstLine = false; + } + br.close(); + return text; + } + + private static boolean hasLine(String text, String target) { + String current = ""; + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + if (c == '\n') { + if (current.equals(target)) { + return true; + } + current = ""; + } + else { + current += c; + } + } + return current.equals(target); + } + + private static String findTreeHashInIndex(String indexText, String path) { + String current = ""; + for (int i = 0; i < indexText.length(); i++) { + char c = indexText.charAt(i); + if (c == '\n') { + String hash = getTreeHashFromLine(current, path); + if (hash != null) { + return hash; + } + current = ""; + } + else { + current += c; + } + } + return getTreeHashFromLine(current, path); + } + + private static String getTreeHashFromLine(String line, String path) { + if (!line.startsWith("tree ")) { + return null; + } + + int firstSpaceAfterTree = line.indexOf(' ', 5); + if (firstSpaceAfterTree == -1) { + return null; + } + + String hash = line.substring(5, firstSpaceAfterTree); + String rest = line.substring(firstSpaceAfterTree + 1); + + if (rest.equals(path)) { + return hash; + } + return null; + } + + private static boolean fileEndsWithNewline(File file) throws IOException { + if (!file.exists()) { + return false; + } + + FileReader fr = new FileReader(file); + int lastChar = -1; + int ch; + + while ((ch = fr.read()) != -1) { + lastChar = ch; + } + fr.close(); + + return lastChar == '\n'; + } + + private static void deleteFolder(File f) { + if (f == null || !f.exists()) { + return; + } + + if (f.isDirectory()) { + File[] children = f.listFiles(); + if (children != null) { + for (int i = 0; i < children.length; i++) { + deleteFolder(children[i]); + } + } + } + f.delete(); + } + + private static void printCheck(String message, boolean ok) { + if (ok) { + System.out.println("[PASS] " + message); + } + else { + System.out.println("[FAIL] " + message); + } + } +} diff --git a/GitHashTester.java b/GitHashTester.java index 95eeb08..9ae6d28 100644 --- a/GitHashTester.java +++ b/GitHashTester.java @@ -1,4 +1,6 @@ +import java.io.BufferedReader; import java.io.File; +import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; @@ -116,6 +118,23 @@ public static void indexMethodsTester(int numFiles, int fileLength) throws IOExc } public static void doIndexEntriesMatchActualFiles(File newTestFile) throws IOException { + String expected = GitHash.generateSHA1Hash(newTestFile) + " " + newTestFile.getName(); + BufferedReader br = new BufferedReader(new FileReader("git/INDEX")); + String line; + String last = null; + while ((line = br.readLine()) != null) { + last = line; + } + br.close(); + + if (expected.equals(last)) { + System.out.println("INDEX entry matches for " + newTestFile.getName()); + } + else { + System.out.println("INDEX entry does NOT match for " + newTestFile.getName()); + System.out.println("Expected: " + expected); + System.out.println("Found: " + last); + } } } diff --git a/INDEX 2 b/INDEX 2 new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md index 19286c3..a9a0ac2 100644 --- a/README.md +++ b/README.md @@ -42,4 +42,41 @@ it may be useful to change some "checker" methods to booleans instead of voids t output test being run before running it in Terminal -the way indexTester functions it will change the contents of each file every run so if there is a problem specific to a file be sure to denote it \ No newline at end of file +the way indexTester functions it will change the contents of each file every run so if there is a problem specific to a file be sure to denote it + + + + + +RK Part 2 Notes: +For Milestone 3.0, I added the ability to back up a directory by creating a tree object that represents the contents of a folder. + +What I added: +- A method to take a directory path and recursively process everything inside it. +- For each file the program: + - hashes the file contents using SHA1 method provided + - creates a BLOB object in git/objects/ if it doesn’t already exist + - adds a blob entry into the directory’s tree content +- For each subdirectory the program: + - recursively creates a tree for that subdirectory + - adds a tree entry into the parent directory’s tree content +- The final tree content is hashed saved into git/objects/ as a tree object and the method returns the tree’s SHA1 hash. + +Milestone 3.1: Recursive tree generation + working list +For Milestone 3.1, I changed the process to build trees using a “working list” approach that starts from the index. + +What I added: +- A method to stage a directory so that git/INDEX contains only file (BLOB) entries using relative paths. +- A method to convert the git/INDEX file into a separate working list file where each line begins with blob. +- A method to generate tree objects bottom-up by: + - reading the working list + - prioritizing deeper directories first + - creating tree objects for each subdirectory and then the root + - appending tree entries into the working list as each directory is processed + +How I tested +I used a tester class (GitHashMilestone31Tester) that creates a small sample folder with a subfolder and two files runs the 3.1 steps and checks that: +- the index and working list are created and updated correctly +- blob objects are created for the files +- tree objects are created for the directories +- the generated tree content matches what the directory structure should produce diff --git a/git/INDEX b/git/INDEX index e69de29..ebbfde0 100644 --- a/git/INDEX +++ b/git/INDEX @@ -0,0 +1,2 @@ +aa6878b1c31a9420245df1daffb7b223338737a3 m31root/sub/B.txt +606ec6e9bd8a8ff2ad14e5fade3f264471e82251 m31root/A.txt \ No newline at end of file diff --git a/git/WORKING_LIST.txt b/git/WORKING_LIST.txt new file mode 100644 index 0000000..3dd82b6 --- /dev/null +++ b/git/WORKING_LIST.txt @@ -0,0 +1,4 @@ +blob aa6878b1c31a9420245df1daffb7b223338737a3 m31root/sub/B.txt +blob 606ec6e9bd8a8ff2ad14e5fade3f264471e82251 m31root/A.txt +tree 318486a944fddbc8ddf74e51300780dfe3286d59 m31root/sub +tree e635e4d997d046c845d23a42e0e83c4421c3580a m31root \ No newline at end of file diff --git a/git/objects/318486a944fddbc8ddf74e51300780dfe3286d59 b/git/objects/318486a944fddbc8ddf74e51300780dfe3286d59 new file mode 100644 index 0000000..65a1771 --- /dev/null +++ b/git/objects/318486a944fddbc8ddf74e51300780dfe3286d59 @@ -0,0 +1 @@ +blob aa6878b1c31a9420245df1daffb7b223338737a3 B.txt \ No newline at end of file diff --git a/git/objects/606ec6e9bd8a8ff2ad14e5fade3f264471e82251 b/git/objects/606ec6e9bd8a8ff2ad14e5fade3f264471e82251 new file mode 100644 index 0000000..43d88b6 --- /dev/null +++ b/git/objects/606ec6e9bd8a8ff2ad14e5fade3f264471e82251 @@ -0,0 +1 @@ +AAA \ No newline at end of file diff --git a/git/objects/aa6878b1c31a9420245df1daffb7b223338737a3 b/git/objects/aa6878b1c31a9420245df1daffb7b223338737a3 new file mode 100644 index 0000000..f6d5afa --- /dev/null +++ b/git/objects/aa6878b1c31a9420245df1daffb7b223338737a3 @@ -0,0 +1 @@ +BBB \ No newline at end of file diff --git a/git/objects/e635e4d997d046c845d23a42e0e83c4421c3580a b/git/objects/e635e4d997d046c845d23a42e0e83c4421c3580a new file mode 100644 index 0000000..922cc18 --- /dev/null +++ b/git/objects/e635e4d997d046c845d23a42e0e83c4421c3580a @@ -0,0 +1,2 @@ +blob 606ec6e9bd8a8ff2ad14e5fade3f264471e82251 A.txt +tree 318486a944fddbc8ddf74e51300780dfe3286d59 sub \ No newline at end of file diff --git a/testFile0.txt b/testFile0.txt index f6fb88f..6752bea 100644 --- a/testFile0.txt +++ b/testFile0.txt @@ -1 +1 @@ -$^bU5/#H\@nLLkZc^0;7 \ No newline at end of file +5f*x>9'OF]Kz+d?XbLVH \ No newline at end of file diff --git a/testFile1.txt b/testFile1.txt index 9feeeba..b880d03 100644 --- a/testFile1.txt +++ b/testFile1.txt @@ -1 +1 @@ -Tvcp~"DiWrSs-oK#~L({ \ No newline at end of file +"iEGMZif6Z2{S*sD4B;= \ No newline at end of file diff --git a/testFile2.txt b/testFile2.txt index 54e0850..9173f86 100644 --- a/testFile2.txt +++ b/testFile2.txt @@ -1 +1 @@ -Wf0ldE*YrF(9cu"_xmV^ \ No newline at end of file +4xL#.ybx~(cJZ6*yro]O \ No newline at end of file diff --git a/testFile3 2.txt b/testFile3 2.txt new file mode 100644 index 0000000..36f3669 --- /dev/null +++ b/testFile3 2.txt @@ -0,0 +1 @@ +vIr:vX!;kxV(l?wR|9/] \ No newline at end of file diff --git a/testFile3.txt b/testFile3.txt index 36f3669..446dcc2 100644 --- a/testFile3.txt +++ b/testFile3.txt @@ -1 +1 @@ -vIr:vX!;kxV(l?wR|9/] \ No newline at end of file +*u2G!(=A|0oBcalf6ON, \ No newline at end of file diff --git a/testFile4 2.txt b/testFile4 2.txt new file mode 100644 index 0000000..1c68012 --- /dev/null +++ b/testFile4 2.txt @@ -0,0 +1 @@ +RpLN^eKf08HSrecrf#Z} \ No newline at end of file diff --git a/testFile4.txt b/testFile4.txt index 1c68012..88cc713 100644 --- a/testFile4.txt +++ b/testFile4.txt @@ -1 +1 @@ -RpLN^eKf08HSrecrf#Z} \ No newline at end of file +$I`'tfe'|jiR.G{)QTTO \ No newline at end of file