From e7af8907b4f724f70bbb64b49d19556f6839cf7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Kr=C3=BChlmann?= Date: Fri, 13 Mar 2026 15:21:53 +0100 Subject: [PATCH] Add GitInfoService --- .../imgfloat/service/GitInfoService.java | 94 +++++++------------ .../git/GitBinaryCommitInfoSource.java | 54 +++++++++++ .../imgfloat/service/git/GitCommitInfo.java | 3 + .../service/git/GitCommitInfoSource.java | 8 ++ .../git/GitPropertiesCommitInfoSource.java | 38 ++++++++ .../imgfloat/service/GitInfoServiceTest.java | 80 ++++++++++++++++ 6 files changed, 216 insertions(+), 61 deletions(-) create mode 100644 src/main/java/dev/kruhlmann/imgfloat/service/git/GitBinaryCommitInfoSource.java create mode 100644 src/main/java/dev/kruhlmann/imgfloat/service/git/GitCommitInfo.java create mode 100644 src/main/java/dev/kruhlmann/imgfloat/service/git/GitCommitInfoSource.java create mode 100644 src/main/java/dev/kruhlmann/imgfloat/service/git/GitPropertiesCommitInfoSource.java create mode 100644 src/test/java/dev/kruhlmann/imgfloat/service/GitInfoServiceTest.java diff --git a/src/main/java/dev/kruhlmann/imgfloat/service/GitInfoService.java b/src/main/java/dev/kruhlmann/imgfloat/service/GitInfoService.java index 3d69692..7d4b86a 100644 --- a/src/main/java/dev/kruhlmann/imgfloat/service/GitInfoService.java +++ b/src/main/java/dev/kruhlmann/imgfloat/service/GitInfoService.java @@ -1,12 +1,10 @@ package dev.kruhlmann.imgfloat.service; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.util.Properties; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import dev.kruhlmann.imgfloat.service.git.GitCommitInfo; +import dev.kruhlmann.imgfloat.service.git.GitCommitInfoSource; +import java.util.List; +import java.util.Objects; +import java.util.Optional; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; @@ -15,18 +13,16 @@ import org.springframework.util.StringUtils; public class GitInfoService { private static final String FALLBACK_GIT_SHA = "unknown"; - private static final Logger LOG = LoggerFactory.getLogger(GitInfoService.class); private final String commitSha; private final String shortCommitSha; private final String commitUrlPrefix; - public GitInfoService(@Value("${IMGFLOAT_COMMIT_URL_PREFIX:}") String commitUrlPrefix) { - CommitInfo commitInfo = resolveFromGitProperties(); - if (commitInfo == null) { - commitInfo = resolveFromGitBinary(); - } - + public GitInfoService( + @Value("${IMGFLOAT_COMMIT_URL_PREFIX:}") String commitUrlPrefix, + List commitInfoSources + ) { + GitCommitInfo commitInfo = resolveCommitInfo(commitInfoSources); String full = commitInfo != null ? commitInfo.fullSha() : null; String abbreviated = commitInfo != null ? commitInfo.shortSha() : null; @@ -59,54 +55,20 @@ public class GitInfoService { return StringUtils.hasText(commitUrlPrefix); } - private CommitInfo resolveFromGitProperties() { - try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream("git.properties")) { - if (inputStream == null) { - return null; - } - Properties properties = new Properties(); - properties.load(inputStream); - String fullSha = normalize(properties.getProperty("git.commit.id")); - String shortSha = normalize(properties.getProperty("git.commit.id.abbrev")); - if (fullSha == null && shortSha == null) { - return null; - } - return new CommitInfo(fullSha, shortSha); - } catch (IOException e) { - LOG.warn("Unable to read git.properties from classpath", e); - return null; - } - } - - private CommitInfo resolveFromGitBinary() { - String fullSha = runGitCommand("rev-parse", "HEAD"); - String shortSha = runGitCommand("rev-parse", "--short", "HEAD"); - if (fullSha == null && shortSha == null) { - return null; - } - return new CommitInfo(fullSha, shortSha); - } - - private String runGitCommand(String... command) { - try { - Process process = new ProcessBuilder(command).redirectErrorStream(true).start(); - try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { - String output = reader.readLine(); - int exitCode = process.waitFor(); - if (exitCode == 0 && output != null && !output.isBlank()) { - return output.trim(); - } - LOG.debug("Git command {} failed with exit code {}", String.join(" ", command), exitCode); - return null; - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - LOG.debug("Thread interrupt during git command {}", String.join(" ", command), e); - return null; - } catch (IOException e) { - LOG.debug("Git command IO error command {}", String.join(" ", command), e); + private GitCommitInfo resolveCommitInfo(List commitInfoSources) { + if (commitInfoSources == null || commitInfoSources.isEmpty()) { return null; } + Optional resolved = commitInfoSources + .stream() + .filter(Objects::nonNull) + .map(GitCommitInfoSource::loadCommitInfo) + .filter(Optional::isPresent) + .map(Optional::get) + .map(this::normalizeCommitInfo) + .filter(Objects::nonNull) + .findFirst(); + return resolved.orElse(null); } private String abbreviate(String value) { @@ -130,5 +92,15 @@ public class GitInfoService { return value; } - private record CommitInfo(String fullSha, String shortSha) {} + private GitCommitInfo normalizeCommitInfo(GitCommitInfo commitInfo) { + if (commitInfo == null) { + return null; + } + String fullSha = normalize(commitInfo.fullSha()); + String shortSha = normalize(commitInfo.shortSha()); + if (fullSha == null && shortSha == null) { + return null; + } + return new GitCommitInfo(fullSha, shortSha); + } } diff --git a/src/main/java/dev/kruhlmann/imgfloat/service/git/GitBinaryCommitInfoSource.java b/src/main/java/dev/kruhlmann/imgfloat/service/git/GitBinaryCommitInfoSource.java new file mode 100644 index 0000000..aead252 --- /dev/null +++ b/src/main/java/dev/kruhlmann/imgfloat/service/git/GitBinaryCommitInfoSource.java @@ -0,0 +1,54 @@ +package dev.kruhlmann.imgfloat.service.git; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +@Component +@Order(1) +public class GitBinaryCommitInfoSource implements GitCommitInfoSource { + + private static final Logger LOG = LoggerFactory.getLogger(GitBinaryCommitInfoSource.class); + + @Override + public Optional loadCommitInfo() { + String fullSha = runGitCommand("rev-parse", "HEAD"); + String shortSha = runGitCommand("rev-parse", "--short", "HEAD"); + if (fullSha == null && shortSha == null) { + return Optional.empty(); + } + return Optional.of(new GitCommitInfo(fullSha, shortSha)); + } + + private String runGitCommand(String... args) { + List command = new ArrayList<>(args.length + 1); + command.add("git"); + command.addAll(List.of(args)); + try { + Process process = new ProcessBuilder(command).redirectErrorStream(true).start(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + String output = reader.readLine(); + int exitCode = process.waitFor(); + if (exitCode == 0 && output != null && !output.isBlank()) { + return output.trim(); + } + LOG.debug("Git command {} failed with exit code {}", String.join(" ", command), exitCode); + return null; + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LOG.debug("Thread interrupt during git command {}", String.join(" ", command), e); + return null; + } catch (IOException e) { + LOG.debug("Git command IO error command {}", String.join(" ", command), e); + return null; + } + } +} diff --git a/src/main/java/dev/kruhlmann/imgfloat/service/git/GitCommitInfo.java b/src/main/java/dev/kruhlmann/imgfloat/service/git/GitCommitInfo.java new file mode 100644 index 0000000..2622c18 --- /dev/null +++ b/src/main/java/dev/kruhlmann/imgfloat/service/git/GitCommitInfo.java @@ -0,0 +1,3 @@ +package dev.kruhlmann.imgfloat.service.git; + +public record GitCommitInfo(String fullSha, String shortSha) {} diff --git a/src/main/java/dev/kruhlmann/imgfloat/service/git/GitCommitInfoSource.java b/src/main/java/dev/kruhlmann/imgfloat/service/git/GitCommitInfoSource.java new file mode 100644 index 0000000..13e35eb --- /dev/null +++ b/src/main/java/dev/kruhlmann/imgfloat/service/git/GitCommitInfoSource.java @@ -0,0 +1,8 @@ +package dev.kruhlmann.imgfloat.service.git; + +import java.util.Optional; + +@FunctionalInterface +public interface GitCommitInfoSource { + Optional loadCommitInfo(); +} diff --git a/src/main/java/dev/kruhlmann/imgfloat/service/git/GitPropertiesCommitInfoSource.java b/src/main/java/dev/kruhlmann/imgfloat/service/git/GitPropertiesCommitInfoSource.java new file mode 100644 index 0000000..1283511 --- /dev/null +++ b/src/main/java/dev/kruhlmann/imgfloat/service/git/GitPropertiesCommitInfoSource.java @@ -0,0 +1,38 @@ +package dev.kruhlmann.imgfloat.service.git; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Optional; +import java.util.Properties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +@Component +@Order(0) +public class GitPropertiesCommitInfoSource implements GitCommitInfoSource { + + private static final Logger LOG = LoggerFactory.getLogger(GitPropertiesCommitInfoSource.class); + + @Override + public Optional loadCommitInfo() { + try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream("git.properties")) { + if (inputStream == null) { + return Optional.empty(); + } + Properties properties = new Properties(); + properties.load(inputStream); + String fullSha = properties.getProperty("git.commit.id"); + String shortSha = properties.getProperty("git.commit.id.abbrev"); + if (!StringUtils.hasText(fullSha) && !StringUtils.hasText(shortSha)) { + return Optional.empty(); + } + return Optional.of(new GitCommitInfo(fullSha, shortSha)); + } catch (IOException e) { + LOG.warn("Unable to read git.properties from classpath", e); + return Optional.empty(); + } + } +} diff --git a/src/test/java/dev/kruhlmann/imgfloat/service/GitInfoServiceTest.java b/src/test/java/dev/kruhlmann/imgfloat/service/GitInfoServiceTest.java new file mode 100644 index 0000000..90758ab --- /dev/null +++ b/src/test/java/dev/kruhlmann/imgfloat/service/GitInfoServiceTest.java @@ -0,0 +1,80 @@ +package dev.kruhlmann.imgfloat.service; + +import static org.assertj.core.api.Assertions.assertThat; + +import dev.kruhlmann.imgfloat.service.git.GitCommitInfo; +import dev.kruhlmann.imgfloat.service.git.GitCommitInfoSource; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Test; + +class GitInfoServiceTest { + + @Test + void usesFirstCommitInfoSource() { + TrackingSource first = new TrackingSource(Optional.of(new GitCommitInfo("full-sha", "short"))); + TrackingSource second = new TrackingSource(Optional.of(new GitCommitInfo("other-sha", "other"))); + + GitInfoService service = new GitInfoService("https://example/commit/", List.of(first, second)); + + assertThat(service.getShortCommitSha()).isEqualTo("short"); + assertThat(service.getCommitUrl()).isEqualTo("https://example/commit/full-sha"); + assertThat(first.calls()).isEqualTo(1); + assertThat(second.calls()).isEqualTo(0); + } + + @Test + void fallsBackToNextSourceWhenFirstIsEmpty() { + TrackingSource first = new TrackingSource(Optional.empty()); + TrackingSource second = new TrackingSource(Optional.of(new GitCommitInfo(null, "abc1234"))); + + GitInfoService service = new GitInfoService("https://example/commit/", List.of(first, second)); + + assertThat(service.getShortCommitSha()).isEqualTo("abc1234"); + assertThat(service.getCommitUrl()).isEqualTo("https://example/commit/abc1234"); + assertThat(first.calls()).isEqualTo(1); + assertThat(second.calls()).isEqualTo(1); + } + + @Test + void abbreviatesShortShaWhenOnlyFullIsProvided() { + GitInfoService service = new GitInfoService( + "https://example/commit/", + List.of(() -> Optional.of(new GitCommitInfo("1234567890", null))) + ); + + assertThat(service.getShortCommitSha()).isEqualTo("1234567"); + assertThat(service.getCommitUrl()).isEqualTo("https://example/commit/1234567890"); + } + + @Test + void hidesCommitUrlWhenPrefixOrShaIsMissing() { + GitInfoService missingPrefix = new GitInfoService(" ", List.of()); + GitInfoService missingSha = new GitInfoService("https://example/commit/", List.of()); + + assertThat(missingPrefix.shouldShowCommitChip()).isFalse(); + assertThat(missingPrefix.getCommitUrl()).isNull(); + assertThat(missingSha.getShortCommitSha()).isEqualTo("unknown"); + assertThat(missingSha.getCommitUrl()).isNull(); + } + + private static final class TrackingSource implements GitCommitInfoSource { + private final Optional payload; + private final AtomicInteger calls = new AtomicInteger(); + + private TrackingSource(Optional payload) { + this.payload = payload; + } + + @Override + public Optional loadCommitInfo() { + calls.incrementAndGet(); + return payload; + } + + private int calls() { + return calls.get(); + } + } +}