feat: support SVN auth and project credentials

- add username/password fields to project dialog and model
- pass optional auth to SVN info/status/log/diff/update/commit services
- centralize SVN CLI auth flags in SvnService and fix header text

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
liumangmang
2026-02-04 17:54:16 +08:00
parent 610793f276
commit 52c099e0ba
11 changed files with 198 additions and 27 deletions

View File

@@ -203,7 +203,10 @@ public class MainController {
protected Void call() throws Exception {
try {
// 获取SVN信息
com.svnmanager.model.SvnInfo info = infoService.getInfo(currentProject.getPath());
com.svnmanager.model.SvnInfo info = infoService.getInfo(
currentProject.getPath(),
currentProject.getUsername(),
currentProject.getPassword());
if (info != null && info.getRevision() != null) {
Platform.runLater(() -> {
currentVersionLabel.setText("r" + info.getRevision());
@@ -212,7 +215,10 @@ public class MainController {
}
// 获取状态
SvnStatus status = statusService.getStatus(currentProject.getPath());
SvnStatus status = statusService.getStatus(
currentProject.getPath(),
currentProject.getUsername(),
currentProject.getPassword());
Platform.runLater(() -> {
fileStatusList.clear();
fileStatusList.addAll(status.getFiles());
@@ -263,8 +269,15 @@ public class MainController {
if (result == ButtonType.OK) {
Project project = controller.getProject();
if (project != null) {
ConfigUtil.addProject(project);
boolean saved = ConfigUtil.addProject(project);
if (saved) {
loadProjects();
} else {
showError("保存失败", "无法保存项目配置,请检查当前用户目录下是否有写入权限:\n"
+ ConfigUtil.getProjectsFilePath());
}
} else {
showWarning("提示", "项目名称和项目路径不能为空。");
}
}
});
@@ -306,7 +319,10 @@ public class MainController {
Task<UpdateService.UpdateResult> task = new Task<UpdateService.UpdateResult>() {
@Override
protected UpdateService.UpdateResult call() throws Exception {
return updateService.update(currentProject.getPath());
return updateService.update(
currentProject.getPath(),
currentProject.getUsername(),
currentProject.getPassword());
}
};
@@ -348,7 +364,11 @@ public class MainController {
Task<CommitService.CommitResult> task = new Task<CommitService.CommitResult>() {
@Override
protected CommitService.CommitResult call() throws Exception {
return commitService.commit(currentProject.getPath(), message);
return commitService.commit(
currentProject.getPath(),
message,
currentProject.getUsername(),
currentProject.getPassword());
}
};
@@ -380,7 +400,10 @@ public class MainController {
Task<SvnStatus> task = new Task<SvnStatus>() {
@Override
protected SvnStatus call() throws Exception {
return statusService.getStatus(currentProject.getPath());
return statusService.getStatus(
currentProject.getPath(),
currentProject.getUsername(),
currentProject.getPassword());
}
};
@@ -409,7 +432,11 @@ public class MainController {
Task<List<com.svnmanager.model.SvnLog>> task = new Task<List<com.svnmanager.model.SvnLog>>() {
@Override
protected List<com.svnmanager.model.SvnLog> call() throws Exception {
return logService.getLog(currentProject.getPath(), 20);
return logService.getLog(
currentProject.getPath(),
20,
currentProject.getUsername(),
currentProject.getPassword());
}
};
@@ -435,7 +462,11 @@ public class MainController {
Task<String> task = new Task<String>() {
@Override
protected String call() throws Exception {
return diffService.getDiff(currentProject.getPath());
return diffService.getDiff(
currentProject.getPath(),
null,
currentProject.getUsername(),
currentProject.getPassword());
}
};
@@ -461,7 +492,10 @@ public class MainController {
Task<com.svnmanager.model.SvnInfo> task = new Task<com.svnmanager.model.SvnInfo>() {
@Override
protected com.svnmanager.model.SvnInfo call() throws Exception {
return infoService.getInfo(currentProject.getPath());
return infoService.getInfo(
currentProject.getPath(),
currentProject.getUsername(),
currentProject.getPassword());
}
};

View File

@@ -1,6 +1,8 @@
package com.svnmanager.controller;
import com.svnmanager.model.Project;
import com.svnmanager.model.SvnInfo;
import com.svnmanager.service.InfoService;
import javafx.fxml.FXML;
import javafx.scene.control.TextField;
import javafx.stage.DirectoryChooser;
@@ -16,6 +18,8 @@ public class ProjectDialogController {
@FXML private TextField nameField;
@FXML private TextField pathField;
@FXML private TextField svnUrlField;
@FXML private TextField usernameField;
@FXML private TextField passwordField;
private Project project;
@@ -34,7 +38,31 @@ public class ProjectDialogController {
File selectedDirectory = directoryChooser.showDialog(pathField.getScene().getWindow());
if (selectedDirectory != null) {
pathField.setText(selectedDirectory.getAbsolutePath());
String selectedPath = selectedDirectory.getAbsolutePath();
pathField.setText(selectedPath);
// 如果是已存在的SVN工作副本自动获取URL填充
autoFillSvnUrl(selectedPath);
}
}
/**
* 根据工作副本自动填充 SVN URL
*/
private void autoFillSvnUrl(String workingDirectory) {
// 仅当当前输入框为空时才自动填充,避免覆盖用户手动输入
if (svnUrlField.getText() != null && !svnUrlField.getText().trim().isEmpty()) {
return;
}
try {
InfoService infoService = new InfoService();
SvnInfo info = infoService.getInfo(workingDirectory);
if (info != null && info.getUrl() != null && !info.getUrl().isEmpty()) {
svnUrlField.setText(info.getUrl());
}
} catch (Exception e) {
// 自动填充失败时静默忽略,不影响正常使用
}
}
@@ -47,6 +75,8 @@ public class ProjectDialogController {
String name = nameField.getText().trim();
String path = pathField.getText().trim();
String svnUrl = svnUrlField.getText().trim();
String username = usernameField.getText().trim();
String password = passwordField.getText();
if (name.isEmpty() || path.isEmpty()) {
return null;
@@ -59,6 +89,8 @@ public class ProjectDialogController {
project.setName(name);
project.setPath(path);
project.setSvnUrl(svnUrl);
project.setUsername(username.isEmpty() ? null : username);
project.setPassword(password == null || password.isEmpty() ? null : password);
return project;
}
@@ -74,6 +106,14 @@ public class ProjectDialogController {
nameField.setText(project.getName());
pathField.setText(project.getPath());
svnUrlField.setText(project.getSvnUrl());
usernameField.setText(project.getUsername());
passwordField.setText(project.getPassword());
// 如果已有路径但URL为空尝试自动获取
if (project.getPath() != null && !project.getPath().isEmpty()
&& (project.getSvnUrl() == null || project.getSvnUrl().isEmpty())) {
autoFillSvnUrl(project.getPath());
}
}
}
}

View File

@@ -18,6 +18,12 @@ public class Project {
@JsonProperty("svnUrl")
private String svnUrl;
@JsonProperty("username")
private String username;
@JsonProperty("password")
private String password;
@JsonProperty("currentVersion")
private String currentVersion;
@@ -71,6 +77,22 @@ public class Project {
this.svnUrl = svnUrl;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getCurrentVersion() {
return currentVersion;
}

View File

@@ -26,7 +26,8 @@ public class CommitService extends SvnService {
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
public CommitResult commit(String workingDirectory, String message, List<String> files)
public CommitResult commit(String workingDirectory, String message, List<String> files,
String username, String password)
throws IOException, InterruptedException, TimeoutException {
logger.info("提交修改: {}", workingDirectory);
@@ -39,6 +40,7 @@ public class CommitService extends SvnService {
}
List<String> args = new ArrayList<>();
addAuthArgs(args, username, password);
args.add("-m");
args.add(message);
@@ -85,9 +87,14 @@ public class CommitService extends SvnService {
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
public CommitResult commit(String workingDirectory, String message, String username, String password)
throws IOException, InterruptedException, TimeoutException {
return commit(workingDirectory, message, null, username, password);
}
public CommitResult commit(String workingDirectory, String message)
throws IOException, InterruptedException, TimeoutException {
return commit(workingDirectory, message, null);
return commit(workingDirectory, message, null, null);
}
/**

View File

@@ -25,7 +25,7 @@ public class DiffService extends SvnService {
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
public String getDiff(String workingDirectory, String filePath)
public String getDiff(String workingDirectory, String filePath, String username, String password)
throws IOException, InterruptedException, TimeoutException {
logger.debug("获取差异: {}", workingDirectory);
@@ -34,6 +34,7 @@ public class DiffService extends SvnService {
}
List<String> args = new ArrayList<>();
addAuthArgs(args, username, password);
if (filePath != null && !filePath.isEmpty()) {
args.add(filePath);
}
@@ -59,6 +60,21 @@ public class DiffService extends SvnService {
*/
public String getDiff(String workingDirectory)
throws IOException, InterruptedException, TimeoutException {
return getDiff(workingDirectory, null);
return getDiff(workingDirectory, null, null, null);
}
/**
* 获取指定文件的差异(不带认证信息)
*
* @param workingDirectory 工作目录
* @param filePath 文件路径null表示所有文件
* @return 差异内容
* @throws IOException IO异常
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
public String getDiff(String workingDirectory, String filePath)
throws IOException, InterruptedException, TimeoutException {
return getDiff(workingDirectory, filePath, null, null);
}
}

View File

@@ -23,7 +23,7 @@ public class InfoService extends SvnService {
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
public SvnInfo getInfo(String workingDirectory)
public SvnInfo getInfo(String workingDirectory, String username, String password)
throws IOException, InterruptedException, TimeoutException {
logger.debug("获取信息: {}", workingDirectory);
@@ -45,6 +45,11 @@ public class InfoService extends SvnService {
return info;
}
public SvnInfo getInfo(String workingDirectory)
throws IOException, InterruptedException, TimeoutException {
return getInfo(workingDirectory, null, null);
}
/**
* 解析svn info输出
*

View File

@@ -30,7 +30,7 @@ public class LogService extends SvnService {
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
public List<SvnLog> getLog(String workingDirectory, Integer limit)
public List<SvnLog> getLog(String workingDirectory, Integer limit, String username, String password)
throws IOException, InterruptedException, TimeoutException {
logger.debug("获取日志: {}", workingDirectory);
@@ -39,6 +39,7 @@ public class LogService extends SvnService {
}
List<String> args = new ArrayList<>();
addAuthArgs(args, username, password);
args.add("-v"); // 详细输出
if (limit != null && limit > 0) {
args.add("-l");
@@ -68,9 +69,14 @@ public class LogService extends SvnService {
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
public List<SvnLog> getLog(String workingDirectory, String username, String password)
throws IOException, InterruptedException, TimeoutException {
return getLog(workingDirectory, 50, username, password);
}
public List<SvnLog> getLog(String workingDirectory)
throws IOException, InterruptedException, TimeoutException {
return getLog(workingDirectory, 50);
return getLog(workingDirectory, 50, null, null);
}
/**

View File

@@ -26,7 +26,7 @@ public class StatusService extends SvnService {
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
public SvnStatus getStatus(String workingDirectory)
public SvnStatus getStatus(String workingDirectory, String username, String password)
throws IOException, InterruptedException, TimeoutException {
logger.debug("获取状态: {}", workingDirectory);
@@ -35,6 +35,7 @@ public class StatusService extends SvnService {
}
List<String> args = new ArrayList<>();
addAuthArgs(args, username, password);
args.add("-v"); // 详细输出
ProcessUtil.ProcessResult result = executeSvnCommand("status", args, workingDirectory);
@@ -53,6 +54,11 @@ public class StatusService extends SvnService {
return status;
}
public SvnStatus getStatus(String workingDirectory)
throws IOException, InterruptedException, TimeoutException {
return getStatus(workingDirectory, null, null);
}
/**
* 解析svn status输出
*

View File

@@ -15,6 +15,27 @@ import java.util.concurrent.TimeoutException;
public abstract class SvnService {
protected static final Logger logger = LoggerFactory.getLogger(SvnService.class);
/**
* 将认证参数追加到命令参数列表
*
* @param args 命令参数列表
* @param username 用户名(可为空)
* @param password 密码(可为空)
*/
protected void addAuthArgs(List<String> args, String username, String password) {
if (username != null && !username.isEmpty()) {
args.add("--username");
args.add(username);
if (password != null && !password.isEmpty()) {
args.add("--password");
args.add(password);
}
// 避免弹出交互式输入框,统一走非交互模式
args.add("--no-auth-cache");
args.add("--non-interactive");
}
}
/**
* 执行SVN命令
*

View File

@@ -25,7 +25,7 @@ public class UpdateService extends SvnService {
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
public UpdateResult update(String workingDirectory, String revision)
public UpdateResult update(String workingDirectory, String revision, String username, String password)
throws IOException, InterruptedException, TimeoutException {
logger.info("更新工作副本: {}", workingDirectory);
@@ -34,6 +34,7 @@ public class UpdateService extends SvnService {
}
List<String> args = new ArrayList<>();
addAuthArgs(args, username, password);
if (revision != null && !revision.isEmpty()) {
args.add("-r");
args.add(revision);
@@ -77,9 +78,14 @@ public class UpdateService extends SvnService {
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
public UpdateResult update(String workingDirectory, String username, String password)
throws IOException, InterruptedException, TimeoutException {
return update(workingDirectory, null, username, password);
}
public UpdateResult update(String workingDirectory)
throws IOException, InterruptedException, TimeoutException {
return update(workingDirectory, null);
return update(workingDirectory, null, null, null);
}
/**

View File

@@ -10,9 +10,9 @@
<URL value="@../css/styles.css" />
</stylesheets>
<headerText>
<Label text="添加/编辑项目" styleClass="dialog-title"/>
</headerText>
<!-- 对话框标题。
注意headerText 节点只能是纯文本,如果放 Label 会显示成 Label@xxx 文本 -->
<headerText>添加/编辑项目</headerText>
<content>
<VBox spacing="16" style="-fx-padding: 20;">
@@ -36,6 +36,14 @@
<!-- SVN URL -->
<Label text="SVN URL:" GridPane.columnIndex="0" GridPane.rowIndex="2"/>
<TextField fx:id="svnUrlField" promptText="请输入SVN仓库URL" GridPane.columnIndex="1" GridPane.rowIndex="2"/>
<!-- SVN 用户名 -->
<Label text="SVN 用户名:" GridPane.columnIndex="0" GridPane.rowIndex="3"/>
<TextField fx:id="usernameField" promptText="可选,留空则使用默认认证" GridPane.columnIndex="1" GridPane.rowIndex="3"/>
<!-- SVN 密码 -->
<Label text="SVN 密码:" GridPane.columnIndex="0" GridPane.rowIndex="4"/>
<PasswordField fx:id="passwordField" promptText="可选,留空则使用默认认证" GridPane.columnIndex="1" GridPane.rowIndex="4"/>
</GridPane>
</VBox>
</content>