From d68933bc2f5087046541117cedcf1321c9c442eb Mon Sep 17 00:00:00 2001 From: Sebastiaan de Schaetzen Date: Fri, 27 Feb 2026 10:17:30 +0100 Subject: [PATCH] Render images in file browser blob view Add /repo/{name}/raw/{hash}/** endpoint serving binary file content with correct Content-Type. Blob view detects image extensions (png, jpg, gif, bmp, webp, svg, ico) and renders an tag instead of a
 text block.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
 .../webgit/controller/RepoController.java     | 37 ++++++++++++++++++-
 .../seeseepuff/webgit/service/GitService.java | 10 ++++-
 src/main/resources/templates/blob.html        |  5 ++-
 .../webgit/controller/RepoControllerTest.java | 33 +++++++++++++++++
 .../webgit/service/GitServiceTest.java        | 21 +++++++++++
 5 files changed, 103 insertions(+), 3 deletions(-)

diff --git a/src/main/java/be/seeseepuff/webgit/controller/RepoController.java b/src/main/java/be/seeseepuff/webgit/controller/RepoController.java
index 69516de..2609751 100644
--- a/src/main/java/be/seeseepuff/webgit/controller/RepoController.java
+++ b/src/main/java/be/seeseepuff/webgit/controller/RepoController.java
@@ -3,6 +3,8 @@ package be.seeseepuff.webgit.controller;
 import be.seeseepuff.webgit.service.GitService;
 import lombok.RequiredArgsConstructor;
 import org.eclipse.jgit.api.errors.GitAPIException;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
 import org.springframework.stereotype.Controller;
 import org.springframework.ui.Model;
 import org.springframework.web.bind.annotation.GetMapping;
@@ -12,11 +14,25 @@ import org.springframework.web.bind.annotation.RequestParam;
 
 import java.io.IOException;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 @Controller
 @RequiredArgsConstructor
 public class RepoController
 {
+	private static final Set IMAGE_EXTENSIONS = Set.of("png", "jpg", "jpeg", "gif", "bmp", "webp", "svg", "ico");
+	private static final Map IMAGE_MIME_TYPES = Map.of(
+		"png", "image/png",
+		"jpg", "image/jpeg",
+		"jpeg", "image/jpeg",
+		"gif", "image/gif",
+		"bmp", "image/bmp",
+		"webp", "image/webp",
+		"svg", "image/svg+xml",
+		"ico", "image/x-icon"
+	);
+
 	private final GitService gitService;
 
 	@GetMapping("/repo/{name}")
@@ -109,13 +125,32 @@ public class RepoController
 		String fullPath = request.getRequestURI();
 		String prefix = "/repo/" + name + "/blob/" + hash + "/";
 		String filePath = fullPath.substring(prefix.length());
+		String ext = filePath.contains(".") ? filePath.substring(filePath.lastIndexOf('.') + 1).toLowerCase() : "";
+		boolean isImage = IMAGE_EXTENSIONS.contains(ext);
 		model.addAttribute("name", name);
 		model.addAttribute("hash", hash);
 		model.addAttribute("filePath", filePath);
-		model.addAttribute("content", gitService.getFileContentAtCommit(name, hash, filePath));
+		model.addAttribute("isImage", isImage);
+		if (!isImage)
+			model.addAttribute("content", gitService.getFileContentAtCommit(name, hash, filePath));
 		return "blob";
 	}
 
+	@GetMapping("/repo/{name}/raw/{hash}/**")
+	public ResponseEntity rawFile(@PathVariable String name, @PathVariable String hash,
+										  jakarta.servlet.http.HttpServletRequest request) throws IOException
+	{
+		String fullPath = request.getRequestURI();
+		String prefix = "/repo/" + name + "/raw/" + hash + "/";
+		String filePath = fullPath.substring(prefix.length());
+		byte[] bytes = gitService.getRawFileAtCommit(name, hash, filePath);
+		if (bytes == null)
+			return ResponseEntity.notFound().build();
+		String ext = filePath.contains(".") ? filePath.substring(filePath.lastIndexOf('.') + 1).toLowerCase() : "";
+		String mimeType = IMAGE_MIME_TYPES.getOrDefault(ext, MediaType.APPLICATION_OCTET_STREAM_VALUE);
+		return ResponseEntity.ok().contentType(MediaType.parseMediaType(mimeType)).body(bytes);
+	}
+
 	@PostMapping("/repo/{name}/checkout-commit")
 	public String checkoutCommit(@PathVariable String name, @RequestParam String hash) throws IOException, GitAPIException
 	{
diff --git a/src/main/java/be/seeseepuff/webgit/service/GitService.java b/src/main/java/be/seeseepuff/webgit/service/GitService.java
index 0e21bc3..2de7179 100644
--- a/src/main/java/be/seeseepuff/webgit/service/GitService.java
+++ b/src/main/java/be/seeseepuff/webgit/service/GitService.java
@@ -490,6 +490,14 @@ public class GitService
 	}
 
 	public String getFileContentAtCommit(String name, String commitHash, String filePath) throws IOException
+	{
+		byte[] bytes = getRawFileAtCommit(name, commitHash, filePath);
+		if (bytes == null)
+			return null;
+		return new String(bytes, StandardCharsets.UTF_8);
+	}
+
+	public byte[] getRawFileAtCommit(String name, String commitHash, String filePath) throws IOException
 	{
 		try (Git git = openRepository(name))
 		{
@@ -505,7 +513,7 @@ public class GitService
 					if (tw == null)
 						return null;
 					ObjectLoader loader = repo.open(tw.getObjectId(0));
-					return new String(loader.getBytes(), StandardCharsets.UTF_8);
+					return loader.getBytes();
 				}
 			}
 		}
diff --git a/src/main/resources/templates/blob.html b/src/main/resources/templates/blob.html
index 6ff9f13..f7578f6 100644
--- a/src/main/resources/templates/blob.html
+++ b/src/main/resources/templates/blob.html
@@ -25,7 +25,10 @@
 
 
 

Content

-
File content here
+
+ +
+
File content here
diff --git a/src/test/java/be/seeseepuff/webgit/controller/RepoControllerTest.java b/src/test/java/be/seeseepuff/webgit/controller/RepoControllerTest.java index b6c477a..66d9b6c 100644 --- a/src/test/java/be/seeseepuff/webgit/controller/RepoControllerTest.java +++ b/src/test/java/be/seeseepuff/webgit/controller/RepoControllerTest.java @@ -9,8 +9,10 @@ import org.springframework.test.web.servlet.MockMvc; import java.util.List; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.mockito.ArgumentMatchers.any; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @@ -279,6 +281,37 @@ class RepoControllerTest .andExpect(content().string(org.hamcrest.Matchers.containsString("# Hello"))); } + @Test + void blobShowsImageTag() throws Exception + { + mockMvc.perform(get("/repo/myrepo/blob/abc1234/photo.png")) + .andExpect(status().isOk()) + .andExpect(view().name("blob")) + .andExpect(content().string(org.hamcrest.Matchers.containsString("/repo/myrepo/raw/abc1234/photo.png"))); + verify(gitService, never()).getFileContentAtCommit(any(), any(), any()); + } + + @Test + void rawFileServesBytes() throws Exception + { + byte[] imageBytes = new byte[]{(byte)0x89, 0x50, 0x4E, 0x47}; + when(gitService.getRawFileAtCommit("myrepo", "abc1234", "photo.png")).thenReturn(imageBytes); + + mockMvc.perform(get("/repo/myrepo/raw/abc1234/photo.png")) + .andExpect(status().isOk()) + .andExpect(content().contentType("image/png")) + .andExpect(content().bytes(imageBytes)); + } + + @Test + void rawFileReturns404WhenNotFound() throws Exception + { + when(gitService.getRawFileAtCommit("myrepo", "abc1234", "missing.png")).thenReturn(null); + + mockMvc.perform(get("/repo/myrepo/raw/abc1234/missing.png")) + .andExpect(status().isNotFound()); + } + @Test void checkoutCommitRedirectsToCommits() throws Exception { diff --git a/src/test/java/be/seeseepuff/webgit/service/GitServiceTest.java b/src/test/java/be/seeseepuff/webgit/service/GitServiceTest.java index ffe5d5e..cce2fbf 100644 --- a/src/test/java/be/seeseepuff/webgit/service/GitServiceTest.java +++ b/src/test/java/be/seeseepuff/webgit/service/GitServiceTest.java @@ -400,6 +400,27 @@ class GitServiceTest assertEquals("# Test", content); } + @Test + void getRawFileAtCommitReturnsBytes() throws GitAPIException, IOException + { + gitService.cloneRepository(bareRemote.toUri().toString(), "myrepo"); + + var commits = gitService.listCommits("myrepo"); + byte[] bytes = gitService.getRawFileAtCommit("myrepo", commits.getFirst().hash(), "README.md"); + assertNotNull(bytes); + assertEquals("# Test", new String(bytes, java.nio.charset.StandardCharsets.UTF_8)); + } + + @Test + void getRawFileAtCommitReturnsNullForMissingFile() throws GitAPIException, IOException + { + gitService.cloneRepository(bareRemote.toUri().toString(), "myrepo"); + + var commits = gitService.listCommits("myrepo"); + byte[] bytes = gitService.getRawFileAtCommit("myrepo", commits.getFirst().hash(), "nonexistent.png"); + assertNull(bytes); + } + @Test void getFileContentAtCommitReturnsNullForMissingFile() throws GitAPIException, IOException {