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>
This commit is contained in:
@@ -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:/";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/main/resources/templates/home.html
Normal file
43
src/main/resources/templates/home.html
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<head>
|
||||||
|
<title>WebGit</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>WebGit</h1>
|
||||||
|
|
||||||
|
<h2>Repositories</h2>
|
||||||
|
<table border="1" cellpadding="4" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
<tr th:each="repo : ${repositories}">
|
||||||
|
<td th:text="${repo}"></td>
|
||||||
|
<td><a th:href="@{/repo/{name}(name=${repo})}">Open</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr th:if="${#lists.isEmpty(repositories)}">
|
||||||
|
<td colspan="2"><i>No repositories cloned yet.</i></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2>Clone a Repository</h2>
|
||||||
|
<form method="post" action="/clone">
|
||||||
|
<table border="0" cellpadding="4" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td>URL:</td>
|
||||||
|
<td><input type="text" name="url" size="60"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Name:</td>
|
||||||
|
<td><input type="text" name="name" size="30"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td><input type="submit" value="Clone"></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
97
src/main/resources/templates/repo.html
Normal file
97
src/main/resources/templates/repo.html
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<head>
|
||||||
|
<title th:text="'WebGit - ' + ${name}">WebGit - repo</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1><a href="/">WebGit</a></h1>
|
||||||
|
<h2 th:text="${name}">repo</h2>
|
||||||
|
|
||||||
|
<h3>Branch</h3>
|
||||||
|
<table border="0" cellpadding="4" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td>Current branch:</td>
|
||||||
|
<td><b th:text="${currentBranch}"></b></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<table border="0" cellpadding="4" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<form method="post" th:action="@{/repo/{name}/checkout(name=${name})}">
|
||||||
|
Switch to:
|
||||||
|
<select name="branch">
|
||||||
|
<option th:each="b : ${branches}" th:value="${b}" th:text="${b}" th:selected="${b == currentBranch}"></option>
|
||||||
|
</select>
|
||||||
|
<input type="submit" value="Checkout">
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<form method="post" th:action="@{/repo/{name}/new-branch(name=${name})}">
|
||||||
|
New branch: <input type="text" name="branch" size="20">
|
||||||
|
<input type="submit" value="Create">
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h3>Modified Files (unstaged)</h3>
|
||||||
|
<form method="post" th:action="@{/repo/{name}/stage(name=${name})}" th:if="${!#lists.isEmpty(modifiedFiles)}">
|
||||||
|
<table border="1" cellpadding="4" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<th>Stage</th>
|
||||||
|
<th>File</th>
|
||||||
|
</tr>
|
||||||
|
<tr th:each="file : ${modifiedFiles}">
|
||||||
|
<td><input type="checkbox" name="files" th:value="${file}"></td>
|
||||||
|
<td th:text="${file}"></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<br>
|
||||||
|
<input type="submit" value="Stage Selected">
|
||||||
|
</form>
|
||||||
|
<p th:if="${#lists.isEmpty(modifiedFiles)}"><i>No modified files.</i></p>
|
||||||
|
|
||||||
|
<h3>Staged Files</h3>
|
||||||
|
<table border="1" cellpadding="4" cellspacing="0" th:if="${!#lists.isEmpty(stagedFiles)}">
|
||||||
|
<tr>
|
||||||
|
<th>File</th>
|
||||||
|
</tr>
|
||||||
|
<tr th:each="file : ${stagedFiles}">
|
||||||
|
<td th:text="${file}"></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<p th:if="${#lists.isEmpty(stagedFiles)}"><i>No staged files.</i></p>
|
||||||
|
|
||||||
|
<form method="post" th:action="@{/repo/{name}/commit(name=${name})}" th:if="${!#lists.isEmpty(stagedFiles)}">
|
||||||
|
<table border="0" cellpadding="4" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td>Commit message:</td>
|
||||||
|
<td><input type="text" name="message" size="60"></td>
|
||||||
|
<td><input type="submit" value="Commit"></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h3>Remote</h3>
|
||||||
|
<table border="0" cellpadding="4" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<form method="post" th:action="@{/repo/{name}/push(name=${name})}">
|
||||||
|
<input type="submit" value="Push">
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<form method="post" th:action="@{/repo/{name}/pull(name=${name})}">
|
||||||
|
<input type="submit" value="Pull">
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user