diff --git a/CommitCreator.java b/CommitCreator.java new file mode 100644 index 0000000..11f27d2 --- /dev/null +++ b/CommitCreator.java @@ -0,0 +1,148 @@ +import java.io.*; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + + +/** +* Follows Milestone GP-4.2 instructions +* 1. Ensure repo is initialized and files are staged +* 2. Call TREE.createROOT to snapshot the working directory into tree objects +* 3. Call CommitCreator.createCommit with author and message +* +* Commit file layout +* tree: +* parent: (omit this line for the first commit) +* author: +* date: +* message: +*/ +public class CommitCreator { + + + public static String createCommit(String author, String message) { + ensureRepo(); + + + // Build a fresh root tree from the current working directory + TREE.createROOT(); + + + // Find the root tree hash + String rootHash = TraceTree.findRootTreeHash(); + if (rootHash == null || rootHash.isEmpty()) { + System.out.println("No root tree found. Run staging and TREE.createROOT first"); + return null; + } + + + // Read parent from HEAD if present + String parentHash = readHead(); + + + // Build commit text + StringBuilder sb = new StringBuilder(); + sb.append("tree: ").append(rootHash).append("\n"); + if (parentHash != null && !parentHash.isEmpty()) { + sb.append("parent: ").append(parentHash).append("\n"); + } + sb.append("author: ").append(author).append("\n"); + sb.append("date: ").append(nowString()).append("\n"); + sb.append("message: ").append(message).append("\n"); + String commitText = sb.toString(); + + + // Hash of the whole commit content + String commitHash = SHA1.encryptThisString(commitText); + + + // Save commit object file + File commitFile = new File("git/objects/" + commitHash); + writeString(commitFile, commitText); + + + // Update HEAD to point at this commit + writeString(new File("git/HEAD"), commitHash); + + + System.out.println("Commit created: " + commitHash); + return commitHash; + } + + + private static void ensureRepo() { + File git = new File("git"); + if (!git.exists()) { + GitRepositoryInitializer.initGitRepo(); + } + File objects = new File("git/objects"); + if (!objects.exists()) { + objects.mkdir(); + } + File index = new File("git/index"); + if (!index.exists()) { + try { + index.createNewFile(); + } catch (IOException e) { + System.out.println(e); + } + } + File head = new File("git/HEAD"); + if (!head.exists()) { + try { + head.createNewFile(); + } + catch (IOException e) { + System.out.println(e); + } + } + } + + + private static String readHead() { + File head = new File("git/HEAD"); + if (!head.exists()) { + return null; + } + String text = readString(head); + if (text == null) { + return null; + } + text = text.trim(); + if (text.isEmpty()) { + return null; + } + return text; + } + + + private static String nowString() { + LocalDateTime now = LocalDateTime.now(); + DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + return now.format(fmt); + } + + + private static String readString(File file) { + StringBuilder sb = new StringBuilder(); + try (BufferedReader br = new BufferedReader(new FileReader(file))) { + String line; + while ((line = br.readLine()) != null) { + sb.append(line).append("\n"); + } + } + catch (IOException e) { + return null; + } + return sb.toString(); + } + + + private static void writeString(File file, String text) { + try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) { + bw.write(text); + } + catch (IOException e) { + System.out.println(e); + } + } +} diff --git a/CommitTester.java b/CommitTester.java new file mode 100644 index 0000000..0ad5e83 --- /dev/null +++ b/CommitTester.java @@ -0,0 +1,193 @@ +import java.io.*; +import java.nio.file.Files; + +public class CommitTester { + public static void main(String[] args) throws Exception { + System.out.println("Commit tester start"); + cleanup(); + GitRepositoryInitializer.initGitRepo(); + + // Create a small working directory + File work = new File("work"); + work.mkdir(); + File sub = new File("work/sub"); + sub.mkdir(); + File a = new File("work/a.txt"); + File b = new File("work/sub/b.txt"); + a.createNewFile(); + b.createNewFile(); + Files.write(a.toPath(), "alpha".getBytes()); + Files.write(b.toPath(), "beta".getBytes()); + + // Stage and snapshot + BLOB.addFile("work/a.txt"); + BLOB.addFile("work/sub/b.txt"); + TREE.createROOT(); + + // Create first commit + String c1 = CommitCreator.createCommit("user", "initial snapshot"); + System.out.println("\nFirst commit: " + c1); + printCommitFile(c1); + checkCommitFormat(c1, true); + + // Modify a file and create another commit + Files.write(a.toPath(), "alpha v2".getBytes()); + BLOB.addFile("work/a.txt"); + TREE.createROOT(); + String c2 = CommitCreator.createCommit("user", "updated a.txt"); + System.out.println("\nSecond commit: " + c2); + printCommitFile(c2); + checkCommitFormat(c2, false); + + System.out.println("\nHEAD now points to: " + readHead()); + + // Clean up working directory and git folder + deleteRecursively(work); + cleanup(); + + System.out.println("Commit tester end"); + } + + private static void printCommitFile(String commitHash) { + if (commitHash == null || commitHash.isEmpty()) { + System.out.println("No commit hash to print."); + return; + } + File commitFile = new File("git/objects/" + commitHash); + if (!commitFile.exists()) { + System.out.println("Commit file not found: " + commitHash); + return; + } + + System.out.println("----- Commit File Content -----"); + try (BufferedReader br = new BufferedReader(new FileReader(commitFile))) { + String line; + while ((line = br.readLine()) != null) { + System.out.println(line); + } + } + catch (IOException e) { + System.out.println("Error reading commit file: " + e.getMessage()); + } + System.out.println("----- End of Commit File -----\n"); + } + + private static void checkCommitFormat(String commitHash, boolean firstCommit) { + if (commitHash == null || commitHash.isEmpty()) { + System.out.println("No commit hash to verify."); + return; + } + File commitFile = new File("git/objects/" + commitHash); + if (!commitFile.exists()) { + System.out.println("Commit file not found: " + commitHash); + return; + } + + try (BufferedReader br = new BufferedReader(new FileReader(commitFile))) { + String line; + int lineNumber = 0; + boolean hasTree = false; + boolean hasParent = false; + boolean hasAuthor = false; + boolean hasDate = false; + boolean hasMessage = false; + boolean inOrder = true; + + while ((line = br.readLine()) != null) { + lineNumber++; + if (line.startsWith("tree:")) { + hasTree = true; + if (lineNumber != 1) { + inOrder = false; + } + } + else if (line.startsWith("parent:")) { + hasParent = true; + if (firstCommit) { + System.out.println("Warning: first commit should not have parent line."); + } + } + else if (line.startsWith("author:")) { + hasAuthor = true; + } + else if (line.startsWith("date:")) { + hasDate = true; + } + else if (line.startsWith("message:")) { + hasMessage = true; + } + } + + if (!hasTree || !hasAuthor || !hasDate || !hasMessage) { + System.out.println("Commit format incorrect for: " + commitHash); + if (!hasTree) { + System.out.println("Missing 'tree:' line"); + } + if (!firstCommit && !hasParent) { + System.out.println("Missing 'parent:' line for non-initial commit"); + } + if (!hasAuthor) { + System.out.println("Missing 'author:' line"); + } + if (!hasDate) { + System.out.println("Missing 'date:' line"); + } + if (!hasMessage) { + System.out.println("Missing 'message:' line"); + } + } + else if (!inOrder) { + System.out.println("Commit has all lines but order may be incorrect."); + } + else { + System.out.println("Commit file format verified for: " + commitHash); + } + } + catch (IOException e) { + System.out.println("Error reading commit file: " + e.getMessage()); + } + } + + private static void cleanup() { + File git = new File("git"); + if (git.exists()) { + deleteRecursively(git); + } + } + + private static void deleteRecursively(File file) { + if (file == null) { + return; + } + if (!file.exists()) { + return; + } + if (file.isDirectory()) { + File[] kids = file.listFiles(); + if (kids != null) { + for (File k : kids) { + deleteRecursively(k); + } + } + } + file.delete(); + } + + private static String readHead() { + File head = new File("git/HEAD"); + if (!head.exists()) { + return ""; + } + StringBuilder sb = new StringBuilder(); + try (BufferedReader br = new BufferedReader(new FileReader(head))) { + String line; + while ((line = br.readLine()) != null) { + sb.append(line).append("\\n"); + } + } + catch (IOException e) { + return ""; + } + return sb.toString().trim(); + } +} diff --git a/TraceTree.java b/TraceTree.java new file mode 100644 index 0000000..53198d9 --- /dev/null +++ b/TraceTree.java @@ -0,0 +1,231 @@ +import java.io.*; +import java.util.ArrayList; +import java.util.List; + + +/** +* Milestone GP-4.1 Identify and Use the Root Tree +* 1) Find the root tree object in git/objects +* 2) Recursively trace referenced trees and blobs +* 3) Verify every referenced hash exists in git/objects +* 4) For blobs, verify the index contains "blob " +* 5) Print clear messages and return overall success or failure +*/ +public class TraceTree { + + + public static String findRootTreeHash() { + File objects = new File("git/objects"); + if (!objects.exists()) { + return null; + } + + List topNames = listTopLevelNames(); + File[] candidates = objects.listFiles(); + if (candidates == null) { + return null; + } + + int i = 0; + while (i < candidates.length) { + File file = candidates[i]; + if (file.isFile()) { + String content = BLOB.getFileContents(file); + if (content == null) { + content = ""; + } + content = content.trim(); + if (!content.isEmpty()) { + List namesInTree = parseNames(content); + if (namesInTree.size() > 0) { + boolean looksLikeRoot = matchesTopLevel(namesInTree, topNames); + if (looksLikeRoot) { + String hash = file.getName(); + boolean verified = verifyTreeRecursive(hash, ""); + if (verified) { + return hash; + } + } + } + } + } + i = i + 1; + } + return null; + } + + + + /** + * Recursively verifies a tree entries and confirms consistency with the index + * The hash of a tree object stored in git/objects + * The path prefix relative to the repo root, empty string for the root + * True if all entries verify correctly, false otherwise + */ + public static boolean verifyTreeRecursive(String treeHash, String basePath) { + File treeFile = objectPath(treeHash); + if (!treeFile.exists()) { + System.out.println("Missing TREE object: " + treeHash); + return false; + } + + + String content = BLOB.getFileContents(treeFile); + if (content == null) { + content = ""; + } + content = content.trim(); + if (content.isEmpty()) { + System.out.println("Empty TREE object: " + treeHash); + return false; + } + + + boolean allGood = true; + String[] lines = content.split("\n"); + int i = 0; + while (i < lines.length) { + String line = lines[i]; + String[] parts = line.split(" ", 3); + if (parts.length < 3) { + System.out.println("Malformed tree entry in " + treeHash + ": '" + line + "'"); + allGood = false; + } + else { + String type = parts[0]; + String hash = parts[1]; + String name = parts[2]; + + + String childRel; + if (basePath.isEmpty()) { + childRel = name; + } + else { + childRel = basePath + "/" + name; + } + + + File obj = objectPath(hash); + if (!obj.exists()) { + System.out.println("Missing object '" + hash + "' for entry: " + type + " " + name + " under " + treeHash); + allGood = false; + } + else { + if (type.equals("blob")) { + File index = new File("git/index"); + boolean inIndex = TREE.verifyIndexUpdate(childRel, new File(hash), index); + if (!inIndex) { + System.out.println("INDEX mismatch. Expected line for blob '" + hash + " " + childRel + "'"); + allGood = false; + } + } + else if (type.equals("tree")) { + boolean subtreeOK = verifyTreeRecursive(hash, childRel); + if (!subtreeOK) { + allGood = false; + } + } + else { + System.out.println("Unknown entry type in tree: '" + type + "' for name '" + name + "'"); + allGood = false; + } + } + } + i = i + 1; + } + return allGood; + } + + + + + // This method creates a list of all file and folder names that are located in the top level + // of the current working directory (the same place where the program is running). + // It looks through every item in the folder, skipping the "git" folder since that is part of the repository system. + // For each remaining item, it checks if the name is already in the list. + // If the name is not already included, it adds it to the list. + // When finished, it returns a list containing the unique names of all top-level files and folders. + private static List listTopLevelNames() { + List names = new ArrayList(); + File[] list = new File(".").listFiles(); + if (list == null) { + return names; + } + int i = 0; + while (i < list.length) { + File f = list[i]; + String n = f.getName(); + if (!n.equals("git")) { + if (!containsInList(names, n)) { + names.add(n); + } + } + i = i + 1; + } + return names; + } + + + // This method reads the text of a tree file and collects all of the file and folder names listed inside it. + // Each line in the tree file describes one entry in the format: "type hash name". + // The method splits the tree text into lines and then splits each line into its parts. + // It takes the third part (the name) and adds it to a list, but only if that name is not already in the list. + // When it finishes reading all lines, it returns the list of unique names found in the tree file. + private static List parseNames(String treeContent) { + List names = new ArrayList(); + String[] lines = treeContent.split("\n"); + int i = 0; + while (i < lines.length) { + String[] parts = lines[i].split(" ", 3); + if (parts.length >= 3) { + String name = parts[2]; + if (!containsInList(names, name)) { + names.add(name); + } + } + i = i + 1; + } + return names; + } + + + // This method checks if every name listed in treeNames also appears in topNames. + // It goes through each name in the treeNames list one by one. + // For each name, it calls containsInList to see if that name exists in the topNames list. + // If any name from treeNames is missing in topNames, the method returns false right away. + // If all names are found, it returns true at the end. + private static boolean matchesTopLevel(List treeNames, List topNames) { + int i = 0; + while (i < treeNames.size()) { + String name = treeNames.get(i); + boolean found = containsInList(topNames, name); + if (!found) { + return false; + } + i = i + 1; + } + return true; + } + + + // This method checks if a given value is already inside a list of strings. + // It goes through each item in the list one by one. + // If it finds an item that is the same as the given value, it returns true. + // If it finishes checking every item and does not find a match, it returns false. + private static boolean containsInList(List list, String value) { + int i = 0; + while (i < list.size()) { + if (list.get(i).equals(value)) { + return true; + } + i = i + 1; + } + return false; + } + + + private static File objectPath(String hash) { + return new File("git/objects/" + hash); + } +} diff --git a/TraceTreeTester.java b/TraceTreeTester.java new file mode 100644 index 0000000..fbf6b54 --- /dev/null +++ b/TraceTreeTester.java @@ -0,0 +1,82 @@ +import java.io.*; +import java.nio.file.Files; + +public class TraceTreeTester { + + public static void main(String[] args) throws IOException { + System.out.println("TraceTree test start"); + cleanup(); + + // Initialize repo + GitRepositoryInitializer.initGitRepo(); + + // Build a small working directory tree + File dirA = new File("work"); + dirA.mkdir(); + File sub = new File("work/sub"); + sub.mkdir(); + File f1 = new File("work/a.txt"); + File f2 = new File("work/sub/b.txt"); + f1.createNewFile(); + f2.createNewFile(); + Files.write(f1.toPath(), "alpha".getBytes()); + Files.write(f2.toPath(), "beta".getBytes()); + + // Stage files into index and create blobs + BLOB.addFile("work/a.txt"); + BLOB.addFile("work/sub/b.txt"); + + // Build root tree from current folder + TREE.createROOT(); + + // Find and verify the root tree + String root = TraceTree.findRootTreeHash(); + if (root == null) { + System.out.println("No root tree found"); + finish(dirA); + return; + } + + System.out.println("Root tree hash: " + root); + + boolean ok = TraceTree.verifyTreeRecursive(root, ""); + if (ok) { + System.out.println("Trace verification passed"); + } + else { + System.out.println("Trace verification found problems"); + } + + // Cleanup + finish(dirA); + System.out.println("TraceTree test end"); + } + + private static void finish(File workRoot) { + deleteRecursively(workRoot); + cleanup(); + } + + private static void cleanup() { + File git = new File("git"); + deleteRecursively(git); + } + + private static void deleteRecursively(File file) { + if (file == null) { + return; + } + if (!file.exists()) { + return; + } + if (file.isDirectory()) { + File[] children = file.listFiles(); + if (children != null) { + for (File child : children) { + deleteRecursively(child); + } + } + } + file.delete(); + } +}