diff --git a/src/main/java/be/seeseepuff/webgit/service/GitService.java b/src/main/java/be/seeseepuff/webgit/service/GitService.java index fc34981..dad031a 100644 --- a/src/main/java/be/seeseepuff/webgit/service/GitService.java +++ b/src/main/java/be/seeseepuff/webgit/service/GitService.java @@ -619,7 +619,20 @@ public class GitService if (properties.getUsername() != null) cmd.setCredentialsProvider(new org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider( properties.getUsername(), properties.getPassword())); - cmd.call(); + Iterable results = cmd.call(); + for (var result : results) + { + for (var update : result.getRemoteUpdates()) + { + var status = update.getStatus(); + if (status != org.eclipse.jgit.transport.RemoteRefUpdate.Status.OK + && status != org.eclipse.jgit.transport.RemoteRefUpdate.Status.UP_TO_DATE) + { + throw new IOException("Push failed: " + update.getRemoteName() + " " + status + + (update.getMessage() != null ? " - " + update.getMessage() : "")); + } + } + } } } @@ -631,7 +644,11 @@ public class GitService if (properties.getUsername() != null) cmd.setCredentialsProvider(new org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider( properties.getUsername(), properties.getPassword())); - cmd.call(); + var result = cmd.call(); + if (!result.isSuccessful()) + { + throw new IOException("Pull failed: " + result.getMergeResult().getMergeStatus()); + } } } diff --git a/src/test/java/be/seeseepuff/webgit/controller/RepoControllerTest.java b/src/test/java/be/seeseepuff/webgit/controller/RepoControllerTest.java index e6c17b1..a5f0f5d 100644 --- a/src/test/java/be/seeseepuff/webgit/controller/RepoControllerTest.java +++ b/src/test/java/be/seeseepuff/webgit/controller/RepoControllerTest.java @@ -351,4 +351,78 @@ class RepoControllerTest .andExpect(model().attribute("filePath", "src/main.txt")) .andExpect(content().string(org.hamcrest.Matchers.containsString("+hello"))); } + + @Test + void changesPageShowsAheadBehind() throws Exception + { + when(gitService.getModifiedFiles("myrepo")).thenReturn(List.of()); + when(gitService.getStagedFiles("myrepo")).thenReturn(List.of()); + when(gitService.getAheadBehind("myrepo")).thenReturn(new int[]{3, 1}); + + mockMvc.perform(get("/repo/myrepo/changes")) + .andExpect(status().isOk()) + .andExpect(model().attribute("commitsAhead", 3)) + .andExpect(model().attribute("commitsBehind", 1)); + } + + @Test + void changesPageHidesAheadBehindWhenNoTracking() throws Exception + { + when(gitService.getModifiedFiles("myrepo")).thenReturn(List.of()); + when(gitService.getStagedFiles("myrepo")).thenReturn(List.of()); + when(gitService.getAheadBehind("myrepo")).thenReturn(null); + + mockMvc.perform(get("/repo/myrepo/changes")) + .andExpect(status().isOk()) + .andExpect(model().attributeDoesNotExist("commitsAhead")) + .andExpect(model().attributeDoesNotExist("commitsBehind")); + } + + @Test + void pushRedirectsToChangesWhenRequested() throws Exception + { + mockMvc.perform(post("/repo/myrepo/push") + .param("redirectTo", "changes")) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/repo/myrepo/changes")); + + verify(gitService).push("myrepo"); + } + + @Test + void pullRedirectsToChangesWhenRequested() throws Exception + { + mockMvc.perform(post("/repo/myrepo/pull") + .param("redirectTo", "changes")) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/repo/myrepo/changes")); + + verify(gitService).pull("myrepo"); + } + + @Test + void stageWithSelectAllStagesAllModifiedFiles() throws Exception + { + when(gitService.getModifiedFiles("myrepo")).thenReturn(List.of("a.txt", "b.txt", "c.txt")); + + mockMvc.perform(post("/repo/myrepo/stage") + .param("selectAll", "on")) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/repo/myrepo/changes")); + + verify(gitService).stageFiles("myrepo", List.of("a.txt", "b.txt", "c.txt")); + } + + @Test + void unstageWithSelectAllUnstagesAllStagedFiles() throws Exception + { + when(gitService.getStagedFiles("myrepo")).thenReturn(List.of("a.txt", "b.txt")); + + mockMvc.perform(post("/repo/myrepo/unstage") + .param("selectAll", "on")) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/repo/myrepo/changes")); + + verify(gitService).unstageFiles("myrepo", List.of("a.txt", "b.txt")); + } } diff --git a/src/test/java/be/seeseepuff/webgit/service/GitServiceTest.java b/src/test/java/be/seeseepuff/webgit/service/GitServiceTest.java index ca678b5..c24eeb5 100644 --- a/src/test/java/be/seeseepuff/webgit/service/GitServiceTest.java +++ b/src/test/java/be/seeseepuff/webgit/service/GitServiceTest.java @@ -561,4 +561,72 @@ class GitServiceTest assertEquals("# Test", Files.readString(readme)); } + + @Test + void getAheadBehindReturnsZeroWhenUpToDate() throws GitAPIException, IOException + { + gitService.cloneRepository(bareRemote.toUri().toString(), "myrepo"); + + int[] result = gitService.getAheadBehind("myrepo"); + assertNotNull(result); + assertEquals(0, result[0]); + assertEquals(0, result[1]); + } + + @Test + void getAheadBehindReturnsAheadCount() throws GitAPIException, IOException + { + gitService.cloneRepository(bareRemote.toUri().toString(), "myrepo"); + Files.writeString(worktreePath.resolve("myrepo/newfile.txt"), "hello"); + gitService.stageFiles("myrepo", List.of("newfile.txt")); + gitService.commit("myrepo", "Local commit"); + + int[] result = gitService.getAheadBehind("myrepo"); + assertNotNull(result); + assertEquals(1, result[0]); + assertEquals(0, result[1]); + } + + @Test + void getAheadBehindReturnsBehindCount() throws GitAPIException, IOException + { + gitService.cloneRepository(bareRemote.toUri().toString(), "myrepo"); + + // Push a commit to the remote via a second clone + Path tmpWork = tempDir.resolve("tmp-work2"); + try (Git tmp = Git.cloneRepository() + .setURI(bareRemote.toUri().toString()) + .setDirectory(tmpWork.toFile()) + .call()) + { + Files.writeString(tmpWork.resolve("remote-file.txt"), "remote"); + tmp.add().addFilepattern("remote-file.txt").call(); + tmp.commit().setMessage("Remote commit").call(); + tmp.push().call(); + } + + // Fetch so our repo knows about the remote commit + try (Git git = Git.open(worktreePath.resolve("myrepo").toFile())) + { + git.fetch().call(); + } + + int[] result = gitService.getAheadBehind("myrepo"); + assertNotNull(result); + assertEquals(0, result[0]); + assertEquals(1, result[1]); + } + + @Test + void pushFailsWithBadRemote() throws GitAPIException, IOException + { + gitService.cloneRepository(bareRemote.toUri().toString(), "myrepo"); + gitService.updateRemoteUrl("myrepo", "origin", "https://invalid.example.com/repo.git"); + + Files.writeString(worktreePath.resolve("myrepo/newfile.txt"), "hello"); + gitService.stageFiles("myrepo", List.of("newfile.txt")); + gitService.commit("myrepo", "Local commit"); + + assertThrows(Exception.class, () -> gitService.push("myrepo")); + } }