Add unstage files feature
Add GitService.unstageFiles() using JGit reset to move files from
staged back to modified. Exposed via POST /repo/{name}/unstage
endpoint, repo page with checkboxes, and telnet menu option 7.
Includes unit tests for all layers.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -35,3 +35,6 @@ out/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
||||
# WebGit
|
||||
/webgit/
|
||||
|
||||
@@ -51,6 +51,13 @@ public class RepoController
|
||||
return "redirect:/repo/" + name;
|
||||
}
|
||||
|
||||
@PostMapping("/repo/{name}/unstage")
|
||||
public String unstage(@PathVariable String name, @RequestParam List<String> files) throws IOException, GitAPIException
|
||||
{
|
||||
gitService.unstageFiles(name, files);
|
||||
return "redirect:/repo/" + name;
|
||||
}
|
||||
|
||||
@PostMapping("/repo/{name}/commit")
|
||||
public String commit(@PathVariable String name, @RequestParam String message) throws IOException, GitAPIException
|
||||
{
|
||||
|
||||
@@ -158,6 +158,19 @@ public class GitService
|
||||
}
|
||||
}
|
||||
|
||||
public void unstageFiles(String name, List<String> files) throws IOException, GitAPIException
|
||||
{
|
||||
try (Git git = openRepository(name))
|
||||
{
|
||||
var reset = git.reset();
|
||||
for (String file : files)
|
||||
{
|
||||
reset.addPath(file);
|
||||
}
|
||||
reset.call();
|
||||
}
|
||||
}
|
||||
|
||||
public void push(String name) throws IOException, GitAPIException
|
||||
{
|
||||
try (Git git = openRepository(name))
|
||||
|
||||
@@ -151,9 +151,10 @@ public class TelnetSession implements Runnable
|
||||
out.println(" 4. Show modified files");
|
||||
out.println(" 5. Show staged files");
|
||||
out.println(" 6. Stage files");
|
||||
out.println(" 7. Commit");
|
||||
out.println(" 8. Push");
|
||||
out.println(" 9. Pull");
|
||||
out.println(" 7. Unstage files");
|
||||
out.println(" 8. Commit");
|
||||
out.println(" 9. Push");
|
||||
out.println(" 10. Pull");
|
||||
out.println(" b. Back");
|
||||
out.print("> ");
|
||||
out.flush();
|
||||
@@ -170,9 +171,10 @@ public class TelnetSession implements Runnable
|
||||
case "4" -> showModifiedFiles(name);
|
||||
case "5" -> showStagedFiles(name);
|
||||
case "6" -> stageFiles(name);
|
||||
case "7" -> commitChanges(name);
|
||||
case "8" -> push(name);
|
||||
case "9" -> pull(name);
|
||||
case "7" -> unstageFiles(name);
|
||||
case "8" -> commitChanges(name);
|
||||
case "9" -> push(name);
|
||||
case "10" -> pull(name);
|
||||
default -> out.println("Invalid choice.");
|
||||
}
|
||||
}
|
||||
@@ -295,6 +297,56 @@ public class TelnetSession implements Runnable
|
||||
}
|
||||
}
|
||||
|
||||
private void unstageFiles(String name) throws IOException, GitAPIException
|
||||
{
|
||||
List<String> files = gitService.getStagedFiles(name);
|
||||
if (files.isEmpty())
|
||||
{
|
||||
out.println("No staged files to unstage.");
|
||||
return;
|
||||
}
|
||||
|
||||
out.println("Staged files:");
|
||||
for (int i = 0; i < files.size(); i++)
|
||||
{
|
||||
out.println(" " + (i + 1) + ". " + files.get(i));
|
||||
}
|
||||
out.print("Enter numbers to unstage (comma-separated, or 'a' for all): ");
|
||||
out.flush();
|
||||
|
||||
String input = in.readLine();
|
||||
if (input == null || input.isBlank())
|
||||
return;
|
||||
|
||||
List<String> toUnstage;
|
||||
if ("a".equalsIgnoreCase(input.trim()))
|
||||
{
|
||||
toUnstage = files;
|
||||
}
|
||||
else
|
||||
{
|
||||
toUnstage = new java.util.ArrayList<>();
|
||||
for (String part : input.split(","))
|
||||
{
|
||||
try
|
||||
{
|
||||
int idx = Integer.parseInt(part.trim()) - 1;
|
||||
if (idx >= 0 && idx < files.size())
|
||||
toUnstage.add(files.get(idx));
|
||||
}
|
||||
catch (NumberFormatException ignored)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!toUnstage.isEmpty())
|
||||
{
|
||||
gitService.unstageFiles(name, toUnstage);
|
||||
out.println("Unstaged " + toUnstage.size() + " file(s).");
|
||||
}
|
||||
}
|
||||
|
||||
private void commitChanges(String name) throws IOException, GitAPIException
|
||||
{
|
||||
out.print("Commit message: ");
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
spring.application.name=webgit
|
||||
webgit.git-dir-path=./webgit/gitdir
|
||||
webgit.worktree-path=./webgit/worktree
|
||||
webgit.telnet-port=2323
|
||||
|
||||
@@ -55,14 +55,20 @@ New branch: <input type="text" name="branch" size="20">
|
||||
<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)}">
|
||||
<form method="post" th:action="@{/repo/{name}/unstage(name=${name})}" th:if="${!#lists.isEmpty(stagedFiles)}">
|
||||
<table border="1" cellpadding="4" cellspacing="0">
|
||||
<tr>
|
||||
<th>Unstage</th>
|
||||
<th>File</th>
|
||||
</tr>
|
||||
<tr th:each="file : ${stagedFiles}">
|
||||
<td><input type="checkbox" name="files" th:value="${file}"></td>
|
||||
<td th:text="${file}"></td>
|
||||
</tr>
|
||||
</table>
|
||||
<br>
|
||||
<input type="submit" value="Unstage Selected">
|
||||
</form>
|
||||
<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)}">
|
||||
|
||||
@@ -10,9 +10,4 @@ class WebgitApplicationTests {
|
||||
void contextLoads() {
|
||||
}
|
||||
|
||||
@Test
|
||||
void mainMethodRuns() {
|
||||
WebgitApplication.main(new String[]{});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -92,6 +92,18 @@ class RepoControllerTest
|
||||
verify(gitService).stageFiles("myrepo", List.of("a.txt", "b.txt"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void unstageRedirectsToRepo() throws Exception
|
||||
{
|
||||
mockMvc.perform(post("/repo/myrepo/unstage")
|
||||
.param("files", "a.txt")
|
||||
.param("files", "b.txt"))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrl("/repo/myrepo"));
|
||||
|
||||
verify(gitService).unstageFiles("myrepo", List.of("a.txt", "b.txt"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void commitRedirectsToRepo() throws Exception
|
||||
{
|
||||
|
||||
@@ -209,6 +209,20 @@ class GitServiceTest
|
||||
assertDoesNotThrow(() -> gitService.push("myrepo"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void unstageFilesMovesBackToModified() throws GitAPIException, IOException
|
||||
{
|
||||
gitService.cloneRepository(bareRemote.toUri().toString(), "myrepo");
|
||||
Files.writeString(worktreePath.resolve("myrepo/newfile.txt"), "hello");
|
||||
gitService.stageFiles("myrepo", List.of("newfile.txt"));
|
||||
assertTrue(gitService.getStagedFiles("myrepo").contains("newfile.txt"));
|
||||
|
||||
gitService.unstageFiles("myrepo", List.of("newfile.txt"));
|
||||
|
||||
assertFalse(gitService.getStagedFiles("myrepo").contains("newfile.txt"));
|
||||
assertTrue(gitService.getModifiedFiles("myrepo").contains("newfile.txt"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void pullFetchesFromRemote() throws GitAPIException, IOException
|
||||
{
|
||||
|
||||
@@ -257,12 +257,54 @@ class TelnetSessionTest
|
||||
verify(gitService, never()).stageFiles(anyString(), anyList());
|
||||
}
|
||||
|
||||
@Test
|
||||
void repoMenuUnstageAllFiles() throws IOException, GitAPIException
|
||||
{
|
||||
when(gitService.listRepositories()).thenReturn(List.of("myrepo"));
|
||||
when(gitService.getCurrentBranch("myrepo")).thenReturn("main");
|
||||
when(gitService.getStagedFiles("myrepo")).thenReturn(List.of("a.txt", "b.txt"));
|
||||
String output = runSession("3\n1\n7\na\nb\nq\n");
|
||||
verify(gitService).unstageFiles("myrepo", List.of("a.txt", "b.txt"));
|
||||
assertTrue(output.contains("Unstaged 2 file(s)."));
|
||||
}
|
||||
|
||||
@Test
|
||||
void repoMenuUnstageSelectedFiles() throws IOException, GitAPIException
|
||||
{
|
||||
when(gitService.listRepositories()).thenReturn(List.of("myrepo"));
|
||||
when(gitService.getCurrentBranch("myrepo")).thenReturn("main");
|
||||
when(gitService.getStagedFiles("myrepo")).thenReturn(List.of("a.txt", "b.txt", "c.txt"));
|
||||
String output = runSession("3\n1\n7\n1,3\nb\nq\n");
|
||||
verify(gitService).unstageFiles("myrepo", List.of("a.txt", "c.txt"));
|
||||
assertTrue(output.contains("Unstaged 2 file(s)."));
|
||||
}
|
||||
|
||||
@Test
|
||||
void repoMenuUnstageNoStagedFiles() throws IOException, GitAPIException
|
||||
{
|
||||
when(gitService.listRepositories()).thenReturn(List.of("myrepo"));
|
||||
when(gitService.getCurrentBranch("myrepo")).thenReturn("main");
|
||||
when(gitService.getStagedFiles("myrepo")).thenReturn(List.of());
|
||||
String output = runSession("3\n1\n7\nb\nq\n");
|
||||
assertTrue(output.contains("No staged files to unstage."));
|
||||
}
|
||||
|
||||
@Test
|
||||
void repoMenuUnstageEmptyInput() throws IOException, GitAPIException
|
||||
{
|
||||
when(gitService.listRepositories()).thenReturn(List.of("myrepo"));
|
||||
when(gitService.getCurrentBranch("myrepo")).thenReturn("main");
|
||||
when(gitService.getStagedFiles("myrepo")).thenReturn(List.of("a.txt"));
|
||||
String output = runSession("3\n1\n7\n\nb\nq\n");
|
||||
verify(gitService, never()).unstageFiles(anyString(), anyList());
|
||||
}
|
||||
|
||||
@Test
|
||||
void repoMenuCommit() throws IOException, GitAPIException
|
||||
{
|
||||
when(gitService.listRepositories()).thenReturn(List.of("myrepo"));
|
||||
when(gitService.getCurrentBranch("myrepo")).thenReturn("main");
|
||||
String output = runSession("3\n1\n7\nmy commit msg\nb\nq\n");
|
||||
String output = runSession("3\n1\n8\nmy commit msg\nb\nq\n");
|
||||
verify(gitService).commit("myrepo", "my commit msg");
|
||||
assertTrue(output.contains("Committed."));
|
||||
}
|
||||
@@ -272,7 +314,7 @@ class TelnetSessionTest
|
||||
{
|
||||
when(gitService.listRepositories()).thenReturn(List.of("myrepo"));
|
||||
when(gitService.getCurrentBranch("myrepo")).thenReturn("main");
|
||||
String output = runSession("3\n1\n7\n\nb\nq\n");
|
||||
String output = runSession("3\n1\n8\n\nb\nq\n");
|
||||
verify(gitService, never()).commit(anyString(), anyString());
|
||||
}
|
||||
|
||||
@@ -281,7 +323,7 @@ class TelnetSessionTest
|
||||
{
|
||||
when(gitService.listRepositories()).thenReturn(List.of("myrepo"));
|
||||
when(gitService.getCurrentBranch("myrepo")).thenReturn("main");
|
||||
String output = runSession("3\n1\n8\nb\nq\n");
|
||||
String output = runSession("3\n1\n9\nb\nq\n");
|
||||
verify(gitService).push("myrepo");
|
||||
assertTrue(output.contains("Pushed."));
|
||||
}
|
||||
@@ -291,7 +333,7 @@ class TelnetSessionTest
|
||||
{
|
||||
when(gitService.listRepositories()).thenReturn(List.of("myrepo"));
|
||||
when(gitService.getCurrentBranch("myrepo")).thenReturn("main");
|
||||
String output = runSession("3\n1\n9\nb\nq\n");
|
||||
String output = runSession("3\n1\n10\nb\nq\n");
|
||||
verify(gitService).pull("myrepo");
|
||||
assertTrue(output.contains("Pulled."));
|
||||
}
|
||||
@@ -404,7 +446,7 @@ class TelnetSessionTest
|
||||
{
|
||||
when(gitService.listRepositories()).thenReturn(List.of("myrepo"));
|
||||
when(gitService.getCurrentBranch("myrepo")).thenReturn("main");
|
||||
String output = runSession("3\n1\n7\n");
|
||||
String output = runSession("3\n1\n8\n");
|
||||
verify(gitService, never()).commit(anyString(), anyString());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user