diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 2ca6797..ec1e41b 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -2,6 +2,7 @@ + \ No newline at end of file diff --git a/src/main/java/eu/m724/blog/BlogBuilder.java b/src/main/java/eu/m724/blog/BlogBuilder.java index 31c2144..c5dac9b 100644 --- a/src/main/java/eu/m724/blog/BlogBuilder.java +++ b/src/main/java/eu/m724/blog/BlogBuilder.java @@ -6,17 +6,19 @@ package eu.m724.blog; +import eu.m724.blog.compress.CommonsCompressor; +import eu.m724.blog.compress.CompressException; import eu.m724.blog.compress.FileCompressor; import eu.m724.blog.compress.NoSuchAlgorithmException; import eu.m724.blog.object.Article; import eu.m724.blog.object.Feed; import eu.m724.blog.object.RenderOptions; import eu.m724.blog.object.Site; +import eu.m724.blog.server.Server; import eu.m724.blog.template.TemplateRenderer; -import org.apache.commons.compress.compressors.CompressorException; +import eu.m724.blog.vc.GitVersionControl; +import eu.m724.blog.vc.VersionControl; import org.apache.commons.io.file.PathUtils; -import org.eclipse.jgit.api.Git; -import org.eclipse.jgit.lib.RepositoryBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,18 +31,18 @@ import java.util.stream.Collectors; /** * The {@code BlogBuilder} class facilitates building a static blog by managing templates, - * assets, articles, and rendering output files. It uses a Git repository as the + * assets, articles, and rendering output files. It uses a version control (Git etc.) repository as the * source for the blog's content and configuration. */ public class BlogBuilder { private static final Logger LOGGER = LoggerFactory.getLogger(BlogBuilder.class); - private final Git git; + private final Site site; + private final RenderOptions renderOptions; + private final VersionControl versionControl; private final Path workingDirectory; - private Site site; private TemplateRenderer template; - private RenderOptions renderOptions; private Minifier minifier; private Path templateDirectory; @@ -50,12 +52,14 @@ public class BlogBuilder { /** * Constructs a {@link BlogBuilder} instance using the provided Git repository. * - * @param git the Git repository to be used for the blog. + * @param versionControl the version control repository to be used for the blog. */ - public BlogBuilder(Git git) { - this.git = git; + private BlogBuilder(Site site, RenderOptions renderOptions, Path workingDirectory, VersionControl versionControl) { + this.site = site; + this.renderOptions = renderOptions; + this.versionControl = versionControl; - this.workingDirectory = git.getRepository().getDirectory().toPath().getParent(); + this.workingDirectory = workingDirectory; this.templateDirectory = workingDirectory.resolve("template"); this.outputDirectory = workingDirectory.resolve("generated_out"); } @@ -64,19 +68,19 @@ public class BlogBuilder { * Creates a new {@link BlogBuilder} instance for the specified working directory. * The directory is expected to be a Git repository. * - * @param workingDirectory the root path of the blog, which must contain a Git repository. + * @param directory the root path of the blog, which must be a Git repository. * @return a {@link BlogBuilder} instance * @throws IOException if there is an error accessing the Git repository */ - public static BlogBuilder fromPath(Path workingDirectory) throws IOException { - var repository = new RepositoryBuilder() - .setGitDir(workingDirectory.resolve(".git").toFile()) - .build(); - var git = new Git(repository); + public static BlogBuilder fromGitRepository(Path directory) throws IOException { + return BlogBuilder.fromDirectory(directory, new GitVersionControl(directory)); + } - // + public static BlogBuilder fromDirectory(Path directory, VersionControl versionControl) throws IOException { + var site = Site.fromConfig(directory.resolve("site.yml")); + var renderOptions = RenderOptions.fromConfig(directory.resolve("render.yml")); - return new BlogBuilder(git); + return new BlogBuilder(site, renderOptions, directory, versionControl); } /** @@ -130,14 +134,7 @@ public class BlogBuilder { * @throws IOException if an I/O error occurs */ public void build() throws IOException { - LOGGER.debug("Loading site..."); - if (site == null) - this.site = Site.fromConfig(workingDirectory.resolve("site.yml")); - - if (renderOptions == null) - this.renderOptions = RenderOptions.fromConfig(workingDirectory.resolve("render.yml")); - - if (this.site.minify()) { + if (renderOptions.minify()) { try { this.minifier = new Minifier(); } catch (NoClassDefFoundError e) { @@ -193,8 +190,7 @@ public class BlogBuilder { continue; } - path = articleDirectory.relativize(path); - var article = Article.fromFile(git, path); + var article = Article.fromFile(versionControl, path); if (article.draft() && !renderDrafts) { LOGGER.info("[Article {}] Draft. Ignoring, because you didn't specify to render drafts.", path.getFileName()); @@ -202,7 +198,7 @@ public class BlogBuilder { } var render = template.renderArticle(article); - var outFile = outputDirectory.resolve("article").resolve(path); + var outFile = outputDirectory.resolve("article").resolve(path.getFileName()); try { Files.createDirectory(outFile.getParent()); @@ -229,8 +225,9 @@ public class BlogBuilder { if (renderOptions.remapAssets()) { var assetHashes = CacheBuster.copyTree(userAssetsDir, outputUserAssetsDir); + // This looks weird, but it's necessary if we want to use a single map assetHashes.forEach((k, v) -> { - fileHashes.put("assets/" + k, v); // TODO this feels like a hack + fileHashes.put("assets/" + k, v); }); } else { FileUtils.copyTree(userAssetsDir, outputUserAssetsDir); @@ -239,8 +236,9 @@ public class BlogBuilder { if (renderOptions.remapTemplateStatic()) { var templateStaticHashes = CacheBuster.copyTree(templateStaticDir, outputTemplateStaticDir); + // This looks weird, but it's necessary if we want to use a single map templateStaticHashes.forEach((k, v) -> { - fileHashes.put("static/" + k, v); // TODO this feels like a hack + fileHashes.put("static/" + k, v); }); } else { FileUtils.copyTree(templateStaticDir, outputTemplateStaticDir); @@ -259,7 +257,7 @@ public class BlogBuilder { for (var algorithm : renderOptions.compress()) { try { - var compressor = new FileCompressor(algorithm); + var compressor = new CommonsCompressor(algorithm); compressors.add(compressor); } catch (NoSuchAlgorithmException e) { LOGGER.error("No such compression algorithm, ignoring: {}", e.getAlgorithm()); @@ -276,8 +274,8 @@ public class BlogBuilder { for (var path : tree) { try { compressor.compress(path); - } catch (CompressorException e) { - LOGGER.error("Error compressing \"{}\" to \"{}\": {}", path, compressor.getAlgorithm(), e.getMessage()); + } catch (CompressException e) { + LOGGER.error("Error compressing \"{}\" to \"{}\": {}", path, compressor.getAlgorithmName(), e.getMessage()); } } } diff --git a/src/main/java/eu/m724/blog/Main.java b/src/main/java/eu/m724/blog/Main.java index ff5b471..e7a3e67 100644 --- a/src/main/java/eu/m724/blog/Main.java +++ b/src/main/java/eu/m724/blog/Main.java @@ -38,7 +38,10 @@ public class Main { var start = System.nanoTime(); - var builder = BlogBuilder.fromPath(workingDirectory) + + LOGGER.debug("Loading site..."); + + var builder = BlogBuilder.fromGitRepository(workingDirectory) .templateDirectory(templateDirectory) .outputDirectory(outputDirectory) .renderDrafts(renderDrafts); diff --git a/src/main/java/eu/m724/blog/YamlLoader.java b/src/main/java/eu/m724/blog/YamlLoader.java new file mode 100644 index 0000000..c0c3624 --- /dev/null +++ b/src/main/java/eu/m724/blog/YamlLoader.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2025 blog-software-java developers + * blog-software-java is licensed under the GNU General Public License. See the LICENSE.md file + * in the project root for the full license text. + */ + +package eu.m724.blog; + +import org.snakeyaml.engine.v2.api.Load; +import org.snakeyaml.engine.v2.api.LoadSettings; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; + +public class YamlLoader { + private static final Load LOAD = new Load(LoadSettings.builder().build()); + + public static Map loadMap(Path file) throws IOException { + try (var inputStream = Files.newInputStream(file)) { + return (Map) LOAD.loadFromInputStream(inputStream); + } + } +} diff --git a/src/main/java/eu/m724/blog/compress/CommonsCompressor.java b/src/main/java/eu/m724/blog/compress/CommonsCompressor.java new file mode 100644 index 0000000..582a9ae --- /dev/null +++ b/src/main/java/eu/m724/blog/compress/CommonsCompressor.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2025 blog-software-java developers + * blog-software-java is licensed under the GNU General Public License. See the LICENSE.md file + * in the project root for the full license text. + */ + +package eu.m724.blog.compress; + +import org.apache.commons.compress.compressors.CompressorException; +import org.apache.commons.compress.compressors.CompressorStreamFactory; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.Files; +import java.nio.file.Path; + +public class CommonsCompressor extends FileCompressor { + /** + * Constructs a {@link FileCompressor} instance using the provided algorithm. + * + * @param algorithm The algorithm, for valid names see {@link CompressorStreamFactory} + * @throws NoSuchAlgorithmException If that algorithm is unavailable or the name is invalid. + */ + public CommonsCompressor(String algorithm) throws NoSuchAlgorithmException { + super(algorithm); + + try { + var os = new CompressorStreamFactory().createCompressorOutputStream(algorithm, OutputStream.nullOutputStream()); + os.close(); + } catch (NoClassDefFoundError | CompressorException e) { + throw new NoSuchAlgorithmException(algorithm, e); + } catch (IOException e) { + throw new RuntimeException("Unexpected IOException closing test output stream", e); + } + } + + @Override + public void compress(Path file) throws IOException, CompressException { + var destination = file.resolveSibling(file.getFileName() + "." + getAlgorithmName()); + compress(file, destination); + } + + @Override + public void compress(Path source, Path destination) throws IOException, CompressException { + if (Files.exists(destination)) + throw new FileAlreadyExistsException(destination.toString()); + + try ( + var outputStream = new CompressorStreamFactory() + .createCompressorOutputStream(getAlgorithmName(), Files.newOutputStream(destination)) + ) { + Files.copy(source, outputStream); + } catch (CompressorException e) { + throw new CompressException(e); + } + } +} diff --git a/src/main/java/eu/m724/blog/compress/CompressException.java b/src/main/java/eu/m724/blog/compress/CompressException.java new file mode 100644 index 0000000..f337a00 --- /dev/null +++ b/src/main/java/eu/m724/blog/compress/CompressException.java @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2025 blog-software-java developers + * blog-software-java is licensed under the GNU General Public License. See the LICENSE.md file + * in the project root for the full license text. + */ + +package eu.m724.blog.compress; + +// TODO really runtime exception? +public class CompressException extends RuntimeException { + public CompressException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/eu/m724/blog/compress/FileCompressor.java b/src/main/java/eu/m724/blog/compress/FileCompressor.java index 2824b25..e6c31f5 100644 --- a/src/main/java/eu/m724/blog/compress/FileCompressor.java +++ b/src/main/java/eu/m724/blog/compress/FileCompressor.java @@ -6,53 +6,20 @@ package eu.m724.blog.compress; -import org.apache.commons.compress.compressors.CompressorException; -import org.apache.commons.compress.compressors.CompressorStreamFactory; - import java.io.IOException; -import java.io.OutputStream; import java.nio.file.*; -public class FileCompressor { - private final String algorithm; +public abstract class FileCompressor { + private final String algorithmName; - /** - * Constructs a {@link FileCompressor} instance using the provided algorithm. - * - * @param algorithm The algorithm, for valid names see {@link CompressorStreamFactory} - * @throws NoSuchAlgorithmException If that algorithm is unavailable or the name is invalid. - */ - public FileCompressor(String algorithm) throws NoSuchAlgorithmException { - this.algorithm = algorithm; - - try { - var os = new CompressorStreamFactory().createCompressorOutputStream(algorithm, OutputStream.nullOutputStream()); - os.close(); - } catch (NoClassDefFoundError | CompressorException e) { - throw new NoSuchAlgorithmException(algorithm, e); - } catch (IOException e) { - throw new RuntimeException("Unexpected IOException closing test output stream", e); - } + protected FileCompressor(String algorithmName) { + this.algorithmName = algorithmName; } - public void compress(Path source) throws IOException, CompressorException { - var destination = source.resolveSibling(source.getFileName() + "." + algorithm); - compress(source, destination); + public String getAlgorithmName() { + return algorithmName; } - public void compress(Path source, Path destination) throws IOException, CompressorException { - if (Files.exists(destination)) - throw new FileAlreadyExistsException(destination.toString()); - - try ( - var outputStream = new CompressorStreamFactory() - .createCompressorOutputStream(algorithm, Files.newOutputStream(destination)) - ) { - Files.copy(source, outputStream); - } - } - - public String getAlgorithm() { - return algorithm; - } + abstract public void compress(Path file) throws IOException, CompressException; + abstract public void compress(Path source, Path destination) throws IOException, CompressException; } diff --git a/src/main/java/eu/m724/blog/object/Article.java b/src/main/java/eu/m724/blog/object/Article.java index 83c8bfb..d43a75a 100644 --- a/src/main/java/eu/m724/blog/object/Article.java +++ b/src/main/java/eu/m724/blog/object/Article.java @@ -6,8 +6,8 @@ package eu.m724.blog.object; -import org.eclipse.jgit.api.Git; -import org.eclipse.jgit.api.errors.GitAPIException; +import eu.m724.blog.vc.VersionControl; +import eu.m724.blog.vc.VersionControlException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -56,17 +56,16 @@ public record Article( * The method extracts metadata properties, content, and versioning information * based on the Git history of the file. * - * @param git the Git repository used to retrieve versioning and commit information + * @param versionControl the version control repository used to retrieve versioning and commit information * @param path the relative path to the file within the "articles" directory * @return a {@link Article} object populated with data extracted from the specified file * @throws IOException if an error occurs during file reading */ - public static Article fromFile(Git git, Path path) throws IOException { + public static Article fromFile(VersionControl versionControl, Path path) throws IOException { /* read properties before filtering */ var slug = path.getFileName().toString().split("\\.")[0]; - path = Path.of("articles").resolve(path); - var lines = Files.readAllLines(git.getRepository().getDirectory().toPath().getParent().resolve(path)); + var lines = Files.readAllLines(path); var properties = new HashMap(); @@ -121,18 +120,18 @@ public record Article( ZonedDateTime modifiedAt = Instant.ofEpochMilli(0).atZone(ZoneOffset.UTC); try { - for (var commit : git.log().addPath(path.toString()).call()) { - createdBy = commit.getAuthorIdent().getName(); - createdAt = Instant.ofEpochSecond(commit.getCommitTime()).atZone(ZoneOffset.UTC); + for (var change : versionControl.getChanges(path)) { + createdBy = change.author(); + createdAt = change.time(); if (revisions++ == 0) { modifiedBy = createdBy; modifiedAt = createdAt; } } - } catch (GitAPIException e) { + } catch (VersionControlException e) { draft = true; - LOGGER.warn("[Article {}] Draft because of a Git exception: {}", slug, e.getMessage()); + LOGGER.warn("[Article {}] Draft because of a VC exception: {}", slug, e.getMessage()); } return new Article(slug, title, summary, draft, revisions, createdBy, createdAt, modifiedBy, modifiedAt, custom, content); diff --git a/src/main/java/eu/m724/blog/object/RenderOptions.java b/src/main/java/eu/m724/blog/object/RenderOptions.java index a8c8222..b64503e 100644 --- a/src/main/java/eu/m724/blog/object/RenderOptions.java +++ b/src/main/java/eu/m724/blog/object/RenderOptions.java @@ -6,41 +6,61 @@ package eu.m724.blog.object; -import org.snakeyaml.engine.v2.api.Load; -import org.snakeyaml.engine.v2.api.LoadSettings; +import eu.m724.blog.YamlLoader; import java.io.IOException; -import java.nio.file.Files; import java.nio.file.Path; -import java.util.ArrayList; import java.util.List; -import java.util.Map; +/** + * Options that are relevant to rendering, not about the {@link Site}. + * + * @param compress list of compression algorithms to compress output + * @param remapAssets whether to remap user assets, add a unique hash to the file name to bypass cache + * @param remapTemplateStatic whether to remap template static assets, add a unique hash to the file name to bypass cache + * @param minify whether to minify output + */ public record RenderOptions( List compress, // TODO rename? + boolean remapAssets, boolean remapTemplateStatic, - boolean remapAssets + + boolean minify ) { + private static final RenderOptions DEFAULT = new RenderOptions( + List.of("gz", "zstd"), + false, + true, + true + ); + /** - * Creates a {@link Site} object by reading and parsing the configuration file at the specified path.
+ * Creates a {@link Site} object by reading and parsing the configuration file at the specified file.
* The configuration file must be a JSON file. * - * @param path the path to the configuration file + * @param file the file to the configuration file * @return a {@link Site} object initialized with the data from the configuration file * @throws IOException if an error occurs during file reading */ - public static RenderOptions fromConfig(Path path) throws IOException { - var load = new Load(LoadSettings.builder().build()); - var yaml = (Map) load.loadFromInputStream(Files.newInputStream(path)); + public static RenderOptions fromConfig(Path file) throws IOException { + var yaml = YamlLoader.loadMap(file); + + /* ---- */ + + List compress = (List) yaml.getOrDefault("compress", DEFAULT.compress()); - List compress = (List) yaml.getOrDefault("compress", new ArrayList<>()); - boolean remapTemplateStatic = (boolean) yaml.getOrDefault("remapTemplateStatic", true); // assets are not remapped by default, because they might be hotlinked - boolean remapAssets = (boolean) yaml.getOrDefault("remapAssets", false); + boolean remapAssets = (boolean) yaml.getOrDefault("remapAssets", DEFAULT.remapAssets()); + + boolean remapTemplateStatic = (boolean) yaml.getOrDefault("remapTemplateStatic", DEFAULT.remapTemplateStatic()); + + var minify = (boolean) yaml.getOrDefault("minify", DEFAULT.minify()); + + /* ---- */ return new RenderOptions( - compress, remapTemplateStatic, remapAssets + compress, remapTemplateStatic, remapAssets, minify ); } } diff --git a/src/main/java/eu/m724/blog/object/Site.java b/src/main/java/eu/m724/blog/object/Site.java index 2b767f8..47b4e67 100644 --- a/src/main/java/eu/m724/blog/object/Site.java +++ b/src/main/java/eu/m724/blog/object/Site.java @@ -6,11 +6,9 @@ package eu.m724.blog.object; -import org.snakeyaml.engine.v2.api.Load; -import org.snakeyaml.engine.v2.api.LoadSettings; +import eu.m724.blog.YamlLoader; import java.io.IOException; -import java.nio.file.Files; import java.nio.file.Path; import java.util.Map; @@ -21,7 +19,6 @@ import java.util.Map; * @param baseUrl the base URL of the site * @param directory The directory that the site is installed into. Like "https://example.com/blog" turns to "/blog" * @param templateArticles whether to parse articles with Pebble templating - * @param minify whether to minify HTML, CSS, JS, etc. * @param custom a map of additional custom properties */ public record Site( @@ -31,28 +28,33 @@ public record Site( String directory, boolean templateArticles, - boolean minify, Map custom ) { + private static final Site DEFAULT = new Site( + "Misconfigured blog", + "/", + "/", + false, + Map.of() + ); + /** * Creates a {@link Site} object by reading and parsing the configuration file at the specified path.
* The configuration file must be a JSON file. * - * @param path the path to the configuration file + * @param file the path to the configuration file * @return a {@link Site} object initialized with the data from the configuration file * @throws IOException if an error occurs during file reading */ - public static Site fromConfig(Path path) throws IOException { - var load = new Load(LoadSettings.builder().build()); - var yaml = (Map) load.loadFromInputStream(Files.newInputStream(path)); + public static Site fromConfig(Path file) throws IOException { + var yaml = YamlLoader.loadMap(file); - String name = (String) yaml.get("name"); - String baseUrl = (String) yaml.getOrDefault("baseUrl", "/"); - var templateArticles = (boolean) yaml.getOrDefault("templateArticles", false); - var minify = (boolean) yaml.getOrDefault("minify", true); + String name = (String) yaml.getOrDefault("name", DEFAULT.name()); + String baseUrl = (String) yaml.getOrDefault("baseUrl", DEFAULT.baseUrl()); + var templateArticles = (boolean) yaml.getOrDefault("templateArticles", DEFAULT.templateArticles()); - String directory = "/"; + String directory = DEFAULT.directory(); if (baseUrl != null) { var temp = baseUrl.substring(baseUrl.indexOf(':') + 3); var slashIndex = temp.indexOf('/'); @@ -62,7 +64,7 @@ public record Site( } return new Site( - name, baseUrl, directory, templateArticles, minify, yaml + name, baseUrl, directory, templateArticles, yaml ); } } diff --git a/src/main/java/eu/m724/blog/Server.java b/src/main/java/eu/m724/blog/server/Server.java similarity index 99% rename from src/main/java/eu/m724/blog/Server.java rename to src/main/java/eu/m724/blog/server/Server.java index 2d02176..16b55cf 100644 --- a/src/main/java/eu/m724/blog/Server.java +++ b/src/main/java/eu/m724/blog/server/Server.java @@ -4,7 +4,7 @@ * in the project root for the full license text. */ -package eu.m724.blog; +package eu.m724.blog.server; import com.sun.net.httpserver.HttpServer; import com.sun.net.httpserver.SimpleFileServer; diff --git a/src/main/java/eu/m724/blog/vc/Change.java b/src/main/java/eu/m724/blog/vc/Change.java new file mode 100644 index 0000000..d7cb004 --- /dev/null +++ b/src/main/java/eu/m724/blog/vc/Change.java @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2025 blog-software-java developers + * blog-software-java is licensed under the GNU General Public License. See the LICENSE.md file + * in the project root for the full license text. + */ + +package eu.m724.blog.vc; + +import java.time.ZonedDateTime; + +public record Change( + String author, + ZonedDateTime time +) { } diff --git a/src/main/java/eu/m724/blog/vc/GitVersionControl.java b/src/main/java/eu/m724/blog/vc/GitVersionControl.java new file mode 100644 index 0000000..631803b --- /dev/null +++ b/src/main/java/eu/m724/blog/vc/GitVersionControl.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2025 blog-software-java developers + * blog-software-java is licensed under the GNU General Public License. See the LICENSE.md file + * in the project root for the full license text. + */ + +package eu.m724.blog.vc; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.lib.RepositoryBuilder; + +import java.io.IOException; +import java.nio.file.Path; +import java.time.Instant; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.List; + +public class GitVersionControl implements VersionControl { + private final Git git; + private final Path directory; + + public GitVersionControl(Path directory) throws IOException { + this.directory = directory; + + var repository = new RepositoryBuilder() + .setGitDir(directory.resolve(".git").toFile()) + .build(); + + this.git = new Git(repository); + } + + @Override + public List getChanges(Path path) throws VersionControlException { + path = path.normalize(); + if (path.startsWith(directory)) { + path = directory.relativize(path); + } + + var changes = new ArrayList(); + + try { + for (var commit : git.log().addPath(path.toString()).call()) { + var author = commit.getAuthorIdent().getName(); + var time = Instant.ofEpochSecond(commit.getCommitTime()).atZone(ZoneOffset.UTC); + + changes.add(new Change(author, time)); + } + } catch (GitAPIException e) { + throw new VersionControlException(e); + } + + return changes; + } +} diff --git a/src/main/java/eu/m724/blog/vc/VersionControl.java b/src/main/java/eu/m724/blog/vc/VersionControl.java new file mode 100644 index 0000000..31c3bdd --- /dev/null +++ b/src/main/java/eu/m724/blog/vc/VersionControl.java @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2025 blog-software-java developers + * blog-software-java is licensed under the GNU General Public License. See the LICENSE.md file + * in the project root for the full license text. + */ + +package eu.m724.blog.vc; + +import java.nio.file.Path; +import java.util.List; + +public interface VersionControl { + List getChanges(Path path) throws VersionControlException; +} + diff --git a/src/main/java/eu/m724/blog/vc/VersionControlException.java b/src/main/java/eu/m724/blog/vc/VersionControlException.java new file mode 100644 index 0000000..e3a251d --- /dev/null +++ b/src/main/java/eu/m724/blog/vc/VersionControlException.java @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2025 blog-software-java developers + * blog-software-java is licensed under the GNU General Public License. See the LICENSE.md file + * in the project root for the full license text. + */ + +package eu.m724.blog.vc; + +// TODO runtime exception really? +public class VersionControlException extends RuntimeException { + public VersionControlException(Throwable cause) { + // TODO maybe I need to raise message here too + super(cause); + } +}