Add telnet interface for FreeDOS users
Telnet server with text-based menu for all Git operations. Enabled via webgit.telnet.enabled=true property, defaults to port 2323. Includes unit tests for session and server. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -15,4 +15,5 @@ public class WebgitProperties
|
|||||||
{
|
{
|
||||||
private Path worktreePath;
|
private Path worktreePath;
|
||||||
private Path gitDirPath;
|
private Path gitDirPath;
|
||||||
|
private Integer telnetPort;
|
||||||
}
|
}
|
||||||
|
|||||||
84
src/main/java/be/seeseepuff/webgit/telnet/TelnetServer.java
Normal file
84
src/main/java/be/seeseepuff/webgit/telnet/TelnetServer.java
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
package be.seeseepuff.webgit.telnet;
|
||||||
|
|
||||||
|
import be.seeseepuff.webgit.config.WebgitProperties;
|
||||||
|
import be.seeseepuff.webgit.service.GitService;
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import jakarta.annotation.PreDestroy;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.net.ServerSocket;
|
||||||
|
import java.net.Socket;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@ConditionalOnProperty(name = "webgit.telnet.enabled", havingValue = "true")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class TelnetServer
|
||||||
|
{
|
||||||
|
private final GitService gitService;
|
||||||
|
private final WebgitProperties properties;
|
||||||
|
private ServerSocket serverSocket;
|
||||||
|
private Thread acceptThread;
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void start() throws IOException
|
||||||
|
{
|
||||||
|
int port = properties.getTelnetPort() != null ? properties.getTelnetPort() : 2323;
|
||||||
|
serverSocket = new ServerSocket(port);
|
||||||
|
log.info("Telnet server listening on port {}", port);
|
||||||
|
|
||||||
|
acceptThread = new Thread(this::acceptConnections, "telnet-accept");
|
||||||
|
acceptThread.setDaemon(true);
|
||||||
|
acceptThread.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreDestroy
|
||||||
|
public void stop() throws IOException
|
||||||
|
{
|
||||||
|
if (serverSocket != null && !serverSocket.isClosed())
|
||||||
|
{
|
||||||
|
serverSocket.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void acceptConnections()
|
||||||
|
{
|
||||||
|
while (!serverSocket.isClosed())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Socket socket = serverSocket.accept();
|
||||||
|
log.info("Telnet connection from {}", socket.getRemoteSocketAddress());
|
||||||
|
|
||||||
|
Thread handler = new Thread(() -> handleConnection(socket), "telnet-client");
|
||||||
|
handler.setDaemon(true);
|
||||||
|
handler.start();
|
||||||
|
}
|
||||||
|
catch (IOException e)
|
||||||
|
{
|
||||||
|
if (!serverSocket.isClosed())
|
||||||
|
{
|
||||||
|
log.error("Error accepting telnet connection", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleConnection(Socket socket)
|
||||||
|
{
|
||||||
|
try (socket;
|
||||||
|
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
|
||||||
|
PrintWriter out = new PrintWriter(socket.getOutputStream(), true))
|
||||||
|
{
|
||||||
|
new TelnetSession(gitService, in, out).run();
|
||||||
|
}
|
||||||
|
catch (IOException e)
|
||||||
|
{
|
||||||
|
log.error("Error handling telnet connection", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
320
src/main/java/be/seeseepuff/webgit/telnet/TelnetSession.java
Normal file
320
src/main/java/be/seeseepuff/webgit/telnet/TelnetSession.java
Normal file
@@ -0,0 +1,320 @@
|
|||||||
|
package be.seeseepuff.webgit.telnet;
|
||||||
|
|
||||||
|
import be.seeseepuff.webgit.service.GitService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class TelnetSession implements Runnable
|
||||||
|
{
|
||||||
|
private final GitService gitService;
|
||||||
|
private final BufferedReader in;
|
||||||
|
private final PrintWriter out;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
out.println("Welcome to WebGit Telnet Interface");
|
||||||
|
out.println("==================================");
|
||||||
|
mainMenu();
|
||||||
|
}
|
||||||
|
catch (IOException | GitAPIException e)
|
||||||
|
{
|
||||||
|
out.println("Error: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void mainMenu() throws IOException, GitAPIException
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
out.println();
|
||||||
|
out.println("Main Menu:");
|
||||||
|
out.println(" 1. List repositories");
|
||||||
|
out.println(" 2. Clone a repository");
|
||||||
|
out.println(" 3. Open a repository");
|
||||||
|
out.println(" q. Quit");
|
||||||
|
out.print("> ");
|
||||||
|
out.flush();
|
||||||
|
|
||||||
|
String choice = in.readLine();
|
||||||
|
if (choice == null || "q".equalsIgnoreCase(choice))
|
||||||
|
{
|
||||||
|
out.println("Goodbye!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (choice.trim())
|
||||||
|
{
|
||||||
|
case "1" -> listRepositories();
|
||||||
|
case "2" -> cloneRepository();
|
||||||
|
case "3" -> openRepository();
|
||||||
|
default -> out.println("Invalid choice.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void listRepositories() throws IOException
|
||||||
|
{
|
||||||
|
List<String> repos = gitService.listRepositories();
|
||||||
|
if (repos.isEmpty())
|
||||||
|
{
|
||||||
|
out.println("No repositories cloned yet.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
out.println("Repositories:");
|
||||||
|
for (int i = 0; i < repos.size(); i++)
|
||||||
|
{
|
||||||
|
out.println(" " + (i + 1) + ". " + repos.get(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cloneRepository() throws IOException, GitAPIException
|
||||||
|
{
|
||||||
|
out.print("URL: ");
|
||||||
|
out.flush();
|
||||||
|
String url = in.readLine();
|
||||||
|
if (url == null || url.isBlank())
|
||||||
|
return;
|
||||||
|
|
||||||
|
out.print("Name: ");
|
||||||
|
out.flush();
|
||||||
|
String name = in.readLine();
|
||||||
|
if (name == null || name.isBlank())
|
||||||
|
return;
|
||||||
|
|
||||||
|
gitService.cloneRepository(url.trim(), name.trim());
|
||||||
|
out.println("Cloned successfully.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openRepository() throws IOException, GitAPIException
|
||||||
|
{
|
||||||
|
List<String> repos = gitService.listRepositories();
|
||||||
|
if (repos.isEmpty())
|
||||||
|
{
|
||||||
|
out.println("No repositories cloned yet.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
out.println("Repositories:");
|
||||||
|
for (int i = 0; i < repos.size(); i++)
|
||||||
|
{
|
||||||
|
out.println(" " + (i + 1) + ". " + repos.get(i));
|
||||||
|
}
|
||||||
|
out.print("Enter number: ");
|
||||||
|
out.flush();
|
||||||
|
|
||||||
|
String input = in.readLine();
|
||||||
|
if (input == null || input.isBlank())
|
||||||
|
return;
|
||||||
|
|
||||||
|
int index;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
index = Integer.parseInt(input.trim()) - 1;
|
||||||
|
}
|
||||||
|
catch (NumberFormatException e)
|
||||||
|
{
|
||||||
|
out.println("Invalid number.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < 0 || index >= repos.size())
|
||||||
|
{
|
||||||
|
out.println("Invalid selection.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
repoMenu(repos.get(index));
|
||||||
|
}
|
||||||
|
|
||||||
|
void repoMenu(String name) throws IOException, GitAPIException
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
out.println();
|
||||||
|
out.println("Repository: " + name);
|
||||||
|
out.println(" Branch: " + gitService.getCurrentBranch(name));
|
||||||
|
out.println();
|
||||||
|
out.println(" 1. List branches");
|
||||||
|
out.println(" 2. Checkout branch");
|
||||||
|
out.println(" 3. Create new branch");
|
||||||
|
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(" b. Back");
|
||||||
|
out.print("> ");
|
||||||
|
out.flush();
|
||||||
|
|
||||||
|
String choice = in.readLine();
|
||||||
|
if (choice == null || "b".equalsIgnoreCase(choice))
|
||||||
|
return;
|
||||||
|
|
||||||
|
switch (choice.trim())
|
||||||
|
{
|
||||||
|
case "1" -> listBranches(name);
|
||||||
|
case "2" -> checkoutBranch(name);
|
||||||
|
case "3" -> createBranch(name);
|
||||||
|
case "4" -> showModifiedFiles(name);
|
||||||
|
case "5" -> showStagedFiles(name);
|
||||||
|
case "6" -> stageFiles(name);
|
||||||
|
case "7" -> commitChanges(name);
|
||||||
|
case "8" -> push(name);
|
||||||
|
case "9" -> pull(name);
|
||||||
|
default -> out.println("Invalid choice.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void listBranches(String name) throws IOException, GitAPIException
|
||||||
|
{
|
||||||
|
List<String> branches = gitService.listBranches(name);
|
||||||
|
String current = gitService.getCurrentBranch(name);
|
||||||
|
out.println("Branches:");
|
||||||
|
for (String branch : branches)
|
||||||
|
{
|
||||||
|
out.println(branch.equals(current) ? " * " + branch : " " + branch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkoutBranch(String name) throws IOException, GitAPIException
|
||||||
|
{
|
||||||
|
out.print("Branch name: ");
|
||||||
|
out.flush();
|
||||||
|
String branch = in.readLine();
|
||||||
|
if (branch == null || branch.isBlank())
|
||||||
|
return;
|
||||||
|
gitService.checkoutBranch(name, branch.trim());
|
||||||
|
out.println("Switched to " + branch.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createBranch(String name) throws IOException, GitAPIException
|
||||||
|
{
|
||||||
|
out.print("New branch name: ");
|
||||||
|
out.flush();
|
||||||
|
String branch = in.readLine();
|
||||||
|
if (branch == null || branch.isBlank())
|
||||||
|
return;
|
||||||
|
gitService.createAndCheckoutBranch(name, branch.trim());
|
||||||
|
out.println("Created and switched to " + branch.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showModifiedFiles(String name) throws IOException, GitAPIException
|
||||||
|
{
|
||||||
|
List<String> files = gitService.getModifiedFiles(name);
|
||||||
|
if (files.isEmpty())
|
||||||
|
{
|
||||||
|
out.println("No modified files.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
out.println("Modified files:");
|
||||||
|
for (int i = 0; i < files.size(); i++)
|
||||||
|
{
|
||||||
|
out.println(" " + (i + 1) + ". " + files.get(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showStagedFiles(String name) throws IOException, GitAPIException
|
||||||
|
{
|
||||||
|
List<String> files = gitService.getStagedFiles(name);
|
||||||
|
if (files.isEmpty())
|
||||||
|
{
|
||||||
|
out.println("No staged files.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
out.println("Staged files:");
|
||||||
|
for (String file : files)
|
||||||
|
{
|
||||||
|
out.println(" " + file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stageFiles(String name) throws IOException, GitAPIException
|
||||||
|
{
|
||||||
|
List<String> files = gitService.getModifiedFiles(name);
|
||||||
|
if (files.isEmpty())
|
||||||
|
{
|
||||||
|
out.println("No modified files to stage.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
out.println("Modified files:");
|
||||||
|
for (int i = 0; i < files.size(); i++)
|
||||||
|
{
|
||||||
|
out.println(" " + (i + 1) + ". " + files.get(i));
|
||||||
|
}
|
||||||
|
out.print("Enter numbers to stage (comma-separated, or 'a' for all): ");
|
||||||
|
out.flush();
|
||||||
|
|
||||||
|
String input = in.readLine();
|
||||||
|
if (input == null || input.isBlank())
|
||||||
|
return;
|
||||||
|
|
||||||
|
List<String> toStage;
|
||||||
|
if ("a".equalsIgnoreCase(input.trim()))
|
||||||
|
{
|
||||||
|
toStage = files;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
toStage = new java.util.ArrayList<>();
|
||||||
|
for (String part : input.split(","))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int idx = Integer.parseInt(part.trim()) - 1;
|
||||||
|
if (idx >= 0 && idx < files.size())
|
||||||
|
toStage.add(files.get(idx));
|
||||||
|
}
|
||||||
|
catch (NumberFormatException ignored)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!toStage.isEmpty())
|
||||||
|
{
|
||||||
|
gitService.stageFiles(name, toStage);
|
||||||
|
out.println("Staged " + toStage.size() + " file(s).");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void commitChanges(String name) throws IOException, GitAPIException
|
||||||
|
{
|
||||||
|
out.print("Commit message: ");
|
||||||
|
out.flush();
|
||||||
|
String message = in.readLine();
|
||||||
|
if (message == null || message.isBlank())
|
||||||
|
return;
|
||||||
|
gitService.commit(name, message.trim());
|
||||||
|
out.println("Committed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void push(String name) throws IOException, GitAPIException
|
||||||
|
{
|
||||||
|
gitService.push(name);
|
||||||
|
out.println("Pushed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pull(String name) throws IOException, GitAPIException
|
||||||
|
{
|
||||||
|
gitService.pull(name);
|
||||||
|
out.println("Pulled.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,8 @@ import static org.junit.jupiter.api.Assertions.*;
|
|||||||
@SpringBootTest
|
@SpringBootTest
|
||||||
@TestPropertySource(properties = {
|
@TestPropertySource(properties = {
|
||||||
"webgit.worktree-path=/mnt/shared/repos",
|
"webgit.worktree-path=/mnt/shared/repos",
|
||||||
"webgit.git-dir-path=/var/lib/webgit/git"
|
"webgit.git-dir-path=/var/lib/webgit/git",
|
||||||
|
"webgit.telnet-port=2323"
|
||||||
})
|
})
|
||||||
class WebgitPropertiesTest
|
class WebgitPropertiesTest
|
||||||
{
|
{
|
||||||
@@ -30,4 +31,10 @@ class WebgitPropertiesTest
|
|||||||
{
|
{
|
||||||
assertEquals(Path.of("/var/lib/webgit/git"), properties.getGitDirPath());
|
assertEquals(Path.of("/var/lib/webgit/git"), properties.getGitDirPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void telnetPortIsBound()
|
||||||
|
{
|
||||||
|
assertEquals(2323, properties.getTelnetPort());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package be.seeseepuff.webgit.telnet;
|
||||||
|
|
||||||
|
import be.seeseepuff.webgit.config.WebgitProperties;
|
||||||
|
import be.seeseepuff.webgit.service.GitService;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class TelnetServerTest
|
||||||
|
{
|
||||||
|
@TempDir
|
||||||
|
Path tempDir;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void serverAcceptsConnectionAndResponds() throws Exception
|
||||||
|
{
|
||||||
|
WebgitProperties props = new WebgitProperties();
|
||||||
|
props.setWorktreePath(tempDir.resolve("worktrees"));
|
||||||
|
props.setGitDirPath(tempDir.resolve("gitdirs"));
|
||||||
|
props.setTelnetPort(0); // use any free port
|
||||||
|
|
||||||
|
GitService gitService = new GitService(props);
|
||||||
|
|
||||||
|
// We'll test with a real server socket but use port 0 for a random free port
|
||||||
|
// Since the TelnetServer uses PostConstruct, we'll test the component manually
|
||||||
|
var serverSocket = new java.net.ServerSocket(0);
|
||||||
|
int port = serverSocket.getLocalPort();
|
||||||
|
serverSocket.close();
|
||||||
|
|
||||||
|
props.setTelnetPort(port);
|
||||||
|
TelnetServer server = new TelnetServer(gitService, props);
|
||||||
|
server.start();
|
||||||
|
|
||||||
|
try (Socket socket = new Socket("localhost", port);
|
||||||
|
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
|
||||||
|
PrintWriter out = new PrintWriter(socket.getOutputStream(), true))
|
||||||
|
{
|
||||||
|
// Read the welcome message
|
||||||
|
String line = in.readLine();
|
||||||
|
assertTrue(line.contains("Welcome to WebGit"));
|
||||||
|
|
||||||
|
// Send quit
|
||||||
|
out.println("q");
|
||||||
|
|
||||||
|
// Read until we see Goodbye
|
||||||
|
String response = "";
|
||||||
|
String l;
|
||||||
|
while ((l = in.readLine()) != null)
|
||||||
|
{
|
||||||
|
response += l + "\n";
|
||||||
|
}
|
||||||
|
assertTrue(response.contains("Goodbye!"));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
server.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
352
src/test/java/be/seeseepuff/webgit/telnet/TelnetSessionTest.java
Normal file
352
src/test/java/be/seeseepuff/webgit/telnet/TelnetSessionTest.java
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
package be.seeseepuff.webgit.telnet;
|
||||||
|
|
||||||
|
import be.seeseepuff.webgit.service.GitService;
|
||||||
|
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class TelnetSessionTest
|
||||||
|
{
|
||||||
|
@Mock
|
||||||
|
private GitService gitService;
|
||||||
|
|
||||||
|
private TelnetSession createSession(String input)
|
||||||
|
{
|
||||||
|
BufferedReader in = new BufferedReader(new StringReader(input));
|
||||||
|
PrintWriter out = new PrintWriter(new StringWriter());
|
||||||
|
return new TelnetSession(gitService, in, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String runSession(String input)
|
||||||
|
{
|
||||||
|
BufferedReader in = new BufferedReader(new StringReader(input));
|
||||||
|
StringWriter sw = new StringWriter();
|
||||||
|
PrintWriter out = new PrintWriter(sw, true);
|
||||||
|
new TelnetSession(gitService, in, out).run();
|
||||||
|
return sw.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void quitExitsImmediately()
|
||||||
|
{
|
||||||
|
String output = runSession("q\n");
|
||||||
|
assertTrue(output.contains("Welcome to WebGit"));
|
||||||
|
assertTrue(output.contains("Goodbye!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void nullInputExits()
|
||||||
|
{
|
||||||
|
String output = runSession("");
|
||||||
|
assertTrue(output.contains("Welcome to WebGit"));
|
||||||
|
assertTrue(output.contains("Goodbye!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void listRepositoriesShowsEmpty() throws IOException
|
||||||
|
{
|
||||||
|
when(gitService.listRepositories()).thenReturn(List.of());
|
||||||
|
String output = runSession("1\nq\n");
|
||||||
|
assertTrue(output.contains("No repositories cloned yet."));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void listRepositoriesShowsRepos() throws IOException
|
||||||
|
{
|
||||||
|
when(gitService.listRepositories()).thenReturn(List.of("alpha", "beta"));
|
||||||
|
String output = runSession("1\nq\n");
|
||||||
|
assertTrue(output.contains("alpha"));
|
||||||
|
assertTrue(output.contains("beta"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void cloneRepository() throws IOException, GitAPIException
|
||||||
|
{
|
||||||
|
String output = runSession("2\nhttps://example.com/repo.git\nmyrepo\nq\n");
|
||||||
|
assertTrue(output.contains("Cloned successfully."));
|
||||||
|
verify(gitService).cloneRepository("https://example.com/repo.git", "myrepo");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void cloneRepositoryEmptyUrl()
|
||||||
|
{
|
||||||
|
String output = runSession("2\n\nq\n");
|
||||||
|
assertFalse(output.contains("Cloned successfully."));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void cloneRepositoryEmptyName()
|
||||||
|
{
|
||||||
|
String output = runSession("2\nhttps://example.com/repo.git\n\nq\n");
|
||||||
|
assertFalse(output.contains("Cloned successfully."));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void openRepositoryEmpty() throws IOException
|
||||||
|
{
|
||||||
|
when(gitService.listRepositories()).thenReturn(List.of());
|
||||||
|
String output = runSession("3\nq\n");
|
||||||
|
assertTrue(output.contains("No repositories cloned yet."));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void openRepositoryInvalidNumber() throws IOException
|
||||||
|
{
|
||||||
|
when(gitService.listRepositories()).thenReturn(List.of("myrepo"));
|
||||||
|
String output = runSession("3\nabc\nq\n");
|
||||||
|
assertTrue(output.contains("Invalid number."));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void openRepositoryOutOfRange() throws IOException
|
||||||
|
{
|
||||||
|
when(gitService.listRepositories()).thenReturn(List.of("myrepo"));
|
||||||
|
String output = runSession("3\n5\nq\n");
|
||||||
|
assertTrue(output.contains("Invalid selection."));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void openRepositoryAndGoBack() throws IOException, GitAPIException
|
||||||
|
{
|
||||||
|
when(gitService.listRepositories()).thenReturn(List.of("myrepo"));
|
||||||
|
when(gitService.getCurrentBranch("myrepo")).thenReturn("main");
|
||||||
|
String output = runSession("3\n1\nb\nq\n");
|
||||||
|
assertTrue(output.contains("Repository: myrepo"));
|
||||||
|
assertTrue(output.contains("Branch: main"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void repoMenuListBranches() throws IOException, GitAPIException
|
||||||
|
{
|
||||||
|
when(gitService.listRepositories()).thenReturn(List.of("myrepo"));
|
||||||
|
when(gitService.getCurrentBranch("myrepo")).thenReturn("main");
|
||||||
|
when(gitService.listBranches("myrepo")).thenReturn(List.of("main", "develop"));
|
||||||
|
String output = runSession("3\n1\n1\nb\nq\n");
|
||||||
|
assertTrue(output.contains("* main"));
|
||||||
|
assertTrue(output.contains("develop"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void repoMenuCheckoutBranch() throws IOException, GitAPIException
|
||||||
|
{
|
||||||
|
when(gitService.listRepositories()).thenReturn(List.of("myrepo"));
|
||||||
|
when(gitService.getCurrentBranch("myrepo")).thenReturn("main");
|
||||||
|
String output = runSession("3\n1\n2\ndevelop\nb\nq\n");
|
||||||
|
verify(gitService).checkoutBranch("myrepo", "develop");
|
||||||
|
assertTrue(output.contains("Switched to develop"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void repoMenuCheckoutBranchEmpty() throws IOException, GitAPIException
|
||||||
|
{
|
||||||
|
when(gitService.listRepositories()).thenReturn(List.of("myrepo"));
|
||||||
|
when(gitService.getCurrentBranch("myrepo")).thenReturn("main");
|
||||||
|
String output = runSession("3\n1\n2\n\nb\nq\n");
|
||||||
|
verify(gitService, never()).checkoutBranch(anyString(), anyString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void repoMenuCreateBranch() throws IOException, GitAPIException
|
||||||
|
{
|
||||||
|
when(gitService.listRepositories()).thenReturn(List.of("myrepo"));
|
||||||
|
when(gitService.getCurrentBranch("myrepo")).thenReturn("main");
|
||||||
|
String output = runSession("3\n1\n3\nfeature-x\nb\nq\n");
|
||||||
|
verify(gitService).createAndCheckoutBranch("myrepo", "feature-x");
|
||||||
|
assertTrue(output.contains("Created and switched to feature-x"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void repoMenuCreateBranchEmpty() throws IOException, GitAPIException
|
||||||
|
{
|
||||||
|
when(gitService.listRepositories()).thenReturn(List.of("myrepo"));
|
||||||
|
when(gitService.getCurrentBranch("myrepo")).thenReturn("main");
|
||||||
|
String output = runSession("3\n1\n3\n\nb\nq\n");
|
||||||
|
verify(gitService, never()).createAndCheckoutBranch(anyString(), anyString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void repoMenuShowModifiedFilesEmpty() throws IOException, GitAPIException
|
||||||
|
{
|
||||||
|
when(gitService.listRepositories()).thenReturn(List.of("myrepo"));
|
||||||
|
when(gitService.getCurrentBranch("myrepo")).thenReturn("main");
|
||||||
|
when(gitService.getModifiedFiles("myrepo")).thenReturn(List.of());
|
||||||
|
String output = runSession("3\n1\n4\nb\nq\n");
|
||||||
|
assertTrue(output.contains("No modified files."));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void repoMenuShowModifiedFiles() throws IOException, GitAPIException
|
||||||
|
{
|
||||||
|
when(gitService.listRepositories()).thenReturn(List.of("myrepo"));
|
||||||
|
when(gitService.getCurrentBranch("myrepo")).thenReturn("main");
|
||||||
|
when(gitService.getModifiedFiles("myrepo")).thenReturn(List.of("a.txt", "b.txt"));
|
||||||
|
String output = runSession("3\n1\n4\nb\nq\n");
|
||||||
|
assertTrue(output.contains("a.txt"));
|
||||||
|
assertTrue(output.contains("b.txt"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void repoMenuShowStagedFilesEmpty() 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\n5\nb\nq\n");
|
||||||
|
assertTrue(output.contains("No staged files."));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void repoMenuShowStagedFiles() throws IOException, GitAPIException
|
||||||
|
{
|
||||||
|
when(gitService.listRepositories()).thenReturn(List.of("myrepo"));
|
||||||
|
when(gitService.getCurrentBranch("myrepo")).thenReturn("main");
|
||||||
|
when(gitService.getStagedFiles("myrepo")).thenReturn(List.of("staged.txt"));
|
||||||
|
String output = runSession("3\n1\n5\nb\nq\n");
|
||||||
|
assertTrue(output.contains("staged.txt"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void repoMenuStageAllFiles() throws IOException, GitAPIException
|
||||||
|
{
|
||||||
|
when(gitService.listRepositories()).thenReturn(List.of("myrepo"));
|
||||||
|
when(gitService.getCurrentBranch("myrepo")).thenReturn("main");
|
||||||
|
when(gitService.getModifiedFiles("myrepo")).thenReturn(List.of("a.txt", "b.txt"));
|
||||||
|
String output = runSession("3\n1\n6\na\nb\nq\n");
|
||||||
|
verify(gitService).stageFiles("myrepo", List.of("a.txt", "b.txt"));
|
||||||
|
assertTrue(output.contains("Staged 2 file(s)."));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void repoMenuStageSelectedFiles() throws IOException, GitAPIException
|
||||||
|
{
|
||||||
|
when(gitService.listRepositories()).thenReturn(List.of("myrepo"));
|
||||||
|
when(gitService.getCurrentBranch("myrepo")).thenReturn("main");
|
||||||
|
when(gitService.getModifiedFiles("myrepo")).thenReturn(List.of("a.txt", "b.txt", "c.txt"));
|
||||||
|
String output = runSession("3\n1\n6\n1,3\nb\nq\n");
|
||||||
|
verify(gitService).stageFiles("myrepo", List.of("a.txt", "c.txt"));
|
||||||
|
assertTrue(output.contains("Staged 2 file(s)."));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void repoMenuStageNoModifiedFiles() throws IOException, GitAPIException
|
||||||
|
{
|
||||||
|
when(gitService.listRepositories()).thenReturn(List.of("myrepo"));
|
||||||
|
when(gitService.getCurrentBranch("myrepo")).thenReturn("main");
|
||||||
|
when(gitService.getModifiedFiles("myrepo")).thenReturn(List.of());
|
||||||
|
String output = runSession("3\n1\n6\nb\nq\n");
|
||||||
|
assertTrue(output.contains("No modified files to stage."));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void repoMenuStageEmptyInput() throws IOException, GitAPIException
|
||||||
|
{
|
||||||
|
when(gitService.listRepositories()).thenReturn(List.of("myrepo"));
|
||||||
|
when(gitService.getCurrentBranch("myrepo")).thenReturn("main");
|
||||||
|
when(gitService.getModifiedFiles("myrepo")).thenReturn(List.of("a.txt"));
|
||||||
|
String output = runSession("3\n1\n6\n\nb\nq\n");
|
||||||
|
verify(gitService, never()).stageFiles(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");
|
||||||
|
verify(gitService).commit("myrepo", "my commit msg");
|
||||||
|
assertTrue(output.contains("Committed."));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void repoMenuCommitEmpty() throws IOException, GitAPIException
|
||||||
|
{
|
||||||
|
when(gitService.listRepositories()).thenReturn(List.of("myrepo"));
|
||||||
|
when(gitService.getCurrentBranch("myrepo")).thenReturn("main");
|
||||||
|
String output = runSession("3\n1\n7\n\nb\nq\n");
|
||||||
|
verify(gitService, never()).commit(anyString(), anyString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void repoMenuPush() throws IOException, GitAPIException
|
||||||
|
{
|
||||||
|
when(gitService.listRepositories()).thenReturn(List.of("myrepo"));
|
||||||
|
when(gitService.getCurrentBranch("myrepo")).thenReturn("main");
|
||||||
|
String output = runSession("3\n1\n8\nb\nq\n");
|
||||||
|
verify(gitService).push("myrepo");
|
||||||
|
assertTrue(output.contains("Pushed."));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void repoMenuPull() throws IOException, GitAPIException
|
||||||
|
{
|
||||||
|
when(gitService.listRepositories()).thenReturn(List.of("myrepo"));
|
||||||
|
when(gitService.getCurrentBranch("myrepo")).thenReturn("main");
|
||||||
|
String output = runSession("3\n1\n9\nb\nq\n");
|
||||||
|
verify(gitService).pull("myrepo");
|
||||||
|
assertTrue(output.contains("Pulled."));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void invalidMainMenuChoice()
|
||||||
|
{
|
||||||
|
String output = runSession("x\nq\n");
|
||||||
|
assertTrue(output.contains("Invalid choice."));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void invalidRepoMenuChoice() throws IOException, GitAPIException
|
||||||
|
{
|
||||||
|
when(gitService.listRepositories()).thenReturn(List.of("myrepo"));
|
||||||
|
when(gitService.getCurrentBranch("myrepo")).thenReturn("main");
|
||||||
|
String output = runSession("3\n1\nx\nb\nq\n");
|
||||||
|
assertTrue(output.contains("Invalid choice."));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void repoMenuNullInputExits() throws IOException, GitAPIException
|
||||||
|
{
|
||||||
|
when(gitService.listRepositories()).thenReturn(List.of("myrepo"));
|
||||||
|
when(gitService.getCurrentBranch("myrepo")).thenReturn("main");
|
||||||
|
// After entering repo menu, the input stream ends (null)
|
||||||
|
String output = runSession("3\n1\n");
|
||||||
|
assertTrue(output.contains("Repository: myrepo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void openRepositoryEmptyInput() throws IOException
|
||||||
|
{
|
||||||
|
when(gitService.listRepositories()).thenReturn(List.of("myrepo"));
|
||||||
|
String output = runSession("3\n\nq\n");
|
||||||
|
// Empty input returns to main menu
|
||||||
|
assertFalse(output.contains("Repository: myrepo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void openRepositoryZeroIndex() throws IOException
|
||||||
|
{
|
||||||
|
when(gitService.listRepositories()).thenReturn(List.of("myrepo"));
|
||||||
|
String output = runSession("3\n0\nq\n");
|
||||||
|
assertTrue(output.contains("Invalid selection."));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void stageInvalidNumbers() throws IOException, GitAPIException
|
||||||
|
{
|
||||||
|
when(gitService.listRepositories()).thenReturn(List.of("myrepo"));
|
||||||
|
when(gitService.getCurrentBranch("myrepo")).thenReturn("main");
|
||||||
|
when(gitService.getModifiedFiles("myrepo")).thenReturn(List.of("a.txt"));
|
||||||
|
// "abc" is not a valid number - no files staged
|
||||||
|
String output = runSession("3\n1\n6\nabc\nb\nq\n");
|
||||||
|
verify(gitService, never()).stageFiles(anyString(), anyList());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user