diff --git a/src/main/java/be/seeseepuff/webgit/controller/HomeController.java b/src/main/java/be/seeseepuff/webgit/controller/HomeController.java index 080356d..a3a2c6e 100644 --- a/src/main/java/be/seeseepuff/webgit/controller/HomeController.java +++ b/src/main/java/be/seeseepuff/webgit/controller/HomeController.java @@ -18,16 +18,43 @@ public class HomeController private final GitService gitService; @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()); - 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") public String cloneRepo(@RequestParam String url, @RequestParam String name) throws GitAPIException, IOException { gitService.cloneRepository(url, name); - return "redirect:/"; + return "redirect:/?repo=" + name; } } diff --git a/src/main/java/be/seeseepuff/webgit/controller/RepoController.java b/src/main/java/be/seeseepuff/webgit/controller/RepoController.java index 9c6ac8e..434bafb 100644 --- a/src/main/java/be/seeseepuff/webgit/controller/RepoController.java +++ b/src/main/java/be/seeseepuff/webgit/controller/RepoController.java @@ -20,63 +20,90 @@ public class RepoController private final GitService gitService; @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("currentBranch", gitService.getCurrentBranch(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("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") public String checkout(@PathVariable String name, @RequestParam String branch) throws IOException, GitAPIException { gitService.checkoutBranch(name, branch); - return "redirect:/repo/" + name; + return "redirect:/repo/" + name + "/branches"; } @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; + return "redirect:/repo/" + name + "/branches"; } @PostMapping("/repo/{name}/stage") public String stage(@PathVariable String name, @RequestParam List files) throws IOException, GitAPIException { gitService.stageFiles(name, files); - return "redirect:/repo/" + name; + return "redirect:/repo/" + name + "/changes"; } @PostMapping("/repo/{name}/unstage") public String unstage(@PathVariable String name, @RequestParam List files) throws IOException, GitAPIException { gitService.unstageFiles(name, files); - return "redirect:/repo/" + name; + return "redirect:/repo/" + name + "/changes"; } @PostMapping("/repo/{name}/commit") public String commit(@PathVariable String name, @RequestParam String message) throws IOException, GitAPIException { gitService.commit(name, message); - return "redirect:/repo/" + name; + return "redirect:/repo/" + name + "/changes"; } @PostMapping("/repo/{name}/push") public String push(@PathVariable String name) throws IOException, GitAPIException { gitService.push(name); - return "redirect:/repo/" + name; + return "redirect:/repo/" + name + "/remote"; } @PostMapping("/repo/{name}/pull") public String pull(@PathVariable String name) throws IOException, GitAPIException { gitService.pull(name); - return "redirect:/repo/" + name; + return "redirect:/repo/" + name + "/remote"; } @PostMapping("/repo/{name}/delete") diff --git a/src/main/resources/templates/branches.html b/src/main/resources/templates/branches.html new file mode 100644 index 0000000..a8c31e5 --- /dev/null +++ b/src/main/resources/templates/branches.html @@ -0,0 +1,41 @@ + + + +Branches + + +

Branches

+ + + + + + +
Current branch:
+ +

Switch Branch

+
+ + + + + +
+ +
+
+ +

Create New Branch

+
+ + + + + +
+
+ + + diff --git a/src/main/resources/templates/repo.html b/src/main/resources/templates/changes.html similarity index 51% rename from src/main/resources/templates/repo.html rename to src/main/resources/templates/changes.html index dc6ce63..d93bcb1 100644 --- a/src/main/resources/templates/repo.html +++ b/src/main/resources/templates/changes.html @@ -1,41 +1,10 @@ -WebGit - repo +Changes -

WebGit

-

repo

- -

Branch

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

Changes

Modified Files (unstaged)

@@ -81,30 +50,5 @@ New branch:
-
- -

Remote

- - - - - -
-
- -
-
-
- -
-
- -
- -

Danger Zone

-
- -
- diff --git a/src/main/resources/templates/clone.html b/src/main/resources/templates/clone.html new file mode 100644 index 0000000..b626d32 --- /dev/null +++ b/src/main/resources/templates/clone.html @@ -0,0 +1,25 @@ + + + +Clone Repository + + +

Clone a Repository

+
+ + + + + + + + + + + + + +
URL:
Name:
+
+ + diff --git a/src/main/resources/templates/frameset.html b/src/main/resources/templates/frameset.html new file mode 100644 index 0000000..78e20af --- /dev/null +++ b/src/main/resources/templates/frameset.html @@ -0,0 +1,14 @@ + + +WebGit + + + + + +<body> +<p>Your browser does not support frames. <a href="/repos">Click here</a> to continue.</p> +</body> + + + diff --git a/src/main/resources/templates/home.html b/src/main/resources/templates/home.html deleted file mode 100644 index 30e7ecf..0000000 --- a/src/main/resources/templates/home.html +++ /dev/null @@ -1,43 +0,0 @@ - - - -WebGit - - -

WebGit

- -

Repositories

- - - - - - - - - - - - -
NameActions
Open
No repositories cloned yet.
- -

Clone a Repository

-
- - - - - - - - - - - - - -
URL:
Name:
-
- - - diff --git a/src/main/resources/templates/manage.html b/src/main/resources/templates/manage.html new file mode 100644 index 0000000..3c3ff13 --- /dev/null +++ b/src/main/resources/templates/manage.html @@ -0,0 +1,16 @@ + + + +Manage + + +

Manage Repository

+ +

Danger Zone

+

This will permanently delete the repository and its working tree.

+
+ +
+ + + diff --git a/src/main/resources/templates/nav.html b/src/main/resources/templates/nav.html new file mode 100644 index 0000000..6cf8432 --- /dev/null +++ b/src/main/resources/templates/nav.html @@ -0,0 +1,33 @@ + + +Navigation + + +WebGit +
+ +
+ +
+ +
+ +
+ +Repositories
+Clone New
+ + +
+
+Branches
+Changes
+Remote
+Manage
+
+ + + diff --git a/src/main/resources/templates/remote.html b/src/main/resources/templates/remote.html new file mode 100644 index 0000000..ac6b0ad --- /dev/null +++ b/src/main/resources/templates/remote.html @@ -0,0 +1,25 @@ + + + +Remote + + +

Remote

+ + + + + + +
+
+ +
+
+
+ +
+
+ + + diff --git a/src/main/resources/templates/repos.html b/src/main/resources/templates/repos.html new file mode 100644 index 0000000..5c227f2 --- /dev/null +++ b/src/main/resources/templates/repos.html @@ -0,0 +1,22 @@ + + + +Repositories + + +

Repositories

+ + + + + + + + + + + + +
NameActions
Open
No repositories cloned yet.
+ + diff --git a/src/main/resources/templates/welcome.html b/src/main/resources/templates/welcome.html new file mode 100644 index 0000000..0fb38b8 --- /dev/null +++ b/src/main/resources/templates/welcome.html @@ -0,0 +1,9 @@ + + +WebGit + + +

Welcome to WebGit

+

Select a repository from the menu on the left, or clone a new one.

+ + diff --git a/src/test/java/be/seeseepuff/webgit/controller/HomeControllerTest.java b/src/test/java/be/seeseepuff/webgit/controller/HomeControllerTest.java index 767b386..2706e6c 100644 --- a/src/test/java/be/seeseepuff/webgit/controller/HomeControllerTest.java +++ b/src/test/java/be/seeseepuff/webgit/controller/HomeControllerTest.java @@ -25,36 +25,93 @@ class HomeControllerTest private GitService gitService; @Test - void homePageShowsRepositories() throws Exception + void rootReturnsFrameset() 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"))); + .andExpect(view().name("frameset")) + .andExpect(model().attributeDoesNotExist("selectedRepo")); } @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()); - mockMvc.perform(get("/")) + mockMvc.perform(get("/repos")) .andExpect(status().isOk()) .andExpect(content().string(org.hamcrest.Matchers.containsString("No repositories cloned yet."))); } @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") .param("url", "https://example.com/repo.git") .param("name", "myrepo")) .andExpect(status().is3xxRedirection()) - .andExpect(redirectedUrl("/")); + .andExpect(redirectedUrl("/?repo=myrepo")); 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 index 6d97c3f..2506ef7 100644 --- a/src/test/java/be/seeseepuff/webgit/controller/RepoControllerTest.java +++ b/src/test/java/be/seeseepuff/webgit/controller/RepoControllerTest.java @@ -25,118 +25,151 @@ class RepoControllerTest private GitService gitService; @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.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(view().name("repo")) + .andExpect(view().name("branches")) .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 + 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.getStagedFiles("myrepo")).thenReturn(List.of()); - mockMvc.perform(get("/repo/myrepo")) + mockMvc.perform(get("/repo/myrepo/changes")) .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 + 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") .param("branch", "develop")) .andExpect(status().is3xxRedirection()) - .andExpect(redirectedUrl("/repo/myrepo")); + .andExpect(redirectedUrl("/repo/myrepo/branches")); verify(gitService).checkoutBranch("myrepo", "develop"); } @Test - void newBranchRedirectsToRepo() throws Exception + void newBranchRedirectsToBranches() throws Exception { mockMvc.perform(post("/repo/myrepo/new-branch") .param("branch", "feature-z")) .andExpect(status().is3xxRedirection()) - .andExpect(redirectedUrl("/repo/myrepo")); + .andExpect(redirectedUrl("/repo/myrepo/branches")); verify(gitService).createAndCheckoutBranch("myrepo", "feature-z"); } @Test - void stageRedirectsToRepo() throws Exception + void stageRedirectsToChanges() throws Exception { mockMvc.perform(post("/repo/myrepo/stage") .param("files", "a.txt") .param("files", "b.txt")) .andExpect(status().is3xxRedirection()) - .andExpect(redirectedUrl("/repo/myrepo")); + .andExpect(redirectedUrl("/repo/myrepo/changes")); verify(gitService).stageFiles("myrepo", List.of("a.txt", "b.txt")); } @Test - void unstageRedirectsToRepo() throws Exception + void unstageRedirectsToChanges() throws Exception { mockMvc.perform(post("/repo/myrepo/unstage") .param("files", "a.txt") .param("files", "b.txt")) .andExpect(status().is3xxRedirection()) - .andExpect(redirectedUrl("/repo/myrepo")); + .andExpect(redirectedUrl("/repo/myrepo/changes")); verify(gitService).unstageFiles("myrepo", List.of("a.txt", "b.txt")); } @Test - void commitRedirectsToRepo() throws Exception + void commitRedirectsToChanges() throws Exception { mockMvc.perform(post("/repo/myrepo/commit") .param("message", "test commit")) .andExpect(status().is3xxRedirection()) - .andExpect(redirectedUrl("/repo/myrepo")); + .andExpect(redirectedUrl("/repo/myrepo/changes")); verify(gitService).commit("myrepo", "test commit"); } @Test - void pushRedirectsToRepo() throws Exception + void pushRedirectsToRemote() throws Exception { mockMvc.perform(post("/repo/myrepo/push")) .andExpect(status().is3xxRedirection()) - .andExpect(redirectedUrl("/repo/myrepo")); + .andExpect(redirectedUrl("/repo/myrepo/remote")); verify(gitService).push("myrepo"); } @Test - void pullRedirectsToRepo() throws Exception + void pullRedirectsToRemote() throws Exception { mockMvc.perform(post("/repo/myrepo/pull")) .andExpect(status().is3xxRedirection()) - .andExpect(redirectedUrl("/repo/myrepo")); + .andExpect(redirectedUrl("/repo/myrepo/remote")); verify(gitService).pull("myrepo"); } @Test - void deleteRedirectsToHome() throws Exception + void deleteRedirectsToRoot() throws Exception { mockMvc.perform(post("/repo/myrepo/delete")) .andExpect(status().is3xxRedirection())