From b2b3993d854bfb7f849bd74057e51c93d3696ca9 Mon Sep 17 00:00:00 2001 From: Sebastiaan de Schaetzen Date: Fri, 27 Feb 2026 09:57:53 +0100 Subject: [PATCH] Add file diff view to staging page, fix title Rename 'Changes' to 'Staging' in the page title and heading. File names in both modified and staged file lists are now clickable links that show the diff for that file. The diff view shows both unstaged (index vs working tree) and staged (HEAD vs index) differences. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../webgit/controller/RepoController.java | 13 +++++++ .../seeseepuff/webgit/service/GitService.java | 39 +++++++++++++++++++ src/main/resources/templates/changes.html | 8 ++-- src/main/resources/templates/file-diff.html | 16 ++++++++ .../webgit/controller/RepoControllerTest.java | 12 ++++++ .../webgit/service/GitServiceTest.java | 32 +++++++++++++++ 6 files changed, 116 insertions(+), 4 deletions(-) create mode 100644 src/main/resources/templates/file-diff.html diff --git a/src/main/java/be/seeseepuff/webgit/controller/RepoController.java b/src/main/java/be/seeseepuff/webgit/controller/RepoController.java index b456e2b..9e8570c 100644 --- a/src/main/java/be/seeseepuff/webgit/controller/RepoController.java +++ b/src/main/java/be/seeseepuff/webgit/controller/RepoController.java @@ -43,6 +43,19 @@ public class RepoController return "changes"; } + @GetMapping("/repo/{name}/diff/{filePath}/**") + public String fileDiff(@PathVariable String name, + jakarta.servlet.http.HttpServletRequest request, Model model) throws IOException + { + String fullPath = request.getRequestURI(); + String prefix = "/repo/" + name + "/diff/"; + String filePath = fullPath.substring(prefix.length()); + model.addAttribute("name", name); + model.addAttribute("filePath", filePath); + model.addAttribute("diff", gitService.getWorkingTreeDiff(name, filePath)); + return "file-diff"; + } + @GetMapping("/repo/{name}/remote") public String remote(@PathVariable String name, Model model) throws IOException { diff --git a/src/main/java/be/seeseepuff/webgit/service/GitService.java b/src/main/java/be/seeseepuff/webgit/service/GitService.java index be2734a..b6f5890 100644 --- a/src/main/java/be/seeseepuff/webgit/service/GitService.java +++ b/src/main/java/be/seeseepuff/webgit/service/GitService.java @@ -209,6 +209,45 @@ public class GitService } } + public String getWorkingTreeDiff(String name, String filePath) throws IOException + { + try (Git git = openRepository(name)) + { + Repository repo = git.getRepository(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (DiffFormatter df = new DiffFormatter(out)) + { + df.setRepository(repo); + df.setPathFilter(PathFilter.create(filePath)); + // Unstaged: index vs working tree + var dirCache = new org.eclipse.jgit.dircache.DirCacheIterator(repo.readDirCache()); + var workTree = new org.eclipse.jgit.treewalk.FileTreeIterator(repo); + List unstaged = df.scan(dirCache, workTree); + for (DiffEntry diff : unstaged) + { + df.format(diff); + } + // Staged: HEAD vs index + ObjectId headTree = repo.resolve("HEAD^{tree}"); + if (headTree != null) + { + var headIter = new org.eclipse.jgit.treewalk.CanonicalTreeParser(); + try (var reader = repo.newObjectReader()) + { + headIter.reset(reader, headTree); + } + var indexIter = new org.eclipse.jgit.dircache.DirCacheIterator(repo.readDirCache()); + List staged = df.scan(headIter, indexIter); + for (DiffEntry diff : staged) + { + df.format(diff); + } + } + } + return out.toString(StandardCharsets.UTF_8); + } + } + public void stageFiles(String name, List files) throws IOException, GitAPIException { try (Git git = openRepository(name)) diff --git a/src/main/resources/templates/changes.html b/src/main/resources/templates/changes.html index d93bcb1..52a2bfa 100644 --- a/src/main/resources/templates/changes.html +++ b/src/main/resources/templates/changes.html @@ -1,10 +1,10 @@ -Changes +Staging -

Changes

+

Staging

Modified Files (unstaged)

@@ -15,7 +15,7 @@ - +
@@ -32,7 +32,7 @@ - +
diff --git a/src/main/resources/templates/file-diff.html b/src/main/resources/templates/file-diff.html new file mode 100644 index 0000000..f2c00ec --- /dev/null +++ b/src/main/resources/templates/file-diff.html @@ -0,0 +1,16 @@ + + + +Diff + + +

file.txt

+ +

< Back to staging

+ +
Diff output here
+ +

No differences found.

+ + + diff --git a/src/test/java/be/seeseepuff/webgit/controller/RepoControllerTest.java b/src/test/java/be/seeseepuff/webgit/controller/RepoControllerTest.java index 94be4f4..d9bf246 100644 --- a/src/test/java/be/seeseepuff/webgit/controller/RepoControllerTest.java +++ b/src/test/java/be/seeseepuff/webgit/controller/RepoControllerTest.java @@ -290,4 +290,16 @@ class RepoControllerTest verify(gitService).checkoutFilesFromCommit("myrepo", "abc1234", List.of("a.txt", "b.txt")); } + + @Test + void fileDiffShowsDiff() throws Exception + { + when(gitService.getWorkingTreeDiff("myrepo", "src/main.txt")).thenReturn("diff --git a/src/main.txt\n+hello"); + + mockMvc.perform(get("/repo/myrepo/diff/src/main.txt")) + .andExpect(status().isOk()) + .andExpect(view().name("file-diff")) + .andExpect(model().attribute("filePath", "src/main.txt")) + .andExpect(content().string(org.hamcrest.Matchers.containsString("+hello"))); + } } diff --git a/src/test/java/be/seeseepuff/webgit/service/GitServiceTest.java b/src/test/java/be/seeseepuff/webgit/service/GitServiceTest.java index 9c0d9ff..7e11665 100644 --- a/src/test/java/be/seeseepuff/webgit/service/GitServiceTest.java +++ b/src/test/java/be/seeseepuff/webgit/service/GitServiceTest.java @@ -419,4 +419,36 @@ class GitServiceTest assertEquals("# Test", Files.readString(readme)); } + + @Test + void getWorkingTreeDiffShowsUnstagedChanges() throws GitAPIException, IOException + { + gitService.cloneRepository(bareRemote.toUri().toString(), "myrepo"); + Files.writeString(worktreePath.resolve("myrepo").resolve("README.md"), "modified content"); + + String diff = gitService.getWorkingTreeDiff("myrepo", "README.md"); + assertFalse(diff.isEmpty()); + assertTrue(diff.contains("README.md")); + } + + @Test + void getWorkingTreeDiffShowsStagedChanges() throws GitAPIException, IOException + { + gitService.cloneRepository(bareRemote.toUri().toString(), "myrepo"); + Files.writeString(worktreePath.resolve("myrepo").resolve("README.md"), "staged content"); + gitService.stageFiles("myrepo", List.of("README.md")); + + String diff = gitService.getWorkingTreeDiff("myrepo", "README.md"); + assertFalse(diff.isEmpty()); + assertTrue(diff.contains("README.md")); + } + + @Test + void getWorkingTreeDiffReturnsEmptyForUnchangedFile() throws GitAPIException, IOException + { + gitService.cloneRepository(bareRemote.toUri().toString(), "myrepo"); + + String diff = gitService.getWorkingTreeDiff("myrepo", "README.md"); + assertTrue(diff.isEmpty()); + } }