chore: initial project setup

This commit is contained in:
liu
2026-02-03 23:24:32 +08:00
commit 28b517da40
32 changed files with 3776 additions and 0 deletions

View File

@@ -0,0 +1,48 @@
package com.svnmanager;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
/**
* SVN管理器主应用
*/
public class MainApp extends Application {
private static final Logger logger = LoggerFactory.getLogger(MainApp.class);
@Override
public void start(Stage primaryStage) {
try {
logger.info("启动SVN管理器应用");
FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/main.fxml"));
Scene scene = new Scene(loader.load(), 1200, 800);
primaryStage.setTitle("SVN管理器 - 多项目管理");
primaryStage.setScene(scene);
primaryStage.setMinWidth(1000);
primaryStage.setMinHeight(600);
// 设置窗口关闭事件
primaryStage.setOnCloseRequest(e -> {
logger.info("应用关闭");
System.exit(0);
});
primaryStage.show();
logger.info("应用启动成功");
} catch (IOException e) {
logger.error("启动应用失败", e);
e.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
}
}

View File

@@ -0,0 +1,559 @@
package com.svnmanager.controller;
import com.svnmanager.model.Project;
import com.svnmanager.model.SvnFileStatus;
import com.svnmanager.model.SvnStatus;
import com.svnmanager.service.*;
import com.svnmanager.util.ConfigUtil;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.*;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.TimeoutException;
/**
* 主窗口控制器
*/
public class MainController {
private static final Logger logger = LoggerFactory.getLogger(MainController.class);
@FXML private VBox sidebar;
@FXML private VBox projectList;
@FXML private Label projectCountLabel;
@FXML private Button addProjectButton;
@FXML private Label projectTitleLabel;
@FXML private Label projectPathLabel;
@FXML private Button refreshButton;
@FXML private Button executeButton;
@FXML private Button checkoutButton;
@FXML private Button updateButton;
@FXML private Button commitButton;
@FXML private Button statusButton;
@FXML private Button logButton;
@FXML private Button diffButton;
@FXML private Button infoButton;
@FXML private Label currentVersionLabel;
@FXML private Label workingCopyLabel;
@FXML private Label modifiedFilesLabel;
@FXML private TableView<SvnFileStatus> fileStatusTable;
@FXML private TableColumn<SvnFileStatus, String> statusColumn;
@FXML private TableColumn<SvnFileStatus, String> pathColumn;
@FXML private TableColumn<SvnFileStatus, String> actionColumn;
private Project currentProject;
private ObservableList<SvnFileStatus> fileStatusList;
private ObservableList<Project> projects;
// 服务实例
private CheckoutService checkoutService;
private UpdateService updateService;
private CommitService commitService;
private StatusService statusService;
private LogService logService;
private DiffService diffService;
private InfoService infoService;
@FXML
public void initialize() {
logger.info("初始化主控制器");
// 初始化服务
checkoutService = new CheckoutService();
updateService = new UpdateService();
commitService = new CommitService();
statusService = new StatusService();
logService = new LogService();
diffService = new DiffService();
infoService = new InfoService();
// 初始化文件状态列表
fileStatusList = FXCollections.observableArrayList();
fileStatusTable.setItems(fileStatusList);
// 配置表格列
statusColumn.setCellValueFactory(data -> {
SvnFileStatus.FileStatus status = data.getValue().getStatus();
return new javafx.beans.property.SimpleStringProperty(status.getDisplayName());
});
pathColumn.setCellValueFactory(data ->
new javafx.beans.property.SimpleStringProperty(data.getValue().getPath()));
actionColumn.setCellFactory(param -> new TableCell<SvnFileStatus, String>() {
private final Button viewButton = new Button("查看");
@Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setGraphic(null);
} else {
viewButton.setOnAction(e -> handleViewFile(getTableView().getItems().get(getIndex())));
setGraphic(viewButton);
}
}
});
// 加载项目列表
loadProjects();
}
/**
* 加载项目列表
*/
private void loadProjects() {
projects = FXCollections.observableArrayList(ConfigUtil.loadProjects());
projectList.getChildren().clear();
for (Project project : projects) {
VBox projectCard = createProjectCard(project);
projectList.getChildren().add(projectCard);
}
updateProjectCount();
}
/**
* 创建项目卡片
*/
private VBox createProjectCard(Project project) {
VBox card = new VBox(8);
card.getStyleClass().add("project-card");
card.setPrefWidth(280);
card.setPadding(new javafx.geometry.Insets(16));
HBox content = new HBox(12);
// 项目图标
Circle icon = new Circle(24);
icon.setFill(Color.web("#6366f1"));
VBox info = new VBox(4);
Label nameLabel = new Label(project.getName());
nameLabel.getStyleClass().add("project-name");
Label pathLabel = new Label(project.getPath());
pathLabel.getStyleClass().add("project-path");
pathLabel.setWrapText(true);
// 状态徽章
HBox statusBox = new HBox(4);
Circle statusDot = new Circle(4);
statusDot.getStyleClass().add("status-dot");
Label statusLabel = new Label(project.getStatus().getDisplayName());
statusBox.getChildren().addAll(statusDot, statusLabel);
statusBox.getStyleClass().add("status-badge");
info.getChildren().addAll(nameLabel, pathLabel, statusBox);
content.getChildren().addAll(icon, info);
card.getChildren().add(content);
// 点击事件
card.setOnMouseClicked(e -> selectProject(project));
return card;
}
/**
* 选择项目
*/
private void selectProject(Project project) {
currentProject = project;
projectTitleLabel.setText(project.getName());
projectPathLabel.setText(project.getPath());
// 更新项目卡片样式
for (int i = 0; i < projectList.getChildren().size(); i++) {
VBox card = (VBox) projectList.getChildren().get(i);
if (i == projects.indexOf(project)) {
card.getStyleClass().add("active");
} else {
card.getStyleClass().remove("active");
}
}
// 刷新项目状态
refreshProjectStatus();
}
/**
* 刷新项目状态
*/
private void refreshProjectStatus() {
if (currentProject == null) {
return;
}
Task<Void> task = new Task<Void>() {
@Override
protected Void call() throws Exception {
try {
// 获取SVN信息
com.svnmanager.model.SvnInfo info = infoService.getInfo(currentProject.getPath());
if (info != null && info.getRevision() != null) {
Platform.runLater(() -> {
currentVersionLabel.setText("r" + info.getRevision());
workingCopyLabel.setText("r" + info.getRevision());
});
}
// 获取状态
SvnStatus status = statusService.getStatus(currentProject.getPath());
Platform.runLater(() -> {
fileStatusList.clear();
fileStatusList.addAll(status.getFiles());
modifiedFilesLabel.setText(String.valueOf(status.getTotalChangedFiles()));
});
} catch (IllegalArgumentException e) {
// 无效的工作副本
logger.warn("无效的工作副本: {}", e.getMessage());
Platform.runLater(() -> {
currentVersionLabel.setText("r0");
workingCopyLabel.setText("r0");
modifiedFilesLabel.setText("0");
fileStatusList.clear();
showWarning("警告", "无效的SVN工作副本: " + e.getMessage());
});
} catch (Exception e) {
logger.error("刷新项目状态失败", e);
Platform.runLater(() -> {
showError("刷新失败", e.getMessage());
});
}
return null;
}
};
new Thread(task).start();
}
/**
* 更新项目计数
*/
private void updateProjectCount() {
projectCountLabel.setText("" + projects.size() + " 个项目");
}
@FXML
private void handleAddProject() {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/project-dialog.fxml"));
DialogPane dialogPane = loader.load();
ProjectDialogController controller = loader.getController();
Dialog<ButtonType> dialog = new Dialog<>();
dialog.setDialogPane(dialogPane);
dialog.setTitle("添加项目");
dialog.showAndWait().ifPresent(result -> {
if (result == ButtonType.OK) {
Project project = controller.getProject();
if (project != null) {
ConfigUtil.addProject(project);
loadProjects();
}
}
});
} catch (IOException e) {
logger.error("打开添加项目对话框失败", e);
showError("错误", "无法打开添加项目对话框");
}
}
@FXML
private void handleRefresh() {
refreshProjectStatus();
}
@FXML
private void handleExecute() {
// 执行操作按钮的功能可以根据需要实现
showInfo("提示", "请选择具体的操作");
}
@FXML
private void handleCheckout() {
if (currentProject == null) {
showWarning("警告", "请先选择项目");
return;
}
// 实现Checkout功能
showInfo("提示", "Checkout功能待实现");
}
@FXML
private void handleUpdate() {
if (currentProject == null) {
showWarning("警告", "请先选择项目");
return;
}
Task<UpdateService.UpdateResult> task = new Task<UpdateService.UpdateResult>() {
@Override
protected UpdateService.UpdateResult call() throws Exception {
return updateService.update(currentProject.getPath());
}
};
task.setOnSucceeded(e -> {
UpdateService.UpdateResult result = task.getValue();
if (result.isSuccess()) {
showInfo("成功", "更新成功,版本: " + result.getRevision());
refreshProjectStatus();
} else {
showError("失败", result.getError());
}
});
task.setOnFailed(e -> {
showError("错误", task.getException().getMessage());
});
new Thread(task).start();
}
@FXML
private void handleCommit() {
if (currentProject == null) {
showWarning("警告", "请先选择项目");
return;
}
TextInputDialog dialog = new TextInputDialog();
dialog.setTitle("提交修改");
dialog.setHeaderText("请输入提交消息");
dialog.setContentText("消息:");
dialog.showAndWait().ifPresent(message -> {
if (message.trim().isEmpty()) {
showWarning("警告", "提交消息不能为空");
return;
}
Task<CommitService.CommitResult> task = new Task<CommitService.CommitResult>() {
@Override
protected CommitService.CommitResult call() throws Exception {
return commitService.commit(currentProject.getPath(), message);
}
};
task.setOnSucceeded(e -> {
CommitService.CommitResult result = task.getValue();
if (result.isSuccess()) {
showInfo("成功", "提交成功,版本: " + result.getRevision());
refreshProjectStatus();
} else {
showError("失败", result.getError());
}
});
task.setOnFailed(e -> {
showError("错误", task.getException().getMessage());
});
new Thread(task).start();
});
}
@FXML
private void handleStatus() {
if (currentProject == null) {
showWarning("警告", "请先选择项目");
return;
}
Task<SvnStatus> task = new Task<SvnStatus>() {
@Override
protected SvnStatus call() throws Exception {
return statusService.getStatus(currentProject.getPath());
}
};
task.setOnSucceeded(e -> {
SvnStatus status = task.getValue();
fileStatusList.clear();
fileStatusList.addAll(status.getFiles());
modifiedFilesLabel.setText(String.valueOf(status.getTotalChangedFiles()));
showInfo("状态", "" + status.getTotalChangedFiles() + " 个文件有变更");
});
task.setOnFailed(e -> {
showError("错误", task.getException().getMessage());
});
new Thread(task).start();
}
@FXML
private void handleLog() {
if (currentProject == null) {
showWarning("警告", "请先选择项目");
return;
}
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);
}
};
task.setOnSucceeded(e -> {
List<com.svnmanager.model.SvnLog> logs = task.getValue();
showLogDialog(logs);
});
task.setOnFailed(e -> {
showError("错误", task.getException().getMessage());
});
new Thread(task).start();
}
@FXML
private void handleDiff() {
if (currentProject == null) {
showWarning("警告", "请先选择项目");
return;
}
Task<String> task = new Task<String>() {
@Override
protected String call() throws Exception {
return diffService.getDiff(currentProject.getPath());
}
};
task.setOnSucceeded(e -> {
String diff = task.getValue();
showDiffDialog(diff);
});
task.setOnFailed(e -> {
showError("错误", task.getException().getMessage());
});
new Thread(task).start();
}
@FXML
private void handleInfo() {
if (currentProject == null) {
showWarning("警告", "请先选择项目");
return;
}
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());
}
};
task.setOnSucceeded(e -> {
com.svnmanager.model.SvnInfo info = task.getValue();
showInfoDialog(info);
});
task.setOnFailed(e -> {
showError("错误", task.getException().getMessage());
});
new Thread(task).start();
}
private void handleViewFile(SvnFileStatus fileStatus) {
// 实现查看文件功能
showInfo("文件", fileStatus.getPath());
}
private void showLogDialog(List<com.svnmanager.model.SvnLog> logs) {
Dialog<Void> dialog = new Dialog<>();
dialog.setTitle("SVN日志");
TextArea textArea = new TextArea();
StringBuilder sb = new StringBuilder();
for (com.svnmanager.model.SvnLog log : logs) {
sb.append("r").append(log.getRevision()).append(" | ")
.append(log.getAuthor()).append(" | ")
.append(log.getDate()).append("\n")
.append(log.getMessage()).append("\n\n");
}
textArea.setText(sb.toString());
textArea.setEditable(false);
dialog.getDialogPane().setContent(textArea);
dialog.getDialogPane().getButtonTypes().add(ButtonType.CLOSE);
dialog.showAndWait();
}
private void showDiffDialog(String diff) {
Dialog<Void> dialog = new Dialog<>();
dialog.setTitle("差异对比");
TextArea textArea = new TextArea(diff);
textArea.setEditable(false);
textArea.setPrefSize(800, 600);
dialog.getDialogPane().setContent(textArea);
dialog.getDialogPane().getButtonTypes().add(ButtonType.CLOSE);
dialog.showAndWait();
}
private void showInfoDialog(com.svnmanager.model.SvnInfo info) {
Dialog<Void> dialog = new Dialog<>();
dialog.setTitle("SVN信息");
TextArea textArea = new TextArea();
StringBuilder sb = new StringBuilder();
sb.append("路径: ").append(info.getPath()).append("\n");
sb.append("URL: ").append(info.getUrl()).append("\n");
sb.append("版本: ").append(info.getRevision()).append("\n");
sb.append("仓库根: ").append(info.getRepositoryRoot()).append("\n");
sb.append("最后修改作者: ").append(info.getLastChangedAuthor()).append("\n");
sb.append("最后修改版本: ").append(info.getLastChangedRev()).append("\n");
sb.append("最后修改日期: ").append(info.getLastChangedDate()).append("\n");
textArea.setText(sb.toString());
textArea.setEditable(false);
dialog.getDialogPane().setContent(textArea);
dialog.getDialogPane().getButtonTypes().add(ButtonType.CLOSE);
dialog.showAndWait();
}
private void showError(String title, String message) {
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setTitle(title);
alert.setContentText(message);
alert.showAndWait();
}
private void showWarning(String title, String message) {
Alert alert = new Alert(Alert.AlertType.WARNING);
alert.setTitle(title);
alert.setContentText(message);
alert.showAndWait();
}
private void showInfo(String title, String message) {
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setTitle(title);
alert.setContentText(message);
alert.showAndWait();
}
}

View File

@@ -0,0 +1,79 @@
package com.svnmanager.controller;
import com.svnmanager.model.Project;
import javafx.fxml.FXML;
import javafx.scene.control.TextField;
import javafx.stage.DirectoryChooser;
import javafx.stage.Stage;
import java.io.File;
/**
* 项目对话框控制器
*/
public class ProjectDialogController {
@FXML private TextField nameField;
@FXML private TextField pathField;
@FXML private TextField svnUrlField;
private Project project;
@FXML
public void initialize() {
// 初始化
}
/**
* 浏览路径
*/
@FXML
private void handleBrowsePath() {
DirectoryChooser directoryChooser = new DirectoryChooser();
directoryChooser.setTitle("选择项目目录");
File selectedDirectory = directoryChooser.showDialog(pathField.getScene().getWindow());
if (selectedDirectory != null) {
pathField.setText(selectedDirectory.getAbsolutePath());
}
}
/**
* 获取项目
*
* @return 项目对象
*/
public Project getProject() {
String name = nameField.getText().trim();
String path = pathField.getText().trim();
String svnUrl = svnUrlField.getText().trim();
if (name.isEmpty() || path.isEmpty()) {
return null;
}
if (project == null) {
project = new Project();
}
project.setName(name);
project.setPath(path);
project.setSvnUrl(svnUrl);
return project;
}
/**
* 设置项目(用于编辑)
*
* @param project 项目对象
*/
public void setProject(Project project) {
this.project = project;
if (project != null) {
nameField.setText(project.getName());
pathField.setText(project.getPath());
svnUrlField.setText(project.getSvnUrl());
}
}
}

View File

@@ -0,0 +1,117 @@
package com.svnmanager.model;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* 项目数据模型
*/
public class Project {
@JsonProperty("id")
private String id;
@JsonProperty("name")
private String name;
@JsonProperty("path")
private String path;
@JsonProperty("svnUrl")
private String svnUrl;
@JsonProperty("currentVersion")
private String currentVersion;
@JsonProperty("workingCopyVersion")
private String workingCopyVersion;
@JsonProperty("status")
private ProjectStatus status;
public Project() {
this.status = ProjectStatus.UNKNOWN;
}
public Project(String id, String name, String path, String svnUrl) {
this.id = id;
this.name = name;
this.path = path;
this.svnUrl = svnUrl;
this.status = ProjectStatus.UNKNOWN;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public String getSvnUrl() {
return svnUrl;
}
public void setSvnUrl(String svnUrl) {
this.svnUrl = svnUrl;
}
public String getCurrentVersion() {
return currentVersion;
}
public void setCurrentVersion(String currentVersion) {
this.currentVersion = currentVersion;
}
public String getWorkingCopyVersion() {
return workingCopyVersion;
}
public void setWorkingCopyVersion(String workingCopyVersion) {
this.workingCopyVersion = workingCopyVersion;
}
public ProjectStatus getStatus() {
return status;
}
public void setStatus(ProjectStatus status) {
this.status = status;
}
/**
* 项目状态枚举
*/
public enum ProjectStatus {
SYNCED("已同步"),
UPDATES_AVAILABLE("有更新"),
DISCONNECTED("未连接"),
UNKNOWN("未知");
private final String displayName;
ProjectStatus(String displayName) {
this.displayName = displayName;
}
public String getDisplayName() {
return displayName;
}
}
}

View File

@@ -0,0 +1,91 @@
package com.svnmanager.model;
/**
* SVN文件状态模型
*/
public class SvnFileStatus {
private String path;
private FileStatus status;
private String workingCopyStatus;
private String repositoryStatus;
public SvnFileStatus() {
}
public SvnFileStatus(String path, FileStatus status) {
this.path = path;
this.status = status;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public FileStatus getStatus() {
return status;
}
public void setStatus(FileStatus status) {
this.status = status;
}
public String getWorkingCopyStatus() {
return workingCopyStatus;
}
public void setWorkingCopyStatus(String workingCopyStatus) {
this.workingCopyStatus = workingCopyStatus;
}
public String getRepositoryStatus() {
return repositoryStatus;
}
public void setRepositoryStatus(String repositoryStatus) {
this.repositoryStatus = repositoryStatus;
}
/**
* 文件状态枚举
*/
public enum FileStatus {
MODIFIED('M', "已修改"),
ADDED('A', "已添加"),
DELETED('D', "已删除"),
CONFLICTED('C', "冲突"),
UNVERSIONED('?', "未版本控制"),
MISSING('!', "缺失"),
EXTERNAL('X', "外部"),
IGNORED('I', "已忽略"),
NORMAL(' ', "正常");
private final char code;
private final String displayName;
FileStatus(char code, String displayName) {
this.code = code;
this.displayName = displayName;
}
public char getCode() {
return code;
}
public String getDisplayName() {
return displayName;
}
public static FileStatus fromCode(char code) {
for (FileStatus status : values()) {
if (status.code == code) {
return status;
}
}
return NORMAL;
}
}
}

View File

@@ -0,0 +1,100 @@
package com.svnmanager.model;
/**
* SVN信息模型
*/
public class SvnInfo {
private String path;
private String url;
private String repositoryRoot;
private String repositoryUuid;
private String revision;
private String nodeKind;
private String schedule;
private String lastChangedAuthor;
private String lastChangedRev;
private String lastChangedDate;
public SvnInfo() {
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getRepositoryRoot() {
return repositoryRoot;
}
public void setRepositoryRoot(String repositoryRoot) {
this.repositoryRoot = repositoryRoot;
}
public String getRepositoryUuid() {
return repositoryUuid;
}
public void setRepositoryUuid(String repositoryUuid) {
this.repositoryUuid = repositoryUuid;
}
public String getRevision() {
return revision;
}
public void setRevision(String revision) {
this.revision = revision;
}
public String getNodeKind() {
return nodeKind;
}
public void setNodeKind(String nodeKind) {
this.nodeKind = nodeKind;
}
public String getSchedule() {
return schedule;
}
public void setSchedule(String schedule) {
this.schedule = schedule;
}
public String getLastChangedAuthor() {
return lastChangedAuthor;
}
public void setLastChangedAuthor(String lastChangedAuthor) {
this.lastChangedAuthor = lastChangedAuthor;
}
public String getLastChangedRev() {
return lastChangedRev;
}
public void setLastChangedRev(String lastChangedRev) {
this.lastChangedRev = lastChangedRev;
}
public String getLastChangedDate() {
return lastChangedDate;
}
public void setLastChangedDate(String lastChangedDate) {
this.lastChangedDate = lastChangedDate;
}
}

View File

@@ -0,0 +1,72 @@
package com.svnmanager.model;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
/**
* SVN日志模型
*/
public class SvnLog {
private String revision;
private String author;
private LocalDateTime date;
private String message;
private List<String> changedPaths;
public SvnLog() {
this.changedPaths = new ArrayList<>();
}
public SvnLog(String revision, String author, LocalDateTime date, String message) {
this.revision = revision;
this.author = author;
this.date = date;
this.message = message;
this.changedPaths = new ArrayList<>();
}
public String getRevision() {
return revision;
}
public void setRevision(String revision) {
this.revision = revision;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public LocalDateTime getDate() {
return date;
}
public void setDate(LocalDateTime date) {
this.date = date;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public List<String> getChangedPaths() {
return changedPaths;
}
public void setChangedPaths(List<String> changedPaths) {
this.changedPaths = changedPaths;
}
public void addChangedPath(String path) {
this.changedPaths.add(path);
}
}

View File

@@ -0,0 +1,95 @@
package com.svnmanager.model;
import java.util.ArrayList;
import java.util.List;
/**
* SVN状态模型
*/
public class SvnStatus {
private String workingCopyPath;
private String revision;
private List<SvnFileStatus> files;
private int modifiedCount;
private int addedCount;
private int deletedCount;
private int conflictedCount;
public SvnStatus() {
this.files = new ArrayList<>();
}
public String getWorkingCopyPath() {
return workingCopyPath;
}
public void setWorkingCopyPath(String workingCopyPath) {
this.workingCopyPath = workingCopyPath;
}
public String getRevision() {
return revision;
}
public void setRevision(String revision) {
this.revision = revision;
}
public List<SvnFileStatus> getFiles() {
return files;
}
public void setFiles(List<SvnFileStatus> files) {
this.files = files;
updateCounts();
}
public void addFile(SvnFileStatus file) {
this.files.add(file);
updateCounts();
}
public int getModifiedCount() {
return modifiedCount;
}
public int getAddedCount() {
return addedCount;
}
public int getDeletedCount() {
return deletedCount;
}
public int getConflictedCount() {
return conflictedCount;
}
public int getTotalChangedFiles() {
return modifiedCount + addedCount + deletedCount + conflictedCount;
}
private void updateCounts() {
modifiedCount = 0;
addedCount = 0;
deletedCount = 0;
conflictedCount = 0;
for (SvnFileStatus file : files) {
switch (file.getStatus()) {
case MODIFIED:
modifiedCount++;
break;
case ADDED:
addedCount++;
break;
case DELETED:
deletedCount++;
break;
case CONFLICTED:
conflictedCount++;
break;
}
}
}
}

View File

@@ -0,0 +1,67 @@
package com.svnmanager.service;
import com.svnmanager.util.ProcessUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeoutException;
/**
* Checkout服务
*/
public class CheckoutService extends SvnService {
private static final Logger logger = LoggerFactory.getLogger(CheckoutService.class);
/**
* 检出SVN仓库
*
* @param svnUrl SVN仓库URL
* @param targetPath 目标路径
* @param revision 版本号可选null表示最新版本
* @return 是否成功
* @throws IOException IO异常
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
public boolean checkout(String svnUrl, String targetPath, String revision)
throws IOException, InterruptedException, TimeoutException {
logger.info("检出仓库: {} 到 {}", svnUrl, targetPath);
List<String> args = new ArrayList<>();
args.add(svnUrl);
args.add(targetPath);
if (revision != null && !revision.isEmpty()) {
args.add("-r");
args.add(revision);
}
ProcessUtil.ProcessResult result = executeSvnCommand("checkout", args, null);
if (result.isSuccess()) {
logger.info("检出成功");
return true;
} else {
logger.error("检出失败: {}", result.getErrorAsString());
return false;
}
}
/**
* 检出SVN仓库最新版本
*
* @param svnUrl SVN仓库URL
* @param targetPath 目标路径
* @return 是否成功
* @throws IOException IO异常
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
public boolean checkout(String svnUrl, String targetPath)
throws IOException, InterruptedException, TimeoutException {
return checkout(svnUrl, targetPath, null);
}
}

View File

@@ -0,0 +1,134 @@
package com.svnmanager.service;
import com.svnmanager.util.ProcessUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeoutException;
/**
* Commit服务
*/
public class CommitService extends SvnService {
private static final Logger logger = LoggerFactory.getLogger(CommitService.class);
/**
* 提交修改
*
* @param workingDirectory 工作目录
* @param message 提交消息
* @param files 要提交的文件列表null表示提交所有修改
* @return 提交结果
* @throws IOException IO异常
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
public CommitResult commit(String workingDirectory, String message, List<String> files)
throws IOException, InterruptedException, TimeoutException {
logger.info("提交修改: {}", workingDirectory);
if (!isValidWorkingCopy(workingDirectory)) {
throw new IllegalArgumentException("无效的SVN工作副本: " + workingDirectory);
}
if (message == null || message.trim().isEmpty()) {
throw new IllegalArgumentException("提交消息不能为空");
}
List<String> args = new ArrayList<>();
args.add("-m");
args.add(message);
if (files != null && !files.isEmpty()) {
args.addAll(files);
}
ProcessUtil.ProcessResult result = executeSvnCommand("commit", args, workingDirectory);
CommitResult commitResult = new CommitResult();
commitResult.setSuccess(result.isSuccess());
commitResult.setOutput(result.getOutputAsString());
commitResult.setError(result.getErrorAsString());
if (result.isSuccess()) {
// 解析提交后的版本号
String output = result.getOutputAsString();
String revisionLine = output.lines()
.filter(line -> line.contains("Committed revision"))
.findFirst()
.orElse("");
if (!revisionLine.isEmpty()) {
String[] parts = revisionLine.split(" ");
if (parts.length > 0) {
String rev = parts[parts.length - 1].replace(".", "");
commitResult.setRevision(rev);
}
}
logger.info("提交成功,版本: {}", commitResult.getRevision());
} else {
logger.error("提交失败: {}", result.getErrorAsString());
}
return commitResult;
}
/**
* 提交所有修改
*
* @param workingDirectory 工作目录
* @param message 提交消息
* @return 提交结果
* @throws IOException IO异常
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
public CommitResult commit(String workingDirectory, String message)
throws IOException, InterruptedException, TimeoutException {
return commit(workingDirectory, message, null);
}
/**
* 提交结果
*/
public static class CommitResult {
private boolean success;
private String revision;
private String output;
private String error;
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public String getRevision() {
return revision;
}
public void setRevision(String revision) {
this.revision = revision;
}
public String getOutput() {
return output;
}
public void setOutput(String output) {
this.output = output;
}
public String getError() {
return error;
}
public void setError(String error) {
this.error = error;
}
}
}

View File

@@ -0,0 +1,64 @@
package com.svnmanager.service;
import com.svnmanager.util.ProcessUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeoutException;
/**
* Diff服务
*/
public class DiffService extends SvnService {
private static final Logger logger = LoggerFactory.getLogger(DiffService.class);
/**
* 获取差异
*
* @param workingDirectory 工作目录
* @param filePath 文件路径null表示所有文件
* @return 差异内容
* @throws IOException IO异常
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
public String getDiff(String workingDirectory, String filePath)
throws IOException, InterruptedException, TimeoutException {
logger.debug("获取差异: {}", workingDirectory);
if (!isValidWorkingCopy(workingDirectory)) {
throw new IllegalArgumentException("无效的SVN工作副本: " + workingDirectory);
}
List<String> args = new ArrayList<>();
if (filePath != null && !filePath.isEmpty()) {
args.add(filePath);
}
ProcessUtil.ProcessResult result = executeSvnCommand("diff", args, workingDirectory);
if (result.isSuccess()) {
return result.getOutputAsString();
} else {
logger.warn("获取差异失败: {}", result.getErrorAsString());
return result.getErrorAsString();
}
}
/**
* 获取所有文件的差异
*
* @param workingDirectory 工作目录
* @return 差异内容
* @throws IOException IO异常
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
public String getDiff(String workingDirectory)
throws IOException, InterruptedException, TimeoutException {
return getDiff(workingDirectory, null);
}
}

View File

@@ -0,0 +1,84 @@
package com.svnmanager.service;
import com.svnmanager.model.SvnInfo;
import com.svnmanager.util.ProcessUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* Info服务
*/
public class InfoService extends SvnService {
private static final Logger logger = LoggerFactory.getLogger(InfoService.class);
/**
* 获取SVN信息
*
* @param workingDirectory 工作目录
* @return SVN信息
* @throws IOException IO异常
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
public SvnInfo getInfo(String workingDirectory)
throws IOException, InterruptedException, TimeoutException {
logger.debug("获取信息: {}", workingDirectory);
if (!isValidWorkingCopy(workingDirectory)) {
throw new IllegalArgumentException("无效的SVN工作副本: " + workingDirectory);
}
ProcessUtil.ProcessResult result = executeSvnCommand("info", workingDirectory);
SvnInfo info = new SvnInfo();
if (result.isSuccess()) {
// 将输出行合并为一个字符串,便于解析
String output = String.join("\n", result.getOutput());
parseInfoOutput(output, info);
} else {
logger.warn("获取信息失败: {}", result.getErrorAsString());
}
return info;
}
/**
* 解析svn info输出
*
* @param output 命令输出
* @param info 信息对象
*/
private void parseInfoOutput(String output, SvnInfo info) {
for (String line : output.split("\n")) {
line = line.trim();
if (line.isEmpty()) {
continue;
}
if (line.startsWith("Path: ")) {
info.setPath(line.substring(6).trim());
} else if (line.startsWith("URL: ")) {
info.setUrl(line.substring(5).trim());
} else if (line.startsWith("Repository Root: ")) {
info.setRepositoryRoot(line.substring(17).trim());
} else if (line.startsWith("Repository UUID: ")) {
info.setRepositoryUuid(line.substring(18).trim());
} else if (line.startsWith("Revision: ")) {
info.setRevision(line.substring(11).trim());
} else if (line.startsWith("Node Kind: ")) {
info.setNodeKind(line.substring(11).trim());
} else if (line.startsWith("Schedule: ")) {
info.setSchedule(line.substring(11).trim());
} else if (line.startsWith("Last Changed Author: ")) {
info.setLastChangedAuthor(line.substring(22).trim());
} else if (line.startsWith("Last Changed Rev: ")) {
info.setLastChangedRev(line.substring(19).trim());
} else if (line.startsWith("Last Changed Date: ")) {
info.setLastChangedDate(line.substring(20).trim());
}
}
}
}

View File

@@ -0,0 +1,139 @@
package com.svnmanager.service;
import com.svnmanager.model.SvnLog;
import com.svnmanager.util.ProcessUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeoutException;
/**
* Log服务
*/
public class LogService extends SvnService {
private static final Logger logger = LoggerFactory.getLogger(LogService.class);
private static final DateTimeFormatter SVN_DATE_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
/**
* 获取SVN日志
*
* @param workingDirectory 工作目录
* @param limit 限制条数null表示不限制
* @return 日志列表
* @throws IOException IO异常
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
public List<SvnLog> getLog(String workingDirectory, Integer limit)
throws IOException, InterruptedException, TimeoutException {
logger.debug("获取日志: {}", workingDirectory);
if (!isValidWorkingCopy(workingDirectory)) {
throw new IllegalArgumentException("无效的SVN工作副本: " + workingDirectory);
}
List<String> args = new ArrayList<>();
args.add("-v"); // 详细输出
if (limit != null && limit > 0) {
args.add("-l");
args.add(String.valueOf(limit));
}
ProcessUtil.ProcessResult result = executeSvnCommand("log", args, workingDirectory);
List<SvnLog> logs = new ArrayList<>();
if (result.isSuccess()) {
// 将输出行合并为一个字符串,便于解析
String output = String.join("\n", result.getOutput());
parseLogOutput(output, logs);
} else {
logger.warn("获取日志失败: {}", result.getErrorAsString());
}
return logs;
}
/**
* 获取SVN日志默认限制50条
*
* @param workingDirectory 工作目录
* @return 日志列表
* @throws IOException IO异常
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
public List<SvnLog> getLog(String workingDirectory)
throws IOException, InterruptedException, TimeoutException {
return getLog(workingDirectory, 50);
}
/**
* 解析svn log输出
*
* @param output 命令输出
* @param logs 日志列表
*/
private void parseLogOutput(String output, List<SvnLog> logs) {
String[] lines = output.split("\n");
SvnLog currentLog = null;
for (String line : lines) {
line = line.trim();
if (line.startsWith("------------------------------------------------------------------------")) {
if (currentLog != null) {
logs.add(currentLog);
}
currentLog = new SvnLog();
continue;
}
if (currentLog == null) {
continue;
}
if (line.startsWith("r")) {
// 解析版本号、作者、日期
// 格式: "r1234 | author | 2024-01-01 12:00:00 +0800 (Mon, 01 Jan 2024) | 1 line"
String[] parts = line.split("\\|");
if (parts.length >= 3) {
currentLog.setRevision(parts[0].trim());
currentLog.setAuthor(parts[1].trim());
// 解析日期
String dateStr = parts[2].trim();
String datePart = dateStr.split("\\(")[0].trim();
try {
LocalDateTime date = LocalDateTime.parse(datePart, SVN_DATE_FORMATTER);
currentLog.setDate(date);
} catch (Exception e) {
logger.debug("解析日期失败: {}", datePart);
}
}
} else if (line.startsWith("Changed paths:")) {
// 跳过Changed paths标题
continue;
} else if (line.startsWith(" ")) {
// 变更路径
String path = line.trim();
currentLog.addChangedPath(path);
} else if (!line.isEmpty() && currentLog.getMessage() == null) {
// 提交消息
currentLog.setMessage(line);
} else if (!line.isEmpty() && currentLog.getMessage() != null) {
// 追加提交消息(多行)
currentLog.setMessage(currentLog.getMessage() + "\n" + line);
}
}
if (currentLog != null) {
logs.add(currentLog);
}
}
}

View File

@@ -0,0 +1,102 @@
package com.svnmanager.service;
import com.svnmanager.model.SvnFileStatus;
import com.svnmanager.model.SvnStatus;
import com.svnmanager.util.ProcessUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeoutException;
/**
* Status服务
*/
public class StatusService extends SvnService {
private static final Logger logger = LoggerFactory.getLogger(StatusService.class);
/**
* 获取工作副本状态
*
* @param workingDirectory 工作目录
* @return SVN状态
* @throws IOException IO异常
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
public SvnStatus getStatus(String workingDirectory)
throws IOException, InterruptedException, TimeoutException {
logger.debug("获取状态: {}", workingDirectory);
if (!isValidWorkingCopy(workingDirectory)) {
throw new IllegalArgumentException("无效的SVN工作副本: " + workingDirectory);
}
List<String> args = new ArrayList<>();
args.add("-v"); // 详细输出
ProcessUtil.ProcessResult result = executeSvnCommand("status", args, workingDirectory);
SvnStatus status = new SvnStatus();
status.setWorkingCopyPath(workingDirectory);
if (result.isSuccess()) {
// 将输出行合并为一个字符串,便于解析
String output = String.join("\n", result.getOutput());
parseStatusOutput(output, status);
} else {
logger.warn("获取状态失败: {}", result.getErrorAsString());
}
return status;
}
/**
* 解析svn status输出
*
* @param output 命令输出
* @param status 状态对象
*/
private void parseStatusOutput(String output, SvnStatus status) {
List<SvnFileStatus> files = new ArrayList<>();
for (String line : output.split("\n")) {
line = line.trim();
if (line.isEmpty()) {
continue;
}
// SVN status格式: "状态码 工作副本状态 版本号 文件路径"
// 例如: "M 1234 src/Main.java"
if (line.length() < 8) {
continue;
}
char workingCopyStatus = line.charAt(0);
char repositoryStatus = line.length() > 1 ? line.charAt(1) : ' ';
// 跳过标题行
if (workingCopyStatus == 'S' && line.contains("Status")) {
continue;
}
SvnFileStatus.FileStatus fileStatus = SvnFileStatus.FileStatus.fromCode(workingCopyStatus);
// 提取文件路径(跳过状态码和版本号)
String filePath = line.substring(8).trim();
if (filePath.isEmpty()) {
continue;
}
SvnFileStatus fileStatusObj = new SvnFileStatus(filePath, fileStatus);
fileStatusObj.setWorkingCopyStatus(String.valueOf(workingCopyStatus));
fileStatusObj.setRepositoryStatus(String.valueOf(repositoryStatus));
files.add(fileStatusObj);
}
status.setFiles(files);
}
}

View File

@@ -0,0 +1,64 @@
package com.svnmanager.service;
import com.svnmanager.util.ProcessUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeoutException;
/**
* SVN服务基类
*/
public abstract class SvnService {
protected static final Logger logger = LoggerFactory.getLogger(SvnService.class);
/**
* 执行SVN命令
*
* @param svnCommand SVN子命令
* @param args 命令参数
* @param workingDirectory 工作目录
* @return 命令执行结果
* @throws IOException IO异常
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
protected ProcessUtil.ProcessResult executeSvnCommand(String svnCommand, List<String> args, String workingDirectory)
throws IOException, InterruptedException, TimeoutException {
return ProcessUtil.executeSvnCommand(svnCommand, args, workingDirectory);
}
/**
* 执行SVN命令无参数
*
* @param svnCommand SVN子命令
* @param workingDirectory 工作目录
* @return 命令执行结果
* @throws IOException IO异常
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
protected ProcessUtil.ProcessResult executeSvnCommand(String svnCommand, String workingDirectory)
throws IOException, InterruptedException, TimeoutException {
return executeSvnCommand(svnCommand, new ArrayList<>(), workingDirectory);
}
/**
* 验证工作目录是否为有效的SVN工作副本
*
* @param workingDirectory 工作目录
* @return 是否为有效的SVN工作副本
*/
protected boolean isValidWorkingCopy(String workingDirectory) {
try {
ProcessUtil.ProcessResult result = executeSvnCommand("info", workingDirectory);
return result.isSuccess();
} catch (Exception e) {
logger.debug("验证工作副本失败: {}", e.getMessage());
return false;
}
}
}

View File

@@ -0,0 +1,126 @@
package com.svnmanager.service;
import com.svnmanager.util.ProcessUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeoutException;
/**
* Update服务
*/
public class UpdateService extends SvnService {
private static final Logger logger = LoggerFactory.getLogger(UpdateService.class);
/**
* 更新工作副本
*
* @param workingDirectory 工作目录
* @param revision 版本号可选null表示最新版本
* @return 更新结果
* @throws IOException IO异常
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
public UpdateResult update(String workingDirectory, String revision)
throws IOException, InterruptedException, TimeoutException {
logger.info("更新工作副本: {}", workingDirectory);
if (!isValidWorkingCopy(workingDirectory)) {
throw new IllegalArgumentException("无效的SVN工作副本: " + workingDirectory);
}
List<String> args = new ArrayList<>();
if (revision != null && !revision.isEmpty()) {
args.add("-r");
args.add(revision);
}
ProcessUtil.ProcessResult result = executeSvnCommand("update", args, workingDirectory);
UpdateResult updateResult = new UpdateResult();
updateResult.setSuccess(result.isSuccess());
updateResult.setOutput(result.getOutputAsString());
updateResult.setError(result.getErrorAsString());
if (result.isSuccess()) {
// 解析更新后的版本号
String output = result.getOutputAsString();
String revisionLine = output.lines()
.filter(line -> line.contains("Updated to revision"))
.findFirst()
.orElse("");
if (!revisionLine.isEmpty()) {
String[] parts = revisionLine.split(" ");
if (parts.length > 0) {
String rev = parts[parts.length - 1].replace(".", "");
updateResult.setRevision(rev);
}
}
logger.info("更新成功,版本: {}", updateResult.getRevision());
} else {
logger.error("更新失败: {}", result.getErrorAsString());
}
return updateResult;
}
/**
* 更新工作副本到最新版本
*
* @param workingDirectory 工作目录
* @return 更新结果
* @throws IOException IO异常
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
public UpdateResult update(String workingDirectory)
throws IOException, InterruptedException, TimeoutException {
return update(workingDirectory, null);
}
/**
* 更新结果
*/
public static class UpdateResult {
private boolean success;
private String revision;
private String output;
private String error;
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public String getRevision() {
return revision;
}
public void setRevision(String revision) {
this.revision = revision;
}
public String getOutput() {
return output;
}
public void setOutput(String output) {
this.output = output;
}
public String getError() {
return error;
}
public void setError(String error) {
this.error = error;
}
}
}

View File

@@ -0,0 +1,157 @@
package com.svnmanager.util;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.CollectionType;
import com.svnmanager.model.Project;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* 配置管理工具类
*/
public class ConfigUtil {
private static final Logger logger = LoggerFactory.getLogger(ConfigUtil.class);
private static final String CONFIG_DIR = System.getProperty("user.home") + File.separator + ".svn-manager";
private static final String PROJECTS_FILE = CONFIG_DIR + File.separator + "projects.json";
private static final ObjectMapper objectMapper = new ObjectMapper();
static {
// 确保配置目录存在
try {
Path configPath = Paths.get(CONFIG_DIR);
if (!Files.exists(configPath)) {
Files.createDirectories(configPath);
}
} catch (IOException e) {
logger.error("创建配置目录失败", e);
}
}
/**
* 加载项目列表
*
* @return 项目列表
*/
public static List<Project> loadProjects() {
File file = new File(PROJECTS_FILE);
if (!file.exists()) {
logger.info("项目配置文件不存在,返回空列表");
return new ArrayList<>();
}
try {
CollectionType listType = objectMapper.getTypeFactory()
.constructCollectionType(List.class, Project.class);
List<Project> projects = objectMapper.readValue(file, listType);
logger.info("成功加载 {} 个项目", projects.size());
return projects;
} catch (IOException e) {
logger.error("加载项目配置失败", e);
return new ArrayList<>();
}
}
/**
* 保存项目列表
*
* @param projects 项目列表
* @return 是否保存成功
*/
public static boolean saveProjects(List<Project> projects) {
File file = new File(PROJECTS_FILE);
try {
objectMapper.writerWithDefaultPrettyPrinter().writeValue(file, projects);
logger.info("成功保存 {} 个项目", projects.size());
return true;
} catch (IOException e) {
logger.error("保存项目配置失败", e);
return false;
}
}
/**
* 添加项目
*
* @param project 项目
* @return 是否添加成功
*/
public static boolean addProject(Project project) {
if (project.getId() == null || project.getId().isEmpty()) {
project.setId(UUID.randomUUID().toString());
}
List<Project> projects = loadProjects();
projects.add(project);
return saveProjects(projects);
}
/**
* 更新项目
*
* @param project 项目
* @return 是否更新成功
*/
public static boolean updateProject(Project project) {
List<Project> projects = loadProjects();
for (int i = 0; i < projects.size(); i++) {
if (projects.get(i).getId().equals(project.getId())) {
projects.set(i, project);
return saveProjects(projects);
}
}
return false;
}
/**
* 删除项目
*
* @param projectId 项目ID
* @return 是否删除成功
*/
public static boolean deleteProject(String projectId) {
List<Project> projects = loadProjects();
projects.removeIf(p -> p.getId().equals(projectId));
return saveProjects(projects);
}
/**
* 根据ID获取项目
*
* @param projectId 项目ID
* @return 项目如果不存在返回null
*/
public static Project getProjectById(String projectId) {
List<Project> projects = loadProjects();
return projects.stream()
.filter(p -> p.getId().equals(projectId))
.findFirst()
.orElse(null);
}
/**
* 获取配置目录路径
*
* @return 配置目录路径
*/
public static String getConfigDir() {
return CONFIG_DIR;
}
/**
* 获取项目配置文件路径
*
* @return 项目配置文件路径
*/
public static String getProjectsFilePath() {
return PROJECTS_FILE;
}
}

View File

@@ -0,0 +1,51 @@
package com.svnmanager.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 日志工具类
*/
public class LogUtil {
/**
* 获取Logger实例
*
* @param clazz 类
* @return Logger实例
*/
public static Logger getLogger(Class<?> clazz) {
return LoggerFactory.getLogger(clazz);
}
/**
* 记录错误日志
*
* @param logger Logger实例
* @param message 消息
* @param throwable 异常
*/
public static void logError(Logger logger, String message, Throwable throwable) {
logger.error(message, throwable);
}
/**
* 记录信息日志
*
* @param logger Logger实例
* @param message 消息
*/
public static void logInfo(Logger logger, String message) {
logger.info(message);
}
/**
* 记录调试日志
*
* @param logger Logger实例
* @param message 消息
*/
public static void logDebug(Logger logger, String message) {
logger.debug(message);
}
}

View File

@@ -0,0 +1,159 @@
package com.svnmanager.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* 进程执行工具类
*/
public class ProcessUtil {
private static final Logger logger = LoggerFactory.getLogger(ProcessUtil.class);
private static final int DEFAULT_TIMEOUT_SECONDS = 300;
/**
* 执行命令并返回输出
*
* @param command 命令数组
* @param workingDirectory 工作目录
* @return 命令输出行列表
* @throws IOException IO异常
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
public static ProcessResult executeCommand(String[] command, String workingDirectory)
throws IOException, InterruptedException, TimeoutException {
return executeCommand(command, workingDirectory, DEFAULT_TIMEOUT_SECONDS);
}
/**
* 执行命令并返回输出
*
* @param command 命令数组
* @param workingDirectory 工作目录
* @param timeoutSeconds 超时时间(秒)
* @return 命令输出结果
* @throws IOException IO异常
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
public static ProcessResult executeCommand(String[] command, String workingDirectory, int timeoutSeconds)
throws IOException, InterruptedException, TimeoutException {
logger.debug("执行命令: {}", String.join(" ", command));
logger.debug("工作目录: {}", workingDirectory);
ProcessBuilder processBuilder = new ProcessBuilder(command);
if (workingDirectory != null && !workingDirectory.isEmpty()) {
processBuilder.directory(new java.io.File(workingDirectory));
}
processBuilder.redirectErrorStream(true);
Process process = processBuilder.start();
List<String> outputLines = new ArrayList<>();
List<String> errorLines = new ArrayList<>();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream(), Charset.defaultCharset()));
BufferedReader errorReader = new BufferedReader(
new InputStreamReader(process.getErrorStream(), Charset.defaultCharset()))) {
// 读取标准输出
String line;
while ((line = reader.readLine()) != null) {
outputLines.add(line);
}
// 读取错误输出
while ((line = errorReader.readLine()) != null) {
errorLines.add(line);
}
}
boolean finished = process.waitFor(timeoutSeconds, TimeUnit.SECONDS);
if (!finished) {
process.destroyForcibly();
throw new TimeoutException("命令执行超时: " + String.join(" ", command));
}
int exitCode = process.exitValue();
ProcessResult result = new ProcessResult(exitCode, outputLines, errorLines);
logger.debug("命令执行完成,退出码: {}", exitCode);
if (exitCode != 0) {
logger.warn("命令执行失败: {}", String.join("\n", errorLines));
}
return result;
}
/**
* 执行SVN命令
*
* @param svnCommand SVN子命令如 "status", "update"
* @param args 命令参数
* @param workingDirectory 工作目录
* @return 命令输出结果
* @throws IOException IO异常
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
public static ProcessResult executeSvnCommand(String svnCommand, List<String> args, String workingDirectory)
throws IOException, InterruptedException, TimeoutException {
List<String> command = new ArrayList<>();
command.add("svn");
command.add(svnCommand);
if (args != null) {
command.addAll(args);
}
return executeCommand(command.toArray(new String[0]), workingDirectory);
}
/**
* 进程执行结果
*/
public static class ProcessResult {
private final int exitCode;
private final List<String> output;
private final List<String> error;
public ProcessResult(int exitCode, List<String> output, List<String> error) {
this.exitCode = exitCode;
this.output = output;
this.error = error;
}
public int getExitCode() {
return exitCode;
}
public List<String> getOutput() {
return output;
}
public List<String> getError() {
return error;
}
public String getOutputAsString() {
return String.join("\n", output);
}
public String getErrorAsString() {
return String.join("\n", error);
}
public boolean isSuccess() {
return exitCode == 0;
}
}
}