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:
2026-02-26 08:45:25 +01:00
parent 88385d39a1
commit 6218fe345a
6 changed files with 829 additions and 1 deletions
@@ -15,4 +15,5 @@ public class WebgitProperties
{
private Path worktreePath;
private Path gitDirPath;
private Integer telnetPort;
}
@@ -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);
}
}
}
@@ -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.");
}
}