Add GitInfoService

This commit is contained in:
2026-03-13 15:21:53 +01:00
parent 0975c039a0
commit e7af8907b4
6 changed files with 216 additions and 61 deletions

View File

@@ -1,12 +1,10 @@
package dev.kruhlmann.imgfloat.service; package dev.kruhlmann.imgfloat.service;
import java.io.BufferedReader; import dev.kruhlmann.imgfloat.service.git.GitCommitInfo;
import java.io.IOException; import dev.kruhlmann.imgfloat.service.git.GitCommitInfoSource;
import java.io.InputStream; import java.util.List;
import java.io.InputStreamReader; import java.util.Objects;
import java.util.Properties; import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
@@ -15,18 +13,16 @@ import org.springframework.util.StringUtils;
public class GitInfoService { public class GitInfoService {
private static final String FALLBACK_GIT_SHA = "unknown"; private static final String FALLBACK_GIT_SHA = "unknown";
private static final Logger LOG = LoggerFactory.getLogger(GitInfoService.class);
private final String commitSha; private final String commitSha;
private final String shortCommitSha; private final String shortCommitSha;
private final String commitUrlPrefix; private final String commitUrlPrefix;
public GitInfoService(@Value("${IMGFLOAT_COMMIT_URL_PREFIX:}") String commitUrlPrefix) { public GitInfoService(
CommitInfo commitInfo = resolveFromGitProperties(); @Value("${IMGFLOAT_COMMIT_URL_PREFIX:}") String commitUrlPrefix,
if (commitInfo == null) { List<GitCommitInfoSource> commitInfoSources
commitInfo = resolveFromGitBinary(); ) {
} GitCommitInfo commitInfo = resolveCommitInfo(commitInfoSources);
String full = commitInfo != null ? commitInfo.fullSha() : null; String full = commitInfo != null ? commitInfo.fullSha() : null;
String abbreviated = commitInfo != null ? commitInfo.shortSha() : null; String abbreviated = commitInfo != null ? commitInfo.shortSha() : null;
@@ -59,54 +55,20 @@ public class GitInfoService {
return StringUtils.hasText(commitUrlPrefix); return StringUtils.hasText(commitUrlPrefix);
} }
private CommitInfo resolveFromGitProperties() { private GitCommitInfo resolveCommitInfo(List<GitCommitInfoSource> commitInfoSources) {
try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream("git.properties")) { if (commitInfoSources == null || commitInfoSources.isEmpty()) {
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);
return null; return null;
} }
Optional<GitCommitInfo> 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) { private String abbreviate(String value) {
@@ -130,5 +92,15 @@ public class GitInfoService {
return value; 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);
}
} }

View File

@@ -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<GitCommitInfo> 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<String> 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;
}
}
}

View File

@@ -0,0 +1,3 @@
package dev.kruhlmann.imgfloat.service.git;
public record GitCommitInfo(String fullSha, String shortSha) {}

View File

@@ -0,0 +1,8 @@
package dev.kruhlmann.imgfloat.service.git;
import java.util.Optional;
@FunctionalInterface
public interface GitCommitInfoSource {
Optional<GitCommitInfo> loadCommitInfo();
}

View File

@@ -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<GitCommitInfo> 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();
}
}
}

View File

@@ -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<GitCommitInfo> payload;
private final AtomicInteger calls = new AtomicInteger();
private TrackingSource(Optional<GitCommitInfo> payload) {
this.payload = payload;
}
@Override
public Optional<GitCommitInfo> loadCommitInfo() {
calls.incrementAndGet();
return payload;
}
private int calls() {
return calls.get();
}
}
}