-
-
Save dmitrii-artuhov/f7c30137703acb7ca00408be7a3c10e8 to your computer and use it in GitHub Desktop.
Minigit - Git version control system replica
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package ru.hse.mit.git.components.fs; | |
import java.io.IOException; | |
import java.nio.file.Files; | |
import java.nio.file.Path; | |
import java.util.List; | |
import java.util.stream.Stream; | |
import ru.hse.mit.git.GitException; | |
public class AbstractEditableFile { | |
protected String filename; | |
protected Path fullPath; | |
public String getFilename() { | |
return filename; | |
} | |
protected List<String> loadFileFromDisk() throws GitException { | |
try (Stream<String> stream = Files.lines(fullPath)) { | |
return stream.toList(); | |
} catch (IOException e) { | |
throw new GitException(e.getMessage(), e.getCause()); | |
} | |
} | |
protected void saveFileOnDisk(List<String> lines) throws GitException { | |
try { | |
Files.write(fullPath, (Iterable<String>) lines.stream()::iterator); | |
} catch (IOException e) { | |
throw new GitException(e.getMessage(), e.getCause()); | |
} | |
} | |
protected void setContentImmediately(byte[] content) throws GitException { | |
try { | |
Files.write(fullPath, content); | |
} catch (IOException e) { | |
throw new GitException(e.getMessage(), e.getCause()); | |
} | |
} | |
/** | |
* If file did not exist, then it stores new file in the filesystem | |
* @param fileBytes | |
* @throws GitException | |
*/ | |
protected void save(byte[] fileBytes) throws GitException { | |
if (!Files.exists(fullPath)) { | |
try { | |
Files.createFile(fullPath); | |
setContentImmediately(fileBytes); | |
} catch (IOException e) { | |
throw new GitException(e.getMessage(), e.getCause()); | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package ru.hse.mit.git.components.fs; | |
import java.nio.file.Path; | |
import org.jetbrains.annotations.NotNull; | |
import ru.hse.mit.git.GitException; | |
import ru.hse.mit.git.components.utils.MiniGitUtils; | |
public class BlobFile extends AbstractEditableFile { | |
private final byte[] fileBytes; | |
public BlobFile(Path fullPathToDir, byte @NotNull [] fileBytes) { | |
this.filename = MiniGitUtils.getHashFromBytes(fileBytes); | |
this.fullPath = Path.of(fullPathToDir.toString(), filename); | |
this.fileBytes = fileBytes; | |
} | |
public void save() throws GitException { | |
save(fileBytes); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package ru.hse.mit.git.components.graph; | |
import java.util.Optional; | |
public class BlobNode extends Node { | |
public BlobNode(String nodeName, String hash) { | |
super(nodeName, NodeType.BLOB_NODE); | |
this.hash = Optional.of(hash); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package ru.hse.mit.git.components.fs; | |
import java.io.IOException; | |
import java.nio.file.Files; | |
import java.nio.file.Path; | |
import java.time.LocalDateTime; | |
import java.time.OffsetDateTime; | |
import java.time.format.DateTimeFormatter; | |
import java.util.Arrays; | |
import java.util.List; | |
import org.jetbrains.annotations.NotNull; | |
import ru.hse.mit.git.GitException; | |
import ru.hse.mit.git.components.utils.MiniGitUtils; | |
public class CommitFile extends AbstractEditableFile { | |
private final String author; | |
private final OffsetDateTime date; | |
private final String message; | |
private final String rootNodeHash; | |
private final String parentCommitHash; | |
public CommitFile(Path fullPath, @NotNull String rootNodeHash, @NotNull String parentCommitHash, @NotNull String author, @NotNull OffsetDateTime date, @NotNull String message) { | |
String content = getCommitFileContent(rootNodeHash, parentCommitHash, author, date, message); | |
this.filename = MiniGitUtils.getHashFromBytes(content.getBytes()); | |
this.fullPath = Path.of(fullPath.toString(), filename); | |
this.rootNodeHash = rootNodeHash; | |
this.parentCommitHash = parentCommitHash; | |
this.date = date; | |
this.message = message; | |
this.author = author; | |
} | |
public CommitFile(String hash, Path fullPath, @NotNull String rootNodeHash, @NotNull String parentCommitHash, @NotNull String author, @NotNull OffsetDateTime date, @NotNull String message) { | |
this.filename = hash; | |
this.fullPath = Path.of(fullPath.toString(), filename); | |
this.rootNodeHash = rootNodeHash; | |
this.parentCommitHash = parentCommitHash; | |
this.date = date; | |
this.message = message; | |
this.author = author; | |
} | |
public String getParentCommitHash() { | |
return parentCommitHash; | |
} | |
public String getRootNodeHash() { | |
return rootNodeHash; | |
} | |
public static CommitFile load(Path fullPath, String hash) throws GitException { | |
try { | |
List<String> lines = Files.readAllLines(Path.of(fullPath.toString(), hash)); | |
// root tree hash | |
String rootNodeHash = lines.get(0).split(" ")[1]; | |
// parent commit hash | |
String parentCommitHash = ""; | |
String[] parentCommitHashLine = lines.get(1).split(" "); | |
if (parentCommitHashLine.length > 1) { | |
parentCommitHash = parentCommitHashLine[1]; | |
} | |
// author | |
String author = lines.get(2).split(" ")[1]; | |
// date | |
DateTimeFormatter formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME; | |
OffsetDateTime date = OffsetDateTime.parse(lines.get(3).split(" ")[1], formatter); | |
// message | |
String message = lines.get(4).replaceFirst("message ", ""); | |
return new CommitFile(hash, fullPath, rootNodeHash, parentCommitHash, author, date, message); | |
} catch (IOException e) { | |
throw new GitException(e.getMessage(), e.getCause()); | |
} | |
} | |
public void save() throws GitException { | |
save(getCommitFileContent(rootNodeHash, parentCommitHash, author, date, message).getBytes()); | |
} | |
public String getInfo() { | |
return "Commit " + filename + System.lineSeparator() | |
+ "Author: " + author + System.lineSeparator() | |
+ "Date: " + date.toString() + System.lineSeparator() | |
+ System.lineSeparator() + message + System.lineSeparator(); | |
} | |
private String getCommitFileContent(String rootNodeHash, String parentCommitHash, String author, OffsetDateTime date, String message) { | |
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX"); | |
String formattedDate = date.format(formatter); | |
return | |
"tree " + rootNodeHash + System.lineSeparator() + | |
"parent " + parentCommitHash + System.lineSeparator() + | |
"author " + author + System.lineSeparator() + | |
"date " + formattedDate + System.lineSeparator() + | |
"message " + message; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package ru.hse.mit.git; | |
import java.io.PrintStream; | |
import java.util.List; | |
import org.jetbrains.annotations.NotNull; | |
public class GitCliImpl implements GitCli { | |
private PrintStream outputStream = System.out; | |
private final MiniGit git; | |
public GitCliImpl(String workingDir) { | |
git = new MiniGit(workingDir); | |
} | |
@Override | |
public void runCommand(@NotNull String command, @NotNull List<@NotNull String> arguments) | |
throws GitException { | |
String gitOutput = ""; | |
switch (command) { | |
case GitConstants.INIT -> gitOutput = git.init(); | |
case GitConstants.ADD -> gitOutput = git.add(arguments); | |
case GitConstants.RM -> gitOutput = git.rm(arguments); | |
case GitConstants.STATUS -> gitOutput = git.status(); | |
case GitConstants.COMMIT -> { | |
checkExactArguments(command, arguments, 1, List.of("message")); | |
gitOutput = git.commit(arguments.get(0)); | |
} | |
case GitConstants.RESET -> { | |
checkExactArguments(command, arguments, 1, | |
List.of("to_revision: HEAD~N | branch name | commit hash")); | |
String toRevision = arguments.get(0); | |
if (toRevision.startsWith("HEAD~")) { | |
checkHeadShiftArgumentCorrectness(command, toRevision); | |
gitOutput = git.reset(getHeadShiftArgumentValue(toRevision)); | |
} else { | |
gitOutput = git.reset(toRevision); | |
} | |
} | |
case GitConstants.LOG -> { | |
if (arguments.isEmpty()) { | |
gitOutput = git.log(); | |
} else { | |
checkExactArguments(command, arguments, 1, | |
List.of("from_revision: HEAD~N | branch name | commit hash")); | |
String fromRevision = arguments.get(0); | |
if (fromRevision.startsWith("HEAD~")) { | |
checkHeadShiftArgumentCorrectness(command, fromRevision); | |
gitOutput = git.log(getHeadShiftArgumentValue(fromRevision)); | |
} else { | |
gitOutput = git.log(fromRevision); | |
} | |
} | |
} | |
case GitConstants.CHECKOUT -> { | |
if (arguments.size() > 1) { | |
String firstArgument = arguments.get(0); | |
if (!firstArgument.equals("--")) { | |
throw new GitException("Command '" + command | |
+ "' with multiple arguments expects filenames enumeration starting with '--'"); | |
} | |
gitOutput = git.checkout(arguments.subList(1, arguments.size())); | |
} else { | |
checkExactArguments(command, arguments, 1, | |
List.of("revision: HEAD~N | branch name | commit hash")); | |
String fromRevision = arguments.get(0); | |
if (fromRevision.startsWith("HEAD~")) { | |
checkHeadShiftArgumentCorrectness(command, fromRevision); | |
gitOutput = git.checkout(getHeadShiftArgumentValue(fromRevision)); | |
} else { | |
gitOutput = git.checkout(fromRevision); | |
} | |
} | |
} | |
case GitConstants.BRANCH_CREATE -> { | |
checkExactArguments(command, arguments, 1, List.of("branch")); | |
String branchName = arguments.get(0); | |
gitOutput = git.createBranch(branchName); | |
} | |
case GitConstants.SHOW_BRANCHES -> { | |
gitOutput = git.showBranches(); | |
} | |
case GitConstants.BRANCH_REMOVE -> { | |
checkExactArguments(command, arguments, 1, List.of("branch")); | |
String branchName = arguments.get(0); | |
gitOutput = git.removeBranch(branchName); | |
} | |
case GitConstants.MERGE -> { | |
checkExactArguments(command, arguments, 1, List.of("branch")); | |
String branchName = arguments.get(0); | |
gitOutput = git.merge(branchName); | |
} | |
default -> throw new GitException("Unknown command: '" + command + "'"); | |
} | |
outputStream.print(gitOutput); | |
} | |
@Override | |
public void setOutputStream(@NotNull PrintStream outputStream) { | |
this.outputStream = outputStream; | |
} | |
@Override | |
public @NotNull String getRelativeRevisionFromHead(int n) throws GitException { | |
return git.getRelativeRevisionFromHead(n); | |
} | |
private void checkExactArguments(String command, List<String> args, int requiredArgsCount, List<String> argsDescriptions) throws GitException { | |
if (args.size() != requiredArgsCount) { | |
StringBuilder errorMessage = new StringBuilder(); | |
errorMessage | |
.append("Command '") | |
.append(command) | |
.append("' must be followed by exactly ") | |
.append(requiredArgsCount) | |
.append(" argument(s): "); | |
for (String description : argsDescriptions) { | |
errorMessage.append("[").append(description).append("]"); | |
} | |
throw new GitException(errorMessage.toString()); | |
} | |
} | |
private void checkHeadShiftArgumentCorrectness(String command, String revision) throws GitException { | |
String shiftNumber = revision.substring(5); // removing "HEAD~" from string | |
try { | |
int res = Integer.parseInt(shiftNumber); | |
if (res < 0) { | |
throw new Exception(); | |
} | |
} | |
catch (Exception e) { | |
throw new GitException( | |
"Command '" + command + | |
"' accepts argument in HEAD~N format with N being non-negative integer, but got: '" + | |
shiftNumber + "'" | |
); | |
} | |
} | |
private int getHeadShiftArgumentValue(String revision) { | |
String shiftNumber = revision.substring(5); // removing "HEAD~" from string | |
return Integer.parseInt(shiftNumber); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package ru.hse.mit.git.components.fs; | |
import java.io.File; | |
import java.io.IOException; | |
import java.nio.file.Files; | |
import java.nio.file.Path; | |
import java.util.List; | |
import ru.hse.mit.git.GitException; | |
import ru.hse.mit.git.components.graph.TreeNode; | |
public class HeadFile extends AbstractEditableFile { | |
private final Path branchesDir; | |
private final Path commitsDir; | |
private final Path treesDir; | |
public HeadFile(String filename, Path fullPath, Path branchesPath, Path commitsPath, Path treesPath) { | |
this.filename = filename; | |
this.fullPath = fullPath; | |
this.branchesDir = branchesPath; | |
this.commitsDir = commitsPath; | |
this.treesDir = treesPath; | |
} | |
public String getCurrentBranch() throws GitException { | |
if (isDetached()) { | |
return getCurrentCommitHash(); | |
} | |
else { | |
String line = loadFileFromDisk().get(0); | |
return line.split(" ")[1]; | |
} | |
} | |
public void setCurrentBranch(String branchName) throws GitException { | |
if (!branchExists(branchName)) { | |
throw new GitException("Branch '" + branchName + "' does not exist"); | |
} | |
String content = "ref " + branchName; | |
setContentImmediately(content.getBytes()); | |
} | |
public String getCurrentCommitHash() throws GitException { | |
if (isDetached()) { | |
try { | |
return Files.readString(fullPath); | |
} catch (IOException e) { | |
throw new GitException(e); | |
} | |
} | |
else { | |
try { | |
File branch = getBranchFile(); | |
return Files.readString(branch.toPath()); | |
} catch (IOException e) { | |
throw new GitException(e.getMessage(), e.getCause()); | |
} | |
} | |
} | |
public void setCurrentCommitAsDetached(String commitHash) throws GitException { | |
setContentImmediately(commitHash.getBytes()); | |
} | |
public void setCurrentCommit(String commitHash) throws GitException { | |
if (!commitExists(commitHash)) { | |
throw new GitException("Commit '" + commitHash + "' does not exist"); | |
} | |
if (isDetached()) { | |
setContentImmediately(commitHash.getBytes()); | |
} | |
else { | |
try { | |
File branch = getBranchFile(); | |
Files.write(branch.toPath(), commitHash.getBytes()); | |
} catch (IOException e) { | |
throw new GitException(e); | |
} | |
} | |
} | |
public String getShiftedCommitHash(int shift) throws GitException { | |
String currentCommitHash = getCurrentCommitHash(); | |
int n = shift; | |
while (n > 0) { | |
if (currentCommitHash.isEmpty()) { | |
break; | |
} | |
CommitFile commit = CommitFile.load(commitsDir, currentCommitHash); | |
currentCommitHash = commit.getParentCommitHash(); | |
n--; | |
} | |
if (currentCommitHash.isEmpty()) { | |
throw new GitException("No commit found associated with HEAD~" + shift); | |
} | |
return currentCommitHash; | |
} | |
public TreeNode loadTree() throws GitException { | |
if (getCurrentCommitHash().isEmpty()) { | |
return TreeNode.createRoot(); | |
} | |
CommitFile currentCommit = CommitFile.load(commitsDir, getCurrentCommitHash()); | |
return TreeNode.loadTree( | |
treesDir, | |
currentCommit.getRootNodeHash() | |
); | |
} | |
public boolean branchExists(String branchName) { | |
return Files.exists(Path.of(branchesDir.toString(), branchName)); | |
} | |
public boolean commitExists(String commitHash) { | |
return Files.exists(Path.of(commitsDir.toString(), commitHash)); | |
} | |
public boolean isDetached() throws GitException { | |
List<String> lines = loadFileFromDisk(); | |
List<String> data = List.of(lines.get(0).split(" ")); | |
if (data.size() == 1) { | |
return true; | |
} | |
return false; | |
} | |
private File getBranchFile() throws GitException { | |
List<String> lines = loadFileFromDisk(); | |
List<String> data = List.of(lines.get(0).split(" ")); | |
if (data.size() != 2) { | |
throw new GitException("HEAD is not on branch: " + lines.get(0)); | |
} | |
String branchName = data.get(1); | |
File branchFile = Path.of(branchesDir.toString(), branchName).toFile(); | |
if (!branchFile.exists()) { | |
try { | |
Files.createFile(branchFile.toPath()); | |
} catch (IOException e) { | |
throw new GitException(e.getMessage(), e.getCause()); | |
} | |
} | |
return branchFile; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package ru.hse.mit.git.components.fs; | |
import java.io.IOException; | |
import java.nio.file.Files; | |
import java.nio.file.Path; | |
import java.util.ArrayList; | |
import java.util.Collection; | |
import java.util.HashMap; | |
import java.util.HashSet; | |
import java.util.List; | |
import java.util.Map; | |
import java.util.Map.Entry; | |
import java.util.Set; | |
import java.util.stream.Stream; | |
import ru.hse.mit.git.GitException; | |
import ru.hse.mit.git.components.utils.MiniGitUtils; | |
public class IndexFile extends AbstractEditableFile { | |
public enum FileStatus { | |
MODIFIED, | |
NEW, | |
DELETED | |
} | |
private final Map<String, String> entries = new HashMap<>(); | |
public IndexFile(String filename, Path fullPath) { | |
this.filename = filename; | |
this.fullPath = fullPath; | |
} | |
public Set<Entry<String, String>> getEntries() { | |
return entries.entrySet(); | |
} | |
public void load() throws GitException { | |
entries.clear(); | |
List<String> lines = loadFileFromDisk(); | |
for (String line : lines) { | |
String[] keyVal = line.split(" "); | |
entries.put(keyVal[0], keyVal[1]); | |
} | |
} | |
public void save() throws GitException { | |
List<String> lines = entries.entrySet().stream().map(entry -> entry.getKey() + " " + entry.getValue()).toList(); | |
saveFileOnDisk(lines); | |
} | |
public void addEntry(String entryName, String entryHash) { | |
entries.put(entryName, entryHash); | |
} | |
public void removeEntry(String entryName) { | |
entries.remove(entryName); | |
} | |
public void setEntries(Map<String, String> newEtries) { | |
entries.clear(); | |
entries.putAll(newEtries); | |
} | |
public void saveTrackedFilesToWorkingDir(Path workingDir, Path blobsDir) throws GitException { | |
for (var entry : entries.entrySet()) { | |
String filename = entry.getKey(); | |
String hash = entry.getValue(); | |
try { | |
Path path = Path.of(workingDir.toString(), filename); | |
if (!Files.exists(path)) { | |
Files.createDirectories(path.getParent()); | |
Files.createFile(path); | |
} | |
Files.write(path, Files.readAllBytes(Path.of(blobsDir.toString(), hash))); | |
} catch (IOException e) { | |
throw new GitException(e.getMessage(), e.getCause()); | |
} | |
} | |
} | |
public Map<FileStatus, List<String>> getUntrackedFiles(Path workingDir, Path exclude) throws GitException { | |
Collection<String> indexFiles = entries.keySet(); | |
Collection<String> workingDirFiles = getFilesFromWorkingDirectory(workingDir, exclude); | |
Map<FileStatus, List<String>> result = Map.of( | |
FileStatus.MODIFIED, new ArrayList<>(), | |
FileStatus.NEW, new ArrayList<>(), | |
FileStatus.DELETED, new ArrayList<>() | |
); | |
Set<String> allFiles = new HashSet<>(); | |
allFiles.addAll(indexFiles); | |
allFiles.addAll(workingDirFiles); | |
for (String filename : allFiles) { | |
boolean indexFileContains = indexFiles.contains(filename); | |
boolean workingDirContains = workingDirFiles.contains(filename); | |
if (indexFileContains && workingDirContains) { | |
byte[] workingDirFileBytes = MiniGitUtils.getFileBytes(Path.of(workingDir.toString(), filename)); | |
String workingDirFileHash = MiniGitUtils.getHashFromBytes(workingDirFileBytes); | |
if (!entries.get(filename).equals(workingDirFileHash)) { | |
result.get(FileStatus.MODIFIED).add(filename); | |
} | |
} | |
else if (!indexFileContains && workingDirContains) { | |
result.get(FileStatus.NEW).add(filename); | |
} | |
else if (indexFileContains && !workingDirContains) { | |
result.get(FileStatus.DELETED).add(filename); | |
} | |
} | |
return result; | |
} | |
public Map<FileStatus, List<String>> getReadyToCommitFiles(Map<String, String> repoEntries) { | |
Collection<String> indexFiles = entries.keySet(); | |
Collection<String> repoFiles = repoEntries.keySet(); | |
Map<FileStatus, List<String>> result = Map.of( | |
FileStatus.MODIFIED, new ArrayList<>(), | |
FileStatus.NEW, new ArrayList<>(), | |
FileStatus.DELETED, new ArrayList<>() | |
); | |
Collection<String> allFiles = new HashSet<>(); | |
allFiles.addAll(indexFiles); | |
allFiles.addAll(repoFiles); | |
for (String filename : allFiles) { | |
boolean indexContainsFile = indexFiles.contains(filename); | |
boolean repoContainsFile = repoFiles.contains(filename); | |
if (indexContainsFile && repoContainsFile) { | |
String indexHash = entries.get(filename); | |
String repoHash = repoEntries.get(filename); | |
if (!indexHash.equals(repoHash)) { | |
result.get(FileStatus.MODIFIED).add(filename); | |
} | |
} | |
else if (indexContainsFile && !repoContainsFile) { | |
result.get(FileStatus.NEW).add(filename); | |
} | |
else if (!indexContainsFile && repoContainsFile) { | |
result.get(FileStatus.DELETED).add(filename); | |
} | |
} | |
return result; | |
} | |
public Set<String> getFilesFromWorkingDirectory(Path workingDir, Path exclude) throws GitException { | |
Set<String> result = new HashSet<>(); | |
try (Stream<Path> files = Files.walk(workingDir)) { | |
files.forEach(path -> { | |
if (path.toFile().isDirectory() || path.toString().startsWith(exclude.toString())) { | |
return; | |
} | |
String trimmedPath = path.toString().replace(workingDir.toString(), ""); | |
if (trimmedPath.isEmpty()) { | |
return; | |
} | |
trimmedPath = trimmedPath.substring(1).replace("\\", "/"); | |
result.add(trimmedPath); | |
}); | |
return result; | |
} catch (IOException e) { | |
throw new GitException(e.getMessage(), e.getCause()); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package ru.hse.mit.git; | |
import java.io.File; | |
import java.io.IOException; | |
import java.nio.file.DirectoryStream; | |
import java.nio.file.Files; | |
import java.nio.file.Path; | |
import java.security.MessageDigest; | |
import java.time.OffsetDateTime; | |
import java.util.Arrays; | |
import java.util.HashMap; | |
import java.util.List; | |
import java.util.Map; | |
import java.util.Objects; | |
import java.util.Set; | |
import java.util.function.Function; | |
import java.util.stream.Collectors; | |
import java.util.stream.Stream; | |
import org.apache.commons.io.FileUtils; | |
import org.jetbrains.annotations.NotNull; | |
import ru.hse.mit.git.components.fs.BlobFile; | |
import ru.hse.mit.git.components.fs.CommitFile; | |
import ru.hse.mit.git.components.fs.HeadFile; | |
import ru.hse.mit.git.components.fs.IndexFile; | |
import ru.hse.mit.git.components.fs.IndexFile.FileStatus; | |
import ru.hse.mit.git.components.graph.TreeNode; | |
import ru.hse.mit.git.components.utils.MiniGitUtils; | |
public class MiniGit { | |
private final String workingDir; | |
private boolean isInitialized = false; | |
private static final String REPOSITORY_DIR = ".mini-git"; | |
private static final String BLOBS_DIR = "blobs"; | |
private static final String TREES_DIR = "trees"; | |
private static final String COMMITS_DIR = "commits"; | |
private static final String BRANCHES_DIR = "branches"; | |
private static final String HEAD_FILE = "HEAD"; | |
private static final String INDEX_FILE = "INDEX"; | |
private static final String MASTER_BRANCH = "master"; | |
private final HeadFile headFile; | |
private final IndexFile indexFile; | |
public MiniGit(String workingDir) { | |
this.workingDir = workingDir; | |
this.headFile = new HeadFile( | |
HEAD_FILE, | |
getFullPathFromRepository(HEAD_FILE), | |
getFullPathFromRepository(BRANCHES_DIR), | |
getFullPathFromRepository(COMMITS_DIR), | |
getFullPathFromRepository(TREES_DIR) | |
); | |
this.indexFile = new IndexFile(INDEX_FILE, getFullPathFromRepository(INDEX_FILE)); | |
} | |
public String init() throws GitException { | |
try { | |
// directories | |
Files.createDirectories(getFullPathFromRepository(BLOBS_DIR)); | |
Files.createDirectories(getFullPathFromRepository(TREES_DIR)); | |
Files.createDirectories(getFullPathFromRepository(COMMITS_DIR)); | |
Files.createDirectories(getFullPathFromRepository(BRANCHES_DIR)); | |
// files | |
Files.createFile(getFullPathFromRepository(HEAD_FILE)); | |
Files.createFile(getFullPathFromRepository(INDEX_FILE)); | |
// set master branch to head by default | |
Files.createFile(getFullPathFromRepository(BRANCHES_DIR, MASTER_BRANCH)); | |
headFile.setCurrentBranch(MASTER_BRANCH); | |
isInitialized = true; | |
} catch (IOException e) { | |
throw new GitException(e.getMessage(), e.getCause()); | |
} | |
return "Project initialized" + System.lineSeparator(); | |
} | |
public String add(@NotNull List<String> entryNames) throws GitException { | |
checkInitialized(); | |
indexFile.load(); | |
Map<String, File> pureFiles = getPureFiles(entryNames); | |
for (Map.Entry<String, File> fileEntry : pureFiles.entrySet()) { | |
byte[] fileBytes; | |
try { | |
fileBytes = FileUtils.readFileToByteArray(fileEntry.getValue()); | |
} catch (IOException e) { | |
throw new GitException(e.getMessage(), e.getCause()); | |
} | |
// create blob | |
BlobFile blob = new BlobFile(getFullPathFromRepository(BLOBS_DIR), fileBytes); | |
blob.save(); | |
// add entry to index file | |
indexFile.addEntry(fileEntry.getKey(), /* hash */ blob.getFilename()); | |
} | |
indexFile.save(); | |
return "Add completed successful" + System.lineSeparator(); | |
} | |
public String rm(@NotNull List<String> entryNames) throws GitException { | |
checkInitialized(); | |
indexFile.load(); | |
Map<String, File> pureFiles = getPureFiles(entryNames); | |
for (Map.Entry<String, File> fileEntry : pureFiles.entrySet()) { | |
// remove entry from index file | |
indexFile.removeEntry(fileEntry.getKey()); | |
} | |
indexFile.save(); | |
return "Rm completed successful" + System.lineSeparator(); | |
} | |
public String status() throws GitException { | |
checkInitialized(); | |
if (headFile.isDetached()) { | |
// actually, my implementation will show correct diff for the detached HEAD as well | |
// I will stick to the provided tests, though | |
return "Error while performing status: Head is detached" + System.lineSeparator(); | |
} | |
indexFile.load(); | |
Map<IndexFile.FileStatus, List<String>> untrackedFiles = indexFile.getUntrackedFiles( | |
getFullPathFromWorkingDirectory(), | |
getFullPathFromRepository() | |
); | |
Map<IndexFile.FileStatus, List<String>> readyToCommitFiles = indexFile.getReadyToCommitFiles( | |
headFile.loadTree().getBlobs() | |
); | |
StringBuilder content = new StringBuilder(); | |
content.append("Current branch is '").append(headFile.getCurrentBranch()).append("'").append(System.lineSeparator()); | |
boolean untrackedAdded = appendStatus(content, untrackedFiles, "Untracked files:"); | |
boolean readyToCommitAdded = appendStatus(content, readyToCommitFiles, "Ready to commit:"); | |
if (!untrackedAdded && !readyToCommitAdded) { | |
content.append("Everything up to date").append(System.lineSeparator()); | |
} | |
return content.toString(); | |
} | |
public String commit(@NotNull String message) throws GitException { | |
checkInitialized(); | |
indexFile.load(); | |
TreeNode root = TreeNode.createRoot(); | |
for (var entry : indexFile.getEntries()) { | |
String path = entry.getKey(); | |
String blobHash = entry.getValue(); | |
List<String> names = List.of(path.split("/")); | |
root.addChildren(0, names, blobHash); | |
} | |
root.buildGraph(); | |
root.saveGraph(getFullPathFromRepository(TREES_DIR)); | |
CommitFile commit = new CommitFile( | |
getFullPathFromRepository(COMMITS_DIR), | |
root.getHash().get(), | |
headFile.getCurrentCommitHash(), | |
"Dimechik", | |
OffsetDateTime.now(), | |
message | |
); | |
commit.save(); | |
headFile.setCurrentCommit(commit.getFilename()); | |
return "Files committed" + System.lineSeparator(); | |
} | |
/** | |
* | |
* @param checkpointName either commit hash or branch name (eg. master) | |
*/ | |
public String reset(@NotNull String checkpointName) throws GitException { | |
checkInitialized(); | |
return resetImpl(checkpointName); | |
} | |
public String reset(int stepsBackwardsFromHead) throws GitException { | |
checkInitialized(); | |
return resetImpl(headFile.getShiftedCommitHash(stepsBackwardsFromHead)); | |
} | |
private String resetImpl(String checkpointName) throws GitException { | |
// Update HEAD file | |
// branch | |
if (Files.exists(getFullPathFromRepository(BRANCHES_DIR, checkpointName))) { | |
headFile.setCurrentBranch(checkpointName); | |
} | |
// commit | |
else if (Files.exists(getFullPathFromRepository(COMMITS_DIR, checkpointName))) { | |
headFile.setCurrentCommit(checkpointName); | |
} | |
else { | |
throw new GitException("Neither commit, nor branch exists named '" + checkpointName + "'"); | |
} | |
// Update index file | |
TreeNode root = headFile.loadTree(); | |
indexFile.setEntries(root.getBlobs()); | |
indexFile.save(); | |
// Update working directory | |
clearWorkingDirectory(); | |
indexFile.saveTrackedFilesToWorkingDir(getFullPathFromWorkingDirectory(), getFullPathFromRepository(BLOBS_DIR)); | |
return "Reset successful" + System.lineSeparator(); | |
} | |
public String log() throws GitException { | |
checkInitialized(); | |
return logImpl(headFile.getCurrentCommitHash()); | |
} | |
public String log(String commitHash) throws GitException { | |
checkInitialized(); | |
return logImpl(commitHash); | |
} | |
public String log(int stepsBackwardsFromHead) throws GitException { | |
checkInitialized(); | |
return logImpl(headFile.getShiftedCommitHash(stepsBackwardsFromHead)); | |
} | |
private String logImpl(String startingCommit) throws GitException { | |
StringBuilder result = new StringBuilder(); | |
Path fullPathToCommitsDir = getFullPathFromRepository(COMMITS_DIR); | |
String currentCommitHash = startingCommit; | |
while (!currentCommitHash.equals("")) { | |
CommitFile commit = CommitFile.load(fullPathToCommitsDir, currentCommitHash); | |
result.append(commit.getInfo()).append(System.lineSeparator()); | |
currentCommitHash = commit.getParentCommitHash(); | |
} | |
return result.toString(); | |
} | |
/** | |
* | |
* @param checkpointName either commit hash or branch name (eg. master) | |
*/ | |
public String checkout(String checkpointName) throws GitException { | |
checkInitialized(); | |
return checkoutImpl(checkpointName); | |
} | |
public String checkout(int stepsBackwardsFromHead) throws GitException { | |
checkInitialized(); | |
return checkoutImpl(headFile.getShiftedCommitHash(stepsBackwardsFromHead)); | |
} | |
public String checkoutImpl(String checkpointName) throws GitException { | |
TreeNode prevRoot = headFile.loadTree(); | |
// Update HEAD file | |
// branch | |
if (Files.exists(getFullPathFromRepository(BRANCHES_DIR, checkpointName))) { | |
headFile.setCurrentBranch(checkpointName); | |
} | |
// commit | |
else if (Files.exists(getFullPathFromRepository(COMMITS_DIR, checkpointName))) { | |
headFile.setCurrentCommitAsDetached(checkpointName); | |
} | |
else { | |
throw new GitException("Neither commit, nor branch exists named '" + checkpointName + "'"); | |
} | |
// Update index file | |
TreeNode root = headFile.loadTree(); | |
Map<String, String> checkoutBlobs = root.getBlobs(); | |
indexFile.setEntries(checkoutBlobs); | |
indexFile.save(); | |
// add new files from checkout commit/branch | |
indexFile.saveTrackedFilesToWorkingDir(getFullPathFromWorkingDirectory(), getFullPathFromRepository(BLOBS_DIR)); | |
// remove all files from working directory, that are in `prevRoot` but not in `root` | |
Map<String, String> prevBlobs = prevRoot.getBlobs(); | |
for (var entry : prevBlobs.entrySet()) { | |
String filename = entry.getKey(); | |
if (!checkoutBlobs.containsKey(filename)) { | |
try { | |
Files.delete(getFullPathFromWorkingDirectory(filename)); | |
} catch (IOException e) { | |
throw new GitException(e.getMessage(), e.getCause()); | |
} | |
} | |
} | |
removeEmptyWorkingDirectories(getFullPathFromWorkingDirectory()); | |
return "Checkout completed successful" + System.lineSeparator(); | |
} | |
public String checkout(List<String> filenames) throws GitException { | |
checkInitialized(); | |
TreeNode root = headFile.loadTree(); | |
Map<String, String> blobs = root.getBlobs(); | |
for (String filename : filenames) { | |
if (!blobs.containsKey(filename)) { | |
throw new GitException("Filename '" + filename + "' is not recognized by git"); | |
} | |
} | |
for (String filename : filenames) { | |
String hash = blobs.get(filename); | |
try { | |
byte[] fileBytes = Files.readAllBytes(getFullPathFromRepository(BLOBS_DIR, hash)); | |
Files.write( | |
getFullPathFromWorkingDirectory(filename), | |
fileBytes | |
); | |
} catch (IOException e) { | |
throw new GitException(e.getMessage(), e.getCause()); | |
} | |
} | |
return "Checkout completed successful" + System.lineSeparator(); | |
} | |
public String createBranch(String branchName) throws GitException { | |
if (headFile.branchExists(branchName)) { | |
throw new GitException("Branch '" + branchName + "' already exists"); | |
} | |
try { | |
Path branchFile = Files.createFile(getFullPathFromRepository(BRANCHES_DIR, branchName)); | |
Files.write(branchFile, headFile.getCurrentCommitHash().getBytes()); | |
// the return message of this command is pretty weird, considering that we checkout new branch by default | |
// according to the tests | |
headFile.setCurrentBranch(branchName); | |
} catch (IOException e) { | |
throw new GitException(e.getMessage(), e.getCause()); | |
} | |
return | |
"Branch new-feature created successfully" + System.lineSeparator() + | |
"You can checkout it with 'checkout " + branchName + "'" + System.lineSeparator(); | |
} | |
public String showBranches() throws GitException { | |
StringBuilder content = new StringBuilder(); | |
content.append("Available branches:").append(System.lineSeparator()); | |
List<Path> result; | |
try (Stream<Path> walk = Files.walk(getFullPathFromRepository(BRANCHES_DIR))) { | |
result = walk.filter(Files::isRegularFile) | |
.collect(Collectors.toList()); | |
} catch (IOException e) { | |
throw new GitException(e.getMessage(), e.getCause()); | |
} | |
result.forEach(path -> { | |
content.append(path.getFileName().toString()).append(System.lineSeparator()); | |
}); | |
return content.toString(); | |
} | |
public String removeBranch(String branchName) throws GitException { | |
if (!headFile.branchExists(branchName)) { | |
throw new GitException("Branch '" + branchName + "' does not exist"); | |
} | |
if (headFile.getCurrentBranch().equals(branchName)) { | |
throw new GitException("Cannot remove current branch"); | |
} | |
try { | |
Files.delete(getFullPathFromRepository(BRANCHES_DIR, branchName)); | |
} catch (IOException e) { | |
throw new GitException(e.getMessage(), e.getCause()); | |
} | |
return "Branch " + branchName + " removed successfully" + System.lineSeparator(); | |
} | |
public String merge(String otherBranchName) throws GitException { | |
// Нуууу, я почитал, как это делать: | |
// за 1 балл, пожалуй, откажусь + уже нет ментальных сил это реализовывать(( | |
throw new UnsupportedOperationException(); | |
} | |
public String getRelativeRevisionFromHead(int n) throws GitException { | |
return headFile.getShiftedCommitHash(n); | |
} | |
/*------- Helper methods ------------------------------------*/ | |
private Path getFullPathFromRepository(String... paths) { | |
String prefix = Path.of(workingDir, REPOSITORY_DIR).toString(); | |
return Path.of(prefix, paths); | |
} | |
private Path getFullPathFromWorkingDirectory(String... paths) { | |
return Path.of(workingDir, paths); | |
} | |
private void checkInitialized() throws GitException { | |
if (!isInitialized) { | |
throw new GitException("MiniGit repository not initialized"); | |
} | |
} | |
/** | |
* Flattens the directories that {@code entryNames contain}, meaning goes inside of them while no directories left, | |
* the file names are built respectively | |
*/ | |
private Map<String, File> getPureFiles(List<String> entryNames) throws GitException { | |
Map<String, File> files = new HashMap<>(); | |
for (String name : entryNames) { | |
files.put(name, getFullPathFromWorkingDirectory(name).toFile()); | |
} | |
// TODO: Do I have to check for files existance? | |
// MiniGitUtils.checkFilesExists(files.values().stream().toList()); | |
return collectPureFiles("", files); | |
} | |
/** | |
* For every {@code File} that is a directory goes inside of it recursively and collects pure files from it | |
*/ | |
private Map<String, File> collectPureFiles(String prefix, Map<String, File> entryFiles) { | |
Map<String, File> result = new HashMap<>(); | |
entryFiles.forEach((name, file) -> { | |
if (file.isDirectory()) { | |
if (file.getName().equals(REPOSITORY_DIR)) { | |
return; | |
} | |
result.putAll(collectPureFiles( | |
prefix + file.getName() + "/", | |
Arrays.stream(Objects.requireNonNull(file.listFiles())).collect(Collectors.toMap( | |
File::getName, | |
Function.identity() | |
)) | |
)); | |
} | |
else { | |
// replacing './' symbol in path, so that we will not have extra tree-nodes for '.' folders | |
if (prefix.isEmpty()) { | |
result.put(name.replace("./", ""), file); | |
} | |
else { | |
result.put((prefix + name).replace("./", ""), file); | |
} | |
} | |
}); | |
return result; | |
} | |
/** | |
* | |
* @param content | |
* @return {@code true} if some data was appended, otherwise {@code false} | |
*/ | |
private boolean appendStatus( | |
StringBuilder content, | |
Map<FileStatus, List<String>> files, | |
String title | |
) { | |
String filesNew = collectFilesStatus(files.get(FileStatus.NEW)); | |
String filesModified = collectFilesStatus(files.get(FileStatus.MODIFIED)); | |
String filesDeleted = collectFilesStatus(files.get(FileStatus.DELETED)); | |
boolean filesAdded = false; | |
if (filesModified.length() + filesNew.length() + filesDeleted.length() != 0) { | |
filesAdded = true; | |
content.append(title).append(System.lineSeparator()).append(System.lineSeparator()); | |
if (!filesNew.isEmpty()) { | |
content.append("New files:").append(System.lineSeparator()) | |
.append(filesNew).append(System.lineSeparator()); | |
} | |
if (!filesModified.isEmpty()) { | |
content.append("Modified files:").append(System.lineSeparator()) | |
.append(filesModified).append(System.lineSeparator()); | |
} | |
if (!filesDeleted.isEmpty()) { | |
content.append("Removed files:").append(System.lineSeparator()) | |
.append(filesDeleted).append(System.lineSeparator()); | |
} | |
} | |
return filesAdded; | |
} | |
private String collectFilesStatus(List<String> files) { | |
StringBuilder result = new StringBuilder(); | |
for (String filename : files) { | |
result.append("\t").append(filename).append(System.lineSeparator()); | |
} | |
return result.toString(); | |
} | |
private void clearWorkingDirectory() throws GitException { | |
try (DirectoryStream<Path> stream = Files.newDirectoryStream(getFullPathFromWorkingDirectory())) { | |
for (Path entry : stream) { | |
if (entry.toString().contains(REPOSITORY_DIR)) { | |
continue; | |
} | |
if (Files.isDirectory(entry)) { | |
deleteRecursively(entry); | |
} | |
else { | |
Files.delete(entry); | |
} | |
} | |
} catch (IOException e) { | |
throw new GitException(e.getMessage(), e.getCause()); | |
} | |
} | |
private void deleteRecursively(Path path) throws IOException { | |
if (Files.isDirectory(path)) { | |
try (DirectoryStream<Path> stream = Files.newDirectoryStream(path)) { | |
for (Path file : stream) { | |
deleteRecursively(file); | |
} | |
} | |
} | |
Files.deleteIfExists(path); | |
} | |
private void removeEmptyWorkingDirectories(Path currentDir) throws GitException { | |
try (DirectoryStream<Path> stream = Files.newDirectoryStream(currentDir)) { | |
for (Path entry : stream) { | |
if (entry.toString().contains(REPOSITORY_DIR)) { | |
continue; | |
} | |
if (Files.isDirectory(entry)) { | |
removeEmptyWorkingDirectories(entry); | |
} | |
} | |
if (countEntries(currentDir) == 0) { | |
Files.delete(currentDir); | |
} | |
} catch (IOException e) { | |
throw new GitException(e.getMessage(), e.getCause()); | |
} | |
} | |
private int countEntries(Path directory) throws GitException { | |
int count = 0; | |
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory)) { | |
for (Path entry : stream) { | |
count++; | |
} | |
} catch (IOException e) { | |
throw new GitException(e.getMessage(), e.getCause()); | |
} | |
return count; | |
} | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package ru.hse.mit.git.components.utils; | |
import java.io.File; | |
import java.io.IOException; | |
import java.nio.file.Path; | |
import java.security.MessageDigest; | |
import java.security.NoSuchAlgorithmException; | |
import java.util.List; | |
import org.apache.commons.io.FileUtils; | |
import ru.hse.mit.git.GitException; | |
public class MiniGitUtils { | |
public static String getHashFromBytes(byte[] bytes) { | |
// set encryption algorithm | |
MessageDigest md = null; | |
try { | |
md = MessageDigest.getInstance("SHA-1"); | |
} catch (NoSuchAlgorithmException e) { | |
throw new RuntimeException(e); | |
} | |
byte[] hashBytes = md.digest(bytes); | |
// Convert the hash bytes to a hexadecimal string | |
StringBuilder sb = new StringBuilder(); | |
for (byte b : hashBytes) { | |
sb.append(String.format("%02x", b)); | |
} | |
return sb.toString(); | |
} | |
public static void checkFileExists(File file) throws GitException { | |
if (!file.exists()) { | |
throw new GitException("File '" + file.getName() + "' does not exists"); | |
} | |
} | |
public static void checkFilesExists(List<File> files) throws GitException { | |
for (File file : files) { | |
if (!file.exists()) { | |
throw new GitException("File '" + file.getName() + "' does not exists"); | |
} | |
} | |
} | |
public static byte[] getFileBytes(Path fullPath) throws GitException { | |
File file = new File(fullPath.toString()); | |
checkFileExists(file); | |
try { | |
return FileUtils.readFileToByteArray(file); | |
} catch (IOException e) { | |
throw new GitException(e.getMessage(), e.getCause()); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package ru.hse.mit.git.components.graph; | |
import java.util.Optional; | |
public class Node { | |
public enum NodeType { | |
TREE_NODE, | |
BLOB_NODE | |
} | |
protected Optional<String> hash = Optional.empty(); | |
protected String nodeName; | |
protected NodeType type; | |
public Node(String nodeName, NodeType type) { | |
this.nodeName = nodeName; | |
this.type = type; | |
} | |
public String getName() { | |
return nodeName; | |
} | |
public Optional<String> getHash() { | |
return hash; | |
} | |
public NodeType getType() { | |
return type; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package ru.hse.mit.git.components.fs; | |
import java.nio.file.Path; | |
import org.jetbrains.annotations.NotNull; | |
import ru.hse.mit.git.GitException; | |
import ru.hse.mit.git.components.utils.MiniGitUtils; | |
public class TreeFile extends AbstractEditableFile { | |
private final byte[] fileBytes; | |
public TreeFile(Path fullPathToDir, byte @NotNull [] fileBytes) { | |
this.filename = MiniGitUtils.getHashFromBytes(fileBytes); | |
this.fullPath = Path.of(fullPathToDir.toString(), filename); | |
this.fileBytes = fileBytes; | |
} | |
public void save() throws GitException { | |
save(fileBytes); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package ru.hse.mit.git.components.graph; | |
import java.io.IOException; | |
import java.nio.file.Files; | |
import java.nio.file.Path; | |
import java.security.MessageDigest; | |
import java.util.HashMap; | |
import java.util.List; | |
import java.util.Map; | |
import java.util.Optional; | |
import java.util.stream.Stream; | |
import ru.hse.mit.git.GitException; | |
import ru.hse.mit.git.components.fs.TreeFile; | |
import ru.hse.mit.git.components.utils.MiniGitUtils; | |
public class TreeNode extends Node { | |
private final Map<String, Node> children = new HashMap<>(); | |
private String content = ""; | |
public static TreeNode createRoot() { | |
return new TreeNode(""); | |
} | |
public TreeNode(String nodeName) { | |
super(nodeName, NodeType.TREE_NODE); | |
} | |
/** | |
* | |
* @return blobs entries: { filename in working directory, hash } | |
*/ | |
public Map<String, String> getBlobs() { | |
return getBlobs(""); | |
} | |
private Map<String, String> getBlobs(String namePrefix) { | |
Map<String, String> result = new HashMap<>(); | |
for (var childEntry : children.entrySet()) { | |
String childName = childEntry.getKey(); | |
Node childNode = childEntry.getValue(); | |
switch (childNode.getType()) { | |
case TREE_NODE -> result.putAll(((TreeNode)childNode).getBlobs(namePrefix + childName + "/")); | |
case BLOB_NODE -> result.put(namePrefix + childName, childNode.getHash().get()); | |
} | |
} | |
return result; | |
} | |
public static TreeNode loadTree(Path pathToTreesDir, String hash) throws GitException { | |
return loadTree(pathToTreesDir, hash, ""); | |
} | |
private static TreeNode loadTree(Path pathToTreesDir, String hash, String name) throws GitException { | |
TreeNode node = new TreeNode(name); | |
node.hash = Optional.of(hash); | |
try(Stream<String> stream = Files.lines(Path.of(pathToTreesDir.toString(), hash))) { | |
List<String> lines = stream.toList(); | |
for (String line : lines) { | |
String[] data = line.split(" "); | |
String childType = data[0]; | |
String childHash = data[1]; | |
String childName = data[2]; | |
Node child; | |
if (childType.equals("tree")) { | |
child = loadTree(pathToTreesDir, childHash, childName); | |
} | |
else { | |
child = new BlobNode(childName, childHash); | |
} | |
node.children.put(childName, child); | |
} | |
} catch (IOException e) { | |
throw new GitException(e.getMessage(), e.getCause()); | |
} | |
return node; | |
} | |
public void addChildren(int index, List<String> names, String blobHash) { | |
if (index == names.size() - 1) { | |
addBlob(names.get(index), blobHash); | |
return; | |
} | |
String treeNodeName = names.get(index); | |
if (!children.containsKey(treeNodeName)) { | |
children.put(treeNodeName, new TreeNode(treeNodeName)); | |
} | |
((TreeNode)children.get(treeNodeName)).addChildren(index + 1, names, blobHash); | |
} | |
public void addBlob(String name, String hash) { | |
if (!children.containsKey(name)) { | |
children.put(name, new BlobNode(name, hash)); | |
} | |
} | |
public void buildGraph() { | |
StringBuilder content = new StringBuilder(); | |
for (var childEntry : children.entrySet()) { | |
String childName = childEntry.getKey(); | |
Node childNode = childEntry.getValue(); | |
switch (childNode.type) { | |
case TREE_NODE -> { | |
TreeNode treeNode = (TreeNode) childNode; | |
treeNode.buildGraph(); | |
String hash = treeNode.getHash().get(); | |
content | |
.append("tree ") | |
.append(hash).append(" ") | |
.append(childName) | |
.append(System.lineSeparator()); | |
} | |
case BLOB_NODE -> { | |
content | |
.append("blob ") | |
.append(childNode.getHash().get()).append(" ") | |
.append(childName) | |
.append(System.lineSeparator()); | |
} | |
} | |
} | |
String currentNodeHash = MiniGitUtils.getHashFromBytes(content.toString().getBytes()); | |
this.hash = Optional.of(currentNodeHash); | |
this.content = content.toString(); | |
} | |
public void saveGraph(Path fullPath) throws GitException { | |
for (var childEntry : children.entrySet()) { | |
Node childNode = childEntry.getValue(); | |
// only save tree-nodes, because blob-nodes are already saved | |
if (childNode.type == NodeType.TREE_NODE) { | |
TreeNode treeNode = (TreeNode)childNode; | |
treeNode.saveGraph(fullPath); | |
} | |
} | |
TreeFile treeFile = new TreeFile(fullPath, content.getBytes()); | |
treeFile.save(); | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment