Refactor web UI to use HTML frames layout

Replace single-page layout with a frameset: left navigation frame
with repo dropdown and page links, right content frame showing
the selected page. Split the repo page into separate sub-pages:
branches, changes, remote, and manage.

Uses <frameset> for maximum compatibility with ancient browsers
(Netscape 2+, IE 3+). Clone and delete forms target _top to
reload the full frameset when the repo list changes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-02-26 12:12:15 +01:00
parent 7fa68da521
commit c3424362d4
14 changed files with 381 additions and 151 deletions

View File

@@ -18,16 +18,43 @@ public class HomeController
private final GitService gitService; private final GitService gitService;
@GetMapping("/") @GetMapping("/")
public String home(Model model) throws IOException public String frameset(@RequestParam(required = false) String repo, Model model) throws IOException
{
model.addAttribute("selectedRepo", repo);
return "frameset";
}
@GetMapping("/nav")
public String nav(@RequestParam(required = false) String repo, Model model) throws IOException
{ {
model.addAttribute("repositories", gitService.listRepositories()); model.addAttribute("repositories", gitService.listRepositories());
return "home"; model.addAttribute("selectedRepo", repo);
return "nav";
}
@GetMapping("/welcome")
public String welcome()
{
return "welcome";
}
@GetMapping("/repos")
public String repos(Model model) throws IOException
{
model.addAttribute("repositories", gitService.listRepositories());
return "repos";
}
@GetMapping("/clone-form")
public String cloneForm()
{
return "clone";
} }
@PostMapping("/clone") @PostMapping("/clone")
public String cloneRepo(@RequestParam String url, @RequestParam String name) throws GitAPIException, IOException public String cloneRepo(@RequestParam String url, @RequestParam String name) throws GitAPIException, IOException
{ {
gitService.cloneRepository(url, name); gitService.cloneRepository(url, name);
return "redirect:/"; return "redirect:/?repo=" + name;
} }
} }

View File

@@ -20,63 +20,90 @@ public class RepoController
private final GitService gitService; private final GitService gitService;
@GetMapping("/repo/{name}") @GetMapping("/repo/{name}")
public String repo(@PathVariable String name, Model model) throws IOException, GitAPIException public String repo(@PathVariable String name)
{
return "redirect:/repo/" + name + "/branches";
}
@GetMapping("/repo/{name}/branches")
public String branches(@PathVariable String name, Model model) throws IOException, GitAPIException
{ {
model.addAttribute("name", name); model.addAttribute("name", name);
model.addAttribute("currentBranch", gitService.getCurrentBranch(name)); model.addAttribute("currentBranch", gitService.getCurrentBranch(name));
model.addAttribute("branches", gitService.listBranches(name)); model.addAttribute("branches", gitService.listBranches(name));
return "branches";
}
@GetMapping("/repo/{name}/changes")
public String changes(@PathVariable String name, Model model) throws IOException, GitAPIException
{
model.addAttribute("name", name);
model.addAttribute("modifiedFiles", gitService.getModifiedFiles(name)); model.addAttribute("modifiedFiles", gitService.getModifiedFiles(name));
model.addAttribute("stagedFiles", gitService.getStagedFiles(name)); model.addAttribute("stagedFiles", gitService.getStagedFiles(name));
return "repo"; return "changes";
}
@GetMapping("/repo/{name}/remote")
public String remote(@PathVariable String name, Model model)
{
model.addAttribute("name", name);
return "remote";
}
@GetMapping("/repo/{name}/manage")
public String manage(@PathVariable String name, Model model)
{
model.addAttribute("name", name);
return "manage";
} }
@PostMapping("/repo/{name}/checkout") @PostMapping("/repo/{name}/checkout")
public String checkout(@PathVariable String name, @RequestParam String branch) throws IOException, GitAPIException public String checkout(@PathVariable String name, @RequestParam String branch) throws IOException, GitAPIException
{ {
gitService.checkoutBranch(name, branch); gitService.checkoutBranch(name, branch);
return "redirect:/repo/" + name; return "redirect:/repo/" + name + "/branches";
} }
@PostMapping("/repo/{name}/new-branch") @PostMapping("/repo/{name}/new-branch")
public String newBranch(@PathVariable String name, @RequestParam String branch) throws IOException, GitAPIException public String newBranch(@PathVariable String name, @RequestParam String branch) throws IOException, GitAPIException
{ {
gitService.createAndCheckoutBranch(name, branch); gitService.createAndCheckoutBranch(name, branch);
return "redirect:/repo/" + name; return "redirect:/repo/" + name + "/branches";
} }
@PostMapping("/repo/{name}/stage") @PostMapping("/repo/{name}/stage")
public String stage(@PathVariable String name, @RequestParam List<String> files) throws IOException, GitAPIException public String stage(@PathVariable String name, @RequestParam List<String> files) throws IOException, GitAPIException
{ {
gitService.stageFiles(name, files); gitService.stageFiles(name, files);
return "redirect:/repo/" + name; return "redirect:/repo/" + name + "/changes";
} }
@PostMapping("/repo/{name}/unstage") @PostMapping("/repo/{name}/unstage")
public String unstage(@PathVariable String name, @RequestParam List<String> files) throws IOException, GitAPIException public String unstage(@PathVariable String name, @RequestParam List<String> files) throws IOException, GitAPIException
{ {
gitService.unstageFiles(name, files); gitService.unstageFiles(name, files);
return "redirect:/repo/" + name; return "redirect:/repo/" + name + "/changes";
} }
@PostMapping("/repo/{name}/commit") @PostMapping("/repo/{name}/commit")
public String commit(@PathVariable String name, @RequestParam String message) throws IOException, GitAPIException public String commit(@PathVariable String name, @RequestParam String message) throws IOException, GitAPIException
{ {
gitService.commit(name, message); gitService.commit(name, message);
return "redirect:/repo/" + name; return "redirect:/repo/" + name + "/changes";
} }
@PostMapping("/repo/{name}/push") @PostMapping("/repo/{name}/push")
public String push(@PathVariable String name) throws IOException, GitAPIException public String push(@PathVariable String name) throws IOException, GitAPIException
{ {
gitService.push(name); gitService.push(name);
return "redirect:/repo/" + name; return "redirect:/repo/" + name + "/remote";
} }
@PostMapping("/repo/{name}/pull") @PostMapping("/repo/{name}/pull")
public String pull(@PathVariable String name) throws IOException, GitAPIException public String pull(@PathVariable String name) throws IOException, GitAPIException
{ {
gitService.pull(name); gitService.pull(name);
return "redirect:/repo/" + name; return "redirect:/repo/" + name + "/remote";
} }
@PostMapping("/repo/{name}/delete") @PostMapping("/repo/{name}/delete")

View File

@@ -0,0 +1,41 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="'Branches - ' + ${name}">Branches</title>
</head>
<body>
<h2>Branches</h2>
<table border="0" cellpadding="4" cellspacing="0">
<tr>
<td>Current branch:</td>
<td><b th:text="${currentBranch}"></b></td>
</tr>
</table>
<h3>Switch Branch</h3>
<form method="post" th:action="@{/repo/{name}/checkout(name=${name})}">
<table border="0" cellpadding="4" cellspacing="0">
<tr>
<td>
<select name="branch">
<option th:each="b : ${branches}" th:value="${b}" th:text="${b}" th:selected="${b == currentBranch}"></option>
</select>
</td>
<td><input type="submit" value="Checkout"></td>
</tr>
</table>
</form>
<h3>Create New Branch</h3>
<form method="post" th:action="@{/repo/{name}/new-branch(name=${name})}">
<table border="0" cellpadding="4" cellspacing="0">
<tr>
<td><input type="text" name="branch" size="20"></td>
<td><input type="submit" value="Create"></td>
</tr>
</table>
</form>
</body>
</html>

View File

@@ -1,41 +1,10 @@
<!DOCTYPE html> <!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"> <html xmlns:th="http://www.thymeleaf.org">
<head> <head>
<title th:text="'WebGit - ' + ${name}">WebGit - repo</title> <title th:text="'Changes - ' + ${name}">Changes</title>
</head> </head>
<body> <body>
<h1><a href="/">WebGit</a></h1> <h2>Changes</h2>
<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> <h3>Modified Files (unstaged)</h3>
<form method="post" th:action="@{/repo/{name}/stage(name=${name})}" th:if="${!#lists.isEmpty(modifiedFiles)}"> <form method="post" th:action="@{/repo/{name}/stage(name=${name})}" th:if="${!#lists.isEmpty(modifiedFiles)}">
@@ -81,30 +50,5 @@ New branch: <input type="text" name="branch" size="20">
</table> </table>
</form> </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>
<hr>
<h3>Danger Zone</h3>
<form method="post" th:action="@{/repo/{name}/delete(name=${name})}">
<input type="submit" value="Delete Repository">
</form>
</body> </body>
</html> </html>

View File

@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<title>Clone Repository</title>
</head>
<body>
<h2>Clone a Repository</h2>
<form method="post" action="/clone" target="_top">
<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>

View File

@@ -0,0 +1,14 @@
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>WebGit</title>
</head>
<frameset cols="180,*">
<frame th:src="${selectedRepo != null} ? '/nav?repo=' + ${selectedRepo} : '/nav'" name="nav">
<frame th:src="${selectedRepo != null} ? '/repo/' + ${selectedRepo} + '/branches' : '/welcome'" name="content">
<noframes>
<body>
<p>Your browser does not support frames. <a href="/repos">Click here</a> to continue.</p>
</body>
</noframes>
</frameset>
</html>

View File

@@ -1,43 +0,0 @@
<!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>

View File

@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="'Manage - ' + ${name}">Manage</title>
</head>
<body>
<h2>Manage Repository</h2>
<h3>Danger Zone</h3>
<p>This will permanently delete the repository and its working tree.</p>
<form method="post" th:action="@{/repo/{name}/delete(name=${name})}" target="_top">
<input type="submit" value="Delete Repository">
</form>
</body>
</html>

View File

@@ -0,0 +1,33 @@
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Navigation</title>
</head>
<body>
<b>WebGit</b>
<hr>
<form method="get" action="/" target="_top">
<select name="repo">
<option value="">-- Select repo --</option>
<option th:each="r : ${repositories}" th:value="${r}" th:text="${r}" th:selected="${r == selectedRepo}"></option>
</select>
<br>
<input type="submit" value="Go">
</form>
<hr>
<a href="/repos" target="content">Repositories</a><br>
<a href="/clone-form" target="content">Clone New</a><br>
<th:block th:if="${selectedRepo != null}">
<hr>
<b th:text="${selectedRepo}"></b><br>
<a th:href="@{/repo/{name}/branches(name=${selectedRepo})}" target="content">Branches</a><br>
<a th:href="@{/repo/{name}/changes(name=${selectedRepo})}" target="content">Changes</a><br>
<a th:href="@{/repo/{name}/remote(name=${selectedRepo})}" target="content">Remote</a><br>
<a th:href="@{/repo/{name}/manage(name=${selectedRepo})}" target="content">Manage</a><br>
</th:block>
</body>
</html>

View File

@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="'Remote - ' + ${name}">Remote</title>
</head>
<body>
<h2>Remote</h2>
<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>

View File

@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Repositories</title>
</head>
<body>
<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}/branches(name=${repo})}">Open</a></td>
</tr>
<tr th:if="${#lists.isEmpty(repositories)}">
<td colspan="2"><i>No repositories cloned yet.</i></td>
</tr>
</table>
</body>
</html>

View File

@@ -0,0 +1,9 @@
<html>
<head>
<title>WebGit</title>
</head>
<body>
<h1>Welcome to WebGit</h1>
<p>Select a repository from the menu on the left, or clone a new one.</p>
</body>
</html>

View File

@@ -25,36 +25,93 @@ class HomeControllerTest
private GitService gitService; private GitService gitService;
@Test @Test
void homePageShowsRepositories() throws Exception void rootReturnsFrameset() throws Exception
{ {
when(gitService.listRepositories()).thenReturn(List.of("repo1", "repo2"));
mockMvc.perform(get("/")) mockMvc.perform(get("/"))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(view().name("home")) .andExpect(view().name("frameset"))
.andExpect(model().attribute("repositories", List.of("repo1", "repo2"))) .andExpect(model().attributeDoesNotExist("selectedRepo"));
.andExpect(content().string(org.hamcrest.Matchers.containsString("repo1")))
.andExpect(content().string(org.hamcrest.Matchers.containsString("repo2")));
} }
@Test @Test
void homePageShowsEmptyList() throws Exception void rootWithRepoSelectsRepo() throws Exception
{
mockMvc.perform(get("/").param("repo", "myrepo"))
.andExpect(status().isOk())
.andExpect(view().name("frameset"))
.andExpect(model().attribute("selectedRepo", "myrepo"));
}
@Test
void navShowsRepositories() throws Exception
{
when(gitService.listRepositories()).thenReturn(List.of("repo1", "repo2"));
mockMvc.perform(get("/nav"))
.andExpect(status().isOk())
.andExpect(view().name("nav"))
.andExpect(model().attribute("repositories", List.of("repo1", "repo2")))
.andExpect(model().attributeDoesNotExist("selectedRepo"));
}
@Test
void navWithSelectedRepo() throws Exception
{
when(gitService.listRepositories()).thenReturn(List.of("repo1"));
mockMvc.perform(get("/nav").param("repo", "repo1"))
.andExpect(status().isOk())
.andExpect(view().name("nav"))
.andExpect(model().attribute("selectedRepo", "repo1"))
.andExpect(content().string(org.hamcrest.Matchers.containsString("repo1")));
}
@Test
void welcomeReturnsWelcomePage() throws Exception
{
mockMvc.perform(get("/welcome"))
.andExpect(status().isOk())
.andExpect(view().name("welcome"));
}
@Test
void reposShowsRepositories() throws Exception
{
when(gitService.listRepositories()).thenReturn(List.of("alpha", "beta"));
mockMvc.perform(get("/repos"))
.andExpect(status().isOk())
.andExpect(view().name("repos"))
.andExpect(model().attribute("repositories", List.of("alpha", "beta")))
.andExpect(content().string(org.hamcrest.Matchers.containsString("alpha")));
}
@Test
void reposShowsEmptyList() throws Exception
{ {
when(gitService.listRepositories()).thenReturn(List.of()); when(gitService.listRepositories()).thenReturn(List.of());
mockMvc.perform(get("/")) mockMvc.perform(get("/repos"))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(content().string(org.hamcrest.Matchers.containsString("No repositories cloned yet."))); .andExpect(content().string(org.hamcrest.Matchers.containsString("No repositories cloned yet.")));
} }
@Test @Test
void cloneRedirectsToHome() throws Exception void cloneFormReturnsClonePage() throws Exception
{
mockMvc.perform(get("/clone-form"))
.andExpect(status().isOk())
.andExpect(view().name("clone"));
}
@Test
void cloneRedirectsToFramesetWithRepo() throws Exception
{ {
mockMvc.perform(post("/clone") mockMvc.perform(post("/clone")
.param("url", "https://example.com/repo.git") .param("url", "https://example.com/repo.git")
.param("name", "myrepo")) .param("name", "myrepo"))
.andExpect(status().is3xxRedirection()) .andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/")); .andExpect(redirectedUrl("/?repo=myrepo"));
verify(gitService).cloneRepository("https://example.com/repo.git", "myrepo"); verify(gitService).cloneRepository("https://example.com/repo.git", "myrepo");
} }

View File

@@ -25,118 +25,151 @@ class RepoControllerTest
private GitService gitService; private GitService gitService;
@Test @Test
void repoPageShowsDetails() throws Exception void repoRedirectsToBranches() throws Exception
{
mockMvc.perform(get("/repo/myrepo"))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/repo/myrepo/branches"));
}
@Test
void branchesPageShowsDetails() throws Exception
{ {
when(gitService.getCurrentBranch("myrepo")).thenReturn("main"); when(gitService.getCurrentBranch("myrepo")).thenReturn("main");
when(gitService.listBranches("myrepo")).thenReturn(List.of("main", "develop")); 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")) mockMvc.perform(get("/repo/myrepo/branches"))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(view().name("repo")) .andExpect(view().name("branches"))
.andExpect(model().attribute("name", "myrepo")) .andExpect(model().attribute("name", "myrepo"))
.andExpect(model().attribute("currentBranch", "main")) .andExpect(model().attribute("currentBranch", "main"))
.andExpect(model().attribute("branches", List.of("main", "develop"))) .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"))); .andExpect(content().string(org.hamcrest.Matchers.containsString("main")));
} }
@Test @Test
void repoPageShowsNoModifiedFiles() throws Exception void changesPageShowsFiles() throws Exception
{
when(gitService.getModifiedFiles("myrepo")).thenReturn(List.of("file1.txt"));
when(gitService.getStagedFiles("myrepo")).thenReturn(List.of("file2.txt"));
mockMvc.perform(get("/repo/myrepo/changes"))
.andExpect(status().isOk())
.andExpect(view().name("changes"))
.andExpect(model().attribute("name", "myrepo"))
.andExpect(model().attribute("modifiedFiles", List.of("file1.txt")))
.andExpect(model().attribute("stagedFiles", List.of("file2.txt")));
}
@Test
void changesPageShowsNoFiles() 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.getModifiedFiles("myrepo")).thenReturn(List.of());
when(gitService.getStagedFiles("myrepo")).thenReturn(List.of()); when(gitService.getStagedFiles("myrepo")).thenReturn(List.of());
mockMvc.perform(get("/repo/myrepo")) mockMvc.perform(get("/repo/myrepo/changes"))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(content().string(org.hamcrest.Matchers.containsString("No modified files."))) .andExpect(content().string(org.hamcrest.Matchers.containsString("No modified files.")))
.andExpect(content().string(org.hamcrest.Matchers.containsString("No staged files."))); .andExpect(content().string(org.hamcrest.Matchers.containsString("No staged files.")));
} }
@Test @Test
void checkoutRedirectsToRepo() throws Exception void remotePageLoads() throws Exception
{
mockMvc.perform(get("/repo/myrepo/remote"))
.andExpect(status().isOk())
.andExpect(view().name("remote"))
.andExpect(model().attribute("name", "myrepo"));
}
@Test
void managePageLoads() throws Exception
{
mockMvc.perform(get("/repo/myrepo/manage"))
.andExpect(status().isOk())
.andExpect(view().name("manage"))
.andExpect(model().attribute("name", "myrepo"));
}
@Test
void checkoutRedirectsToBranches() throws Exception
{ {
mockMvc.perform(post("/repo/myrepo/checkout") mockMvc.perform(post("/repo/myrepo/checkout")
.param("branch", "develop")) .param("branch", "develop"))
.andExpect(status().is3xxRedirection()) .andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/repo/myrepo")); .andExpect(redirectedUrl("/repo/myrepo/branches"));
verify(gitService).checkoutBranch("myrepo", "develop"); verify(gitService).checkoutBranch("myrepo", "develop");
} }
@Test @Test
void newBranchRedirectsToRepo() throws Exception void newBranchRedirectsToBranches() throws Exception
{ {
mockMvc.perform(post("/repo/myrepo/new-branch") mockMvc.perform(post("/repo/myrepo/new-branch")
.param("branch", "feature-z")) .param("branch", "feature-z"))
.andExpect(status().is3xxRedirection()) .andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/repo/myrepo")); .andExpect(redirectedUrl("/repo/myrepo/branches"));
verify(gitService).createAndCheckoutBranch("myrepo", "feature-z"); verify(gitService).createAndCheckoutBranch("myrepo", "feature-z");
} }
@Test @Test
void stageRedirectsToRepo() throws Exception void stageRedirectsToChanges() throws Exception
{ {
mockMvc.perform(post("/repo/myrepo/stage") mockMvc.perform(post("/repo/myrepo/stage")
.param("files", "a.txt") .param("files", "a.txt")
.param("files", "b.txt")) .param("files", "b.txt"))
.andExpect(status().is3xxRedirection()) .andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/repo/myrepo")); .andExpect(redirectedUrl("/repo/myrepo/changes"));
verify(gitService).stageFiles("myrepo", List.of("a.txt", "b.txt")); verify(gitService).stageFiles("myrepo", List.of("a.txt", "b.txt"));
} }
@Test @Test
void unstageRedirectsToRepo() throws Exception void unstageRedirectsToChanges() throws Exception
{ {
mockMvc.perform(post("/repo/myrepo/unstage") mockMvc.perform(post("/repo/myrepo/unstage")
.param("files", "a.txt") .param("files", "a.txt")
.param("files", "b.txt")) .param("files", "b.txt"))
.andExpect(status().is3xxRedirection()) .andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/repo/myrepo")); .andExpect(redirectedUrl("/repo/myrepo/changes"));
verify(gitService).unstageFiles("myrepo", List.of("a.txt", "b.txt")); verify(gitService).unstageFiles("myrepo", List.of("a.txt", "b.txt"));
} }
@Test @Test
void commitRedirectsToRepo() throws Exception void commitRedirectsToChanges() throws Exception
{ {
mockMvc.perform(post("/repo/myrepo/commit") mockMvc.perform(post("/repo/myrepo/commit")
.param("message", "test commit")) .param("message", "test commit"))
.andExpect(status().is3xxRedirection()) .andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/repo/myrepo")); .andExpect(redirectedUrl("/repo/myrepo/changes"));
verify(gitService).commit("myrepo", "test commit"); verify(gitService).commit("myrepo", "test commit");
} }
@Test @Test
void pushRedirectsToRepo() throws Exception void pushRedirectsToRemote() throws Exception
{ {
mockMvc.perform(post("/repo/myrepo/push")) mockMvc.perform(post("/repo/myrepo/push"))
.andExpect(status().is3xxRedirection()) .andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/repo/myrepo")); .andExpect(redirectedUrl("/repo/myrepo/remote"));
verify(gitService).push("myrepo"); verify(gitService).push("myrepo");
} }
@Test @Test
void pullRedirectsToRepo() throws Exception void pullRedirectsToRemote() throws Exception
{ {
mockMvc.perform(post("/repo/myrepo/pull")) mockMvc.perform(post("/repo/myrepo/pull"))
.andExpect(status().is3xxRedirection()) .andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/repo/myrepo")); .andExpect(redirectedUrl("/repo/myrepo/remote"));
verify(gitService).pull("myrepo"); verify(gitService).pull("myrepo");
} }
@Test @Test
void deleteRedirectsToHome() throws Exception void deleteRedirectsToRoot() throws Exception
{ {
mockMvc.perform(post("/repo/myrepo/delete")) mockMvc.perform(post("/repo/myrepo/delete"))
.andExpect(status().is3xxRedirection()) .andExpect(status().is3xxRedirection())