From 88385d39a17d9c747352cadb67aad401ac8abb60 Mon Sep 17 00:00:00 2001 From: Sebastiaan de Schaetzen Date: Thu, 26 Feb 2026 08:43:15 +0100 Subject: [PATCH] Add web controllers and HTML templates Home page lists repositories with clone form. Repo page shows branch management, file staging, commit, push and pull controls. Table-based layout with no JavaScript or CSS for retro browser compatibility. Includes MockMvc integration tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../webgit/controller/HomeController.java | 33 +++++ .../webgit/controller/RepoController.java | 74 +++++++++++ src/main/resources/templates/home.html | 43 ++++++ src/main/resources/templates/repo.html | 97 ++++++++++++++ .../webgit/controller/HomeControllerTest.java | 61 +++++++++ .../webgit/controller/RepoControllerTest.java | 125 ++++++++++++++++++ 6 files changed, 433 insertions(+) create mode 100644 src/main/java/be/seeseepuff/webgit/controller/HomeController.java create mode 100644 src/main/java/be/seeseepuff/webgit/controller/RepoController.java create mode 100644 src/main/resources/templates/home.html create mode 100644 src/main/resources/templates/repo.html create mode 100644 src/test/java/be/seeseepuff/webgit/controller/HomeControllerTest.java create mode 100644 src/test/java/be/seeseepuff/webgit/controller/RepoControllerTest.java diff --git a/src/main/java/be/seeseepuff/webgit/controller/HomeController.java b/src/main/java/be/seeseepuff/webgit/controller/HomeController.java new file mode 100644 index 0000000..080356d --- /dev/null +++ b/src/main/java/be/seeseepuff/webgit/controller/HomeController.java @@ -0,0 +1,33 @@ +package be.seeseepuff.webgit.controller; + +import be.seeseepuff.webgit.service.GitService; +import lombok.RequiredArgsConstructor; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import java.io.IOException; + +@Controller +@RequiredArgsConstructor +public class HomeController +{ + private final GitService gitService; + + @GetMapping("/") + public String home(Model model) throws IOException + { + model.addAttribute("repositories", gitService.listRepositories()); + return "home"; + } + + @PostMapping("/clone") + public String cloneRepo(@RequestParam String url, @RequestParam String name) throws GitAPIException, IOException + { + gitService.cloneRepository(url, name); + return "redirect:/"; + } +} diff --git a/src/main/java/be/seeseepuff/webgit/controller/RepoController.java b/src/main/java/be/seeseepuff/webgit/controller/RepoController.java new file mode 100644 index 0000000..8213c75 --- /dev/null +++ b/src/main/java/be/seeseepuff/webgit/controller/RepoController.java @@ -0,0 +1,74 @@ +package be.seeseepuff.webgit.controller; + +import be.seeseepuff.webgit.service.GitService; +import lombok.RequiredArgsConstructor; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import java.io.IOException; +import java.util.List; + +@Controller +@RequiredArgsConstructor +public class RepoController +{ + private final GitService gitService; + + @GetMapping("/repo/{name}") + public String repo(@PathVariable String name, Model model) throws IOException, GitAPIException + { + model.addAttribute("name", name); + model.addAttribute("currentBranch", gitService.getCurrentBranch(name)); + model.addAttribute("branches", gitService.listBranches(name)); + model.addAttribute("modifiedFiles", gitService.getModifiedFiles(name)); + model.addAttribute("stagedFiles", gitService.getStagedFiles(name)); + return "repo"; + } + + @PostMapping("/repo/{name}/checkout") + public String checkout(@PathVariable String name, @RequestParam String branch) throws IOException, GitAPIException + { + gitService.checkoutBranch(name, branch); + return "redirect:/repo/" + name; + } + + @PostMapping("/repo/{name}/new-branch") + public String newBranch(@PathVariable String name, @RequestParam String branch) throws IOException, GitAPIException + { + gitService.createAndCheckoutBranch(name, branch); + return "redirect:/repo/" + name; + } + + @PostMapping("/repo/{name}/stage") + public String stage(@PathVariable String name, @RequestParam List files) throws IOException, GitAPIException + { + gitService.stageFiles(name, files); + return "redirect:/repo/" + name; + } + + @PostMapping("/repo/{name}/commit") + public String commit(@PathVariable String name, @RequestParam String message) throws IOException, GitAPIException + { + gitService.commit(name, message); + return "redirect:/repo/" + name; + } + + @PostMapping("/repo/{name}/push") + public String push(@PathVariable String name) throws IOException, GitAPIException + { + gitService.push(name); + return "redirect:/repo/" + name; + } + + @PostMapping("/repo/{name}/pull") + public String pull(@PathVariable String name) throws IOException, GitAPIException + { + gitService.pull(name); + return "redirect:/repo/" + name; + } +} diff --git a/src/main/resources/templates/home.html b/src/main/resources/templates/home.html new file mode 100644 index 0000000..30e7ecf --- /dev/null +++ b/src/main/resources/templates/home.html @@ -0,0 +1,43 @@ + + + +WebGit + + +

WebGit

+ +

Repositories

+ + + + + + + + + + + + +
NameActions
Open
No repositories cloned yet.
+ +

Clone a Repository

+
+ + + + + + + + + + + + + +
URL:
Name:
+
+ + + diff --git a/src/main/resources/templates/repo.html b/src/main/resources/templates/repo.html new file mode 100644 index 0000000..f3df61e --- /dev/null +++ b/src/main/resources/templates/repo.html @@ -0,0 +1,97 @@ + + + +WebGit - repo + + +

WebGit

+

repo

+ +

Branch

+ + + + + +
Current branch:
+ + + + + + +
+
+Switch to: + + +
+
+
+New branch: + +
+
+ +
+ +

Modified Files (unstaged)

+
+ + + + + + + + + +
StageFile
+
+ +
+

No modified files.

+ +

Staged Files

+ + + + + + + +
File
+

No staged files.

+ +
+ + + + + + +
Commit message:
+
+ +
+ +

Remote

+ + + + + +
+
+ +
+
+
+ +
+
+ + + diff --git a/src/test/java/be/seeseepuff/webgit/controller/HomeControllerTest.java b/src/test/java/be/seeseepuff/webgit/controller/HomeControllerTest.java new file mode 100644 index 0000000..767b386 --- /dev/null +++ b/src/test/java/be/seeseepuff/webgit/controller/HomeControllerTest.java @@ -0,0 +1,61 @@ +package be.seeseepuff.webgit.controller; + +import be.seeseepuff.webgit.service.GitService; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.List; + +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +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.*; + +@WebMvcTest(HomeController.class) +class HomeControllerTest +{ + @Autowired + private MockMvc mockMvc; + + @MockitoBean + private GitService gitService; + + @Test + void homePageShowsRepositories() throws Exception + { + when(gitService.listRepositories()).thenReturn(List.of("repo1", "repo2")); + + mockMvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(view().name("home")) + .andExpect(model().attribute("repositories", List.of("repo1", "repo2"))) + .andExpect(content().string(org.hamcrest.Matchers.containsString("repo1"))) + .andExpect(content().string(org.hamcrest.Matchers.containsString("repo2"))); + } + + @Test + void homePageShowsEmptyList() throws Exception + { + when(gitService.listRepositories()).thenReturn(List.of()); + + mockMvc.perform(get("/")) + .andExpect(status().isOk()) + .andExpect(content().string(org.hamcrest.Matchers.containsString("No repositories cloned yet."))); + } + + @Test + void cloneRedirectsToHome() throws Exception + { + mockMvc.perform(post("/clone") + .param("url", "https://example.com/repo.git") + .param("name", "myrepo")) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/")); + + verify(gitService).cloneRepository("https://example.com/repo.git", "myrepo"); + } +} diff --git a/src/test/java/be/seeseepuff/webgit/controller/RepoControllerTest.java b/src/test/java/be/seeseepuff/webgit/controller/RepoControllerTest.java new file mode 100644 index 0000000..f1e60b6 --- /dev/null +++ b/src/test/java/be/seeseepuff/webgit/controller/RepoControllerTest.java @@ -0,0 +1,125 @@ +package be.seeseepuff.webgit.controller; + +import be.seeseepuff.webgit.service.GitService; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.List; + +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +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.*; + +@WebMvcTest(RepoController.class) +class RepoControllerTest +{ + @Autowired + private MockMvc mockMvc; + + @MockitoBean + private GitService gitService; + + @Test + void repoPageShowsDetails() throws Exception + { + when(gitService.getCurrentBranch("myrepo")).thenReturn("main"); + when(gitService.listBranches("myrepo")).thenReturn(List.of("main", "develop")); + when(gitService.getModifiedFiles("myrepo")).thenReturn(List.of("file1.txt")); + when(gitService.getStagedFiles("myrepo")).thenReturn(List.of("file2.txt")); + + mockMvc.perform(get("/repo/myrepo")) + .andExpect(status().isOk()) + .andExpect(view().name("repo")) + .andExpect(model().attribute("name", "myrepo")) + .andExpect(model().attribute("currentBranch", "main")) + .andExpect(model().attribute("branches", List.of("main", "develop"))) + .andExpect(model().attribute("modifiedFiles", List.of("file1.txt"))) + .andExpect(model().attribute("stagedFiles", List.of("file2.txt"))) + .andExpect(content().string(org.hamcrest.Matchers.containsString("myrepo"))) + .andExpect(content().string(org.hamcrest.Matchers.containsString("main"))); + } + + @Test + void repoPageShowsNoModifiedFiles() throws Exception + { + when(gitService.getCurrentBranch("myrepo")).thenReturn("main"); + when(gitService.listBranches("myrepo")).thenReturn(List.of("main")); + when(gitService.getModifiedFiles("myrepo")).thenReturn(List.of()); + when(gitService.getStagedFiles("myrepo")).thenReturn(List.of()); + + mockMvc.perform(get("/repo/myrepo")) + .andExpect(status().isOk()) + .andExpect(content().string(org.hamcrest.Matchers.containsString("No modified files."))) + .andExpect(content().string(org.hamcrest.Matchers.containsString("No staged files."))); + } + + @Test + void checkoutRedirectsToRepo() throws Exception + { + mockMvc.perform(post("/repo/myrepo/checkout") + .param("branch", "develop")) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/repo/myrepo")); + + verify(gitService).checkoutBranch("myrepo", "develop"); + } + + @Test + void newBranchRedirectsToRepo() throws Exception + { + mockMvc.perform(post("/repo/myrepo/new-branch") + .param("branch", "feature-z")) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/repo/myrepo")); + + verify(gitService).createAndCheckoutBranch("myrepo", "feature-z"); + } + + @Test + void stageRedirectsToRepo() throws Exception + { + mockMvc.perform(post("/repo/myrepo/stage") + .param("files", "a.txt") + .param("files", "b.txt")) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/repo/myrepo")); + + verify(gitService).stageFiles("myrepo", List.of("a.txt", "b.txt")); + } + + @Test + void commitRedirectsToRepo() throws Exception + { + mockMvc.perform(post("/repo/myrepo/commit") + .param("message", "test commit")) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/repo/myrepo")); + + verify(gitService).commit("myrepo", "test commit"); + } + + @Test + void pushRedirectsToRepo() throws Exception + { + mockMvc.perform(post("/repo/myrepo/push")) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/repo/myrepo")); + + verify(gitService).push("myrepo"); + } + + @Test + void pullRedirectsToRepo() throws Exception + { + mockMvc.perform(post("/repo/myrepo/pull")) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/repo/myrepo")); + + verify(gitService).pull("myrepo"); + } +}